MOBILE-3839 ios: Implement secure storage plugin for iOS
parent
b8071a6946
commit
f93f2a973a
|
@ -13,4 +13,14 @@
|
|||
</config-file>
|
||||
<source-file src="src/android/SystemUI.java" target-dir="src/com/moodle/moodlemobile" />
|
||||
</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>
|
||||
|
|
|
@ -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.
|
||||
|
||||
import { SystemUI } from './plugins/SystemUI';
|
||||
import { SecureStorage } from './plugins/SecureStorage';
|
||||
|
||||
const api: MoodleAppPlugins = {
|
||||
systemUI: new SystemUI(),
|
||||
secureStorage: new SecureStorage(),
|
||||
};
|
||||
|
||||
// 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.
|
||||
|
||||
import { SystemUI } from '../src/ts/plugins/SystemUI';
|
||||
import { SecureStorage } from '../src/ts/plugins/SecureStorage';
|
||||
|
||||
declare global {
|
||||
|
||||
interface MoodleAppPlugins {
|
||||
systemUI: SystemUI;
|
||||
secureStorage: SecureStorage;
|
||||
}
|
||||
|
||||
interface Cordova {
|
||||
|
|
Loading…
Reference in New Issue