diff --git a/cordova-plugin-moodleapp/plugin.xml b/cordova-plugin-moodleapp/plugin.xml
index a6abaa381..e161454be 100644
--- a/cordova-plugin-moodleapp/plugin.xml
+++ b/cordova-plugin-moodleapp/plugin.xml
@@ -13,4 +13,14 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/cordova-plugin-moodleapp/src/ios/SecureStorage.h b/cordova-plugin-moodleapp/src/ios/SecureStorage.h
new file mode 100644
index 000000000..71f1a8c6b
--- /dev/null
+++ b/cordova-plugin-moodleapp/src/ios/SecureStorage.h
@@ -0,0 +1,11 @@
+#import
+#import
+
+@interface SecureStorage : CDVPlugin {}
+
+- (void)get:(CDVInvokedUrlCommand*)command;
+- (void)store:(CDVInvokedUrlCommand*)command;
+- (void)delete:(CDVInvokedUrlCommand*)command;
+- (void)deleteCollection:(CDVInvokedUrlCommand*)command;
+
+@end
diff --git a/cordova-plugin-moodleapp/src/ios/SecureStorage.m b/cordova-plugin-moodleapp/src/ios/SecureStorage.m
new file mode 100644
index 000000000..c71e752ff
--- /dev/null
+++ b/cordova-plugin-moodleapp/src/ios/SecureStorage.m
@@ -0,0 +1,218 @@
+#import
+#import
+#import
+#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
diff --git a/cordova-plugin-moodleapp/src/ts/index.ts b/cordova-plugin-moodleapp/src/ts/index.ts
index 27a15ad10..c95a35f4d 100644
--- a/cordova-plugin-moodleapp/src/ts/index.ts
+++ b/cordova-plugin-moodleapp/src/ts/index.ts
@@ -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,
diff --git a/cordova-plugin-moodleapp/src/ts/plugins/SecureStorage.ts b/cordova-plugin-moodleapp/src/ts/plugins/SecureStorage.ts
new file mode 100644
index 000000000..26e15a65a
--- /dev/null
+++ b/cordova-plugin-moodleapp/src/ts/plugins/SecureStorage.ts
@@ -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> {
+ 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, collection: string): Promise {
+ 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 {
+ 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 {
+ await new Promise((resolve, reject) => {
+ cordova.exec(resolve, reject, 'SecureStorage', 'deleteCollection', [collection]);
+ });
+ }
+
+}
diff --git a/cordova-plugin-moodleapp/types/index.d.ts b/cordova-plugin-moodleapp/types/index.d.ts
index 77722c985..780b56caa 100644
--- a/cordova-plugin-moodleapp/types/index.d.ts
+++ b/cordova-plugin-moodleapp/types/index.d.ts
@@ -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 {