MOBILE-3839 ios: Implement secure storage plugin for iOS
parent
b8071a6946
commit
f93f2a973a
|
@ -13,4 +13,14 @@
|
||||||
</config-file>
|
</config-file>
|
||||||
<source-file src="src/android/SystemUI.java" target-dir="src/com/moodle/moodlemobile" />
|
<source-file src="src/android/SystemUI.java" target-dir="src/com/moodle/moodlemobile" />
|
||||||
</platform>
|
</platform>
|
||||||
|
<platform name="ios">
|
||||||
|
<config-file target="config.xml" parent="/*">
|
||||||
|
<feature name="SecureStorage">
|
||||||
|
<param name="ios-package" value="SecureStorage" />
|
||||||
|
</feature>
|
||||||
|
</config-file>
|
||||||
|
|
||||||
|
<header-file src="src/ios/SecureStorage.h" />
|
||||||
|
<source-file src="src/ios/SecureStorage.m" />
|
||||||
|
</platform>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <Cordova/CDVPlugin.h>
|
||||||
|
|
||||||
|
@interface SecureStorage : CDVPlugin {}
|
||||||
|
|
||||||
|
- (void)get:(CDVInvokedUrlCommand*)command;
|
||||||
|
- (void)store:(CDVInvokedUrlCommand*)command;
|
||||||
|
- (void)delete:(CDVInvokedUrlCommand*)command;
|
||||||
|
- (void)deleteCollection:(CDVInvokedUrlCommand*)command;
|
||||||
|
|
||||||
|
@end
|
|
@ -0,0 +1,218 @@
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <Cordova/CDVPlugin.h>
|
||||||
|
#import <Cordova/CDVPluginResult.h>
|
||||||
|
#import "SecureStorage.h"
|
||||||
|
|
||||||
|
@implementation SecureStorage
|
||||||
|
|
||||||
|
- (void)get:(CDVInvokedUrlCommand*)command {
|
||||||
|
NSArray* names = [command argumentAtIndex:0];
|
||||||
|
NSString* collection = [command argumentAtIndex:1 withDefault:@""];
|
||||||
|
NSMutableDictionary* result = [NSMutableDictionary new];
|
||||||
|
|
||||||
|
NSLog(@"SecureStorage: Get values with names %@ in collection %@", names, collection);
|
||||||
|
|
||||||
|
for (NSString* name in names) {
|
||||||
|
NSString* value = [self getValue:name inCollection:collection];
|
||||||
|
if (value != nil) {
|
||||||
|
result[name] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:result];
|
||||||
|
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)getValue:(NSString*)name inCollection:(NSString*)collection {
|
||||||
|
if ([name length] == 0) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSDictionary* query = @{
|
||||||
|
(id)kSecClass: (id)kSecClassGenericPassword,
|
||||||
|
(id)kSecAttrAccount: name,
|
||||||
|
(id)kSecAttrService: collection,
|
||||||
|
(id)kSecReturnData: @YES,
|
||||||
|
(id)kSecMatchLimit: (id)kSecMatchLimitOne
|
||||||
|
};
|
||||||
|
NSData* storedData = NULL;
|
||||||
|
|
||||||
|
OSStatus status = SecItemCopyMatching((CFDictionaryRef)query, (void *) &storedData);
|
||||||
|
|
||||||
|
if (status == errSecSuccess) {
|
||||||
|
return [[NSString alloc] initWithData:storedData encoding:NSUTF8StringEncoding];
|
||||||
|
} else if (status != errSecItemNotFound) {
|
||||||
|
NSLog(@"Error getting value for %@ in collection %@. Status: %d", name, collection, (int) status);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)store:(CDVInvokedUrlCommand*)command {
|
||||||
|
|
||||||
|
NSDictionary* data = [command argumentAtIndex:0];
|
||||||
|
NSString* collection = [command argumentAtIndex:1 withDefault:@""];
|
||||||
|
NSArray* names = [data allKeys];
|
||||||
|
BOOL error = false;
|
||||||
|
|
||||||
|
// Variables to be able to rollback changes if something fails.
|
||||||
|
NSMutableArray* insertedNames = [NSMutableArray new];
|
||||||
|
NSMutableDictionary* previousValues = [NSMutableDictionary new];
|
||||||
|
|
||||||
|
NSLog(@"SecureStorage: Store values with names %@ in collection %@", names, collection);
|
||||||
|
|
||||||
|
for (NSString* name in data) {
|
||||||
|
OSStatus status;
|
||||||
|
NSString* storedValue = [self getValue:name inCollection:collection];
|
||||||
|
|
||||||
|
if (storedValue != nil) {
|
||||||
|
status = [self updateName:name withValue:data[name] inCollection: collection];
|
||||||
|
} else {
|
||||||
|
status = [self addName:name withValue:data[name] inCollection: collection];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status != errSecSuccess) {
|
||||||
|
NSLog(@"Error storing value for %@ in collection %@. Status: %d", name, collection, (int) status);
|
||||||
|
error = true;
|
||||||
|
|
||||||
|
// Rollback.
|
||||||
|
for (NSString *name in insertedNames) {
|
||||||
|
[self deleteName:name fromCollection:collection];
|
||||||
|
}
|
||||||
|
for(NSString *name in previousValues) {
|
||||||
|
[self updateName:name withValue:previousValues[name] inCollection: collection];
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
} else if (storedValue != nil) {
|
||||||
|
previousValues[name] = storedValue;
|
||||||
|
} else {
|
||||||
|
[insertedNames addObject:name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CDVPluginResult* pluginResult;
|
||||||
|
if (error) {
|
||||||
|
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
|
||||||
|
messageAsString:@"Error storing one or more values in secure storage."];
|
||||||
|
} else {
|
||||||
|
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
|
||||||
|
}
|
||||||
|
|
||||||
|
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (OSStatus)addName:(NSString*)name withValue:(NSString*)value inCollection:(NSString*)collection {
|
||||||
|
NSData* newValue = [value dataUsingEncoding:NSUTF8StringEncoding];
|
||||||
|
NSMutableDictionary* query = [NSMutableDictionary new];
|
||||||
|
query[(id)kSecClass] = (id)kSecClassGenericPassword;
|
||||||
|
query[(id)kSecAttrAccount] = name;
|
||||||
|
query[(id)kSecAttrService] = collection;
|
||||||
|
query[(id)kSecAttrAccessible] = (id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
|
||||||
|
query[(id)kSecValueData] = newValue;
|
||||||
|
|
||||||
|
return SecItemAdd((CFDictionaryRef)query, nil);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (OSStatus)updateName:(NSString*)name withValue:(NSString*)value inCollection:(NSString*)collection {
|
||||||
|
NSData* newValue = [value dataUsingEncoding:NSUTF8StringEncoding];
|
||||||
|
NSMutableDictionary* query = [NSMutableDictionary new];
|
||||||
|
query[(id)kSecClass] = (id)kSecClassGenericPassword;
|
||||||
|
query[(id)kSecAttrAccount] = name;
|
||||||
|
query[(id)kSecAttrService] = collection;
|
||||||
|
query[(id)kSecAttrAccessible] = (id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
|
||||||
|
|
||||||
|
return SecItemUpdate((CFDictionaryRef)query, (CFDictionaryRef)@{
|
||||||
|
(id)kSecValueData: newValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)delete:(CDVInvokedUrlCommand*)command {
|
||||||
|
NSArray* names = [command argumentAtIndex:0];
|
||||||
|
NSString* collection = [command argumentAtIndex:1 withDefault:@""];
|
||||||
|
BOOL error = false;
|
||||||
|
|
||||||
|
// Variable to be able to rollback changes if something fails.
|
||||||
|
NSMutableDictionary* deletedValues = [NSMutableDictionary new];
|
||||||
|
|
||||||
|
NSLog(@"SecureStorage: Delete values with names %@ in collection %@", names, collection);
|
||||||
|
|
||||||
|
for (NSString* name in names) {
|
||||||
|
if ([name length] == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString* storedValue = [self getValue:name inCollection:collection];
|
||||||
|
if (storedValue == nil) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
OSStatus status = [self deleteName:name fromCollection:collection];
|
||||||
|
|
||||||
|
if (status != errSecSuccess && status != errSecItemNotFound) {
|
||||||
|
NSLog(@"Error deleting entry with name %@ in collection %@. Status: %d", name, collection, (int) status);
|
||||||
|
error = true;
|
||||||
|
|
||||||
|
// Rollback.
|
||||||
|
for (NSString *name in deletedValues) {
|
||||||
|
[self addName:name withValue:deletedValues[name] inCollection:collection];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
deletedValues[name] = storedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CDVPluginResult* pluginResult;
|
||||||
|
if (error) {
|
||||||
|
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
|
||||||
|
messageAsString:@"Error deleting one or more values from secure storage."];
|
||||||
|
} else {
|
||||||
|
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
|
||||||
|
}
|
||||||
|
|
||||||
|
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (OSStatus)deleteName:(NSString*)name fromCollection:(NSString*)collection {
|
||||||
|
NSDictionary* query = @{
|
||||||
|
(id)kSecClass: (id)kSecClassGenericPassword,
|
||||||
|
(id)kSecAttrAccount: name,
|
||||||
|
(id)kSecAttrService: collection,
|
||||||
|
};
|
||||||
|
|
||||||
|
return SecItemDelete((CFDictionaryRef)query);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)deleteCollection:(CDVInvokedUrlCommand*)command {
|
||||||
|
NSString* collection = [command argumentAtIndex:0 withDefault:@""];
|
||||||
|
|
||||||
|
if ([collection length] == 0) {
|
||||||
|
NSLog(@"SecureStorage: Collection cannot be empty in deleteCollection");
|
||||||
|
CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
|
||||||
|
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLog(@"SecureStorage: Delete all values in collection %@", collection);
|
||||||
|
|
||||||
|
NSDictionary* query = @{
|
||||||
|
(id)kSecClass: (id)kSecClassGenericPassword,
|
||||||
|
(id)kSecAttrService: collection,
|
||||||
|
};
|
||||||
|
|
||||||
|
OSStatus status = SecItemDelete((CFDictionaryRef)query);
|
||||||
|
|
||||||
|
CDVPluginResult* pluginResult;
|
||||||
|
if (status == errSecSuccess || status == errSecItemNotFound) {
|
||||||
|
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
|
||||||
|
} else {
|
||||||
|
NSLog(@"Error deleting all values in collection %@. Status: %d", collection, (int) status);
|
||||||
|
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_IO_EXCEPTION
|
||||||
|
messageAsString:@"Error deleting values from secure storage."];
|
||||||
|
}
|
||||||
|
|
||||||
|
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
|
@ -13,9 +13,11 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { SystemUI } from './plugins/SystemUI';
|
import { SystemUI } from './plugins/SystemUI';
|
||||||
|
import { SecureStorage } from './plugins/SecureStorage';
|
||||||
|
|
||||||
const api: MoodleAppPlugins = {
|
const api: MoodleAppPlugins = {
|
||||||
systemUI: new SystemUI(),
|
systemUI: new SystemUI(),
|
||||||
|
secureStorage: new SecureStorage(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// This is necessary to work around the default transpilation behavior,
|
// This is necessary to work around the default transpilation behavior,
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows retrieving and storing items in a secure storage.
|
||||||
|
*/
|
||||||
|
export class SecureStorage {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get one or more values.
|
||||||
|
*
|
||||||
|
* @param names Names of the values to get.
|
||||||
|
* @param collection The collection where the values are stored.
|
||||||
|
* @returns Object with name -> value. If a name isn't found it won't be included in the result.
|
||||||
|
*/
|
||||||
|
async get(names: string | string[], collection: string): Promise<Record<string, string>> {
|
||||||
|
if (typeof names === 'string') {
|
||||||
|
names = [names];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
cordova.exec(resolve, reject, 'SecureStorage', 'get', [names, collection]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set one or more values.
|
||||||
|
*
|
||||||
|
* @param data Object with values to store, in format name -> value. Null or undefined valid values will be ignored.
|
||||||
|
* @param collection The collection where to store the values.
|
||||||
|
*/
|
||||||
|
async store(data: Record<string, string>, collection: string): Promise<void> {
|
||||||
|
for (const name in data) {
|
||||||
|
const value = data[name];
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
delete data[name];
|
||||||
|
} else if (typeof value !== 'string') {
|
||||||
|
throw new Error(`SecureStorage: Invalid value for ${name}. Expected string, received ${typeof value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
cordova.exec(resolve, reject, 'SecureStorage', 'store', [data, collection]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete one or more values.
|
||||||
|
*
|
||||||
|
* @param names Names to delete.
|
||||||
|
* @param collection The collection where to delete the values.
|
||||||
|
*/
|
||||||
|
async delete(names: string | string[], collection: string): Promise<void> {
|
||||||
|
if (typeof names === 'string') {
|
||||||
|
names = [names];
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
cordova.exec(resolve, reject, 'SecureStorage', 'delete', [names, collection]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all values for a certain collection.
|
||||||
|
*
|
||||||
|
* @param collection The collection to delete.
|
||||||
|
*/
|
||||||
|
async deleteCollection(collection: string): Promise<void> {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
cordova.exec(resolve, reject, 'SecureStorage', 'deleteCollection', [collection]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -13,11 +13,13 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { SystemUI } from '../src/ts/plugins/SystemUI';
|
import { SystemUI } from '../src/ts/plugins/SystemUI';
|
||||||
|
import { SecureStorage } from '../src/ts/plugins/SecureStorage';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|
||||||
interface MoodleAppPlugins {
|
interface MoodleAppPlugins {
|
||||||
systemUI: SystemUI;
|
systemUI: SystemUI;
|
||||||
|
secureStorage: SecureStorage;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Cordova {
|
interface Cordova {
|
||||||
|
|
Loading…
Reference in New Issue