diff --git a/native/ios/Comm.xcodeproj/project.pbxproj b/native/ios/Comm.xcodeproj/project.pbxproj --- a/native/ios/Comm.xcodeproj/project.pbxproj +++ b/native/ios/Comm.xcodeproj/project.pbxproj @@ -19,6 +19,12 @@ 443650452E21F47600026241 /* CommInitializerModule.mm in Sources */ = {isa = PBXBuildFile; fileRef = 443650442E21F47600026241 /* CommInitializerModule.mm */; }; 44F116742DE474D30027DDA6 /* DBInit.mm in Sources */ = {isa = PBXBuildFile; fileRef = 44F116732DE474D30027DDA6 /* DBInit.mm */; }; 44F116752DE474D30027DDA6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44F116712DE474D30027DDA6 /* AppDelegate.swift */; }; + 44F116832DE479270027DDA6 /* SecureFileExceptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44F1167E2DE479270027DDA6 /* SecureFileExceptions.swift */; }; + 44F116842DE479270027DDA6 /* SecureStoreModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44F116802DE479270027DDA6 /* SecureStoreModule.swift */; }; + 44F116852DE479270027DDA6 /* SecureStoreOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44F116812DE479270027DDA6 /* SecureStoreOptions.swift */; }; + 44F116862DE4798F0027DDA6 /* SecureFileExceptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44F1167E2DE479270027DDA6 /* SecureFileExceptions.swift */; }; + 44F116872DE479980027DDA6 /* SecureStoreModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44F116802DE479270027DDA6 /* SecureStoreModule.swift */; }; + 44F116882DE4799D0027DDA6 /* SecureStoreOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44F116812DE479270027DDA6 /* SecureStoreOptions.swift */; }; 71142A7726C2650B0039DCBD /* CommSecureStoreIOSWrapper.mm in Sources */ = {isa = PBXBuildFile; fileRef = 71142A7626C2650A0039DCBD /* CommSecureStoreIOSWrapper.mm */; }; 711B408425DA97F9005F8F06 /* dummy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F26E81B24440D87004049C6 /* dummy.swift */; }; 71762A75270D8AAE00F565ED /* PlatformSpecificTools.mm in Sources */ = {isa = PBXBuildFile; fileRef = 71762A74270D8AAE00F565ED /* PlatformSpecificTools.mm */; }; @@ -189,6 +195,10 @@ 44F116712DE474D30027DDA6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = Comm/AppDelegate.swift; sourceTree = ""; }; 44F116722DE474D30027DDA6 /* DBInit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = DBInit.h; path = Comm/DBInit.h; sourceTree = ""; }; 44F116732DE474D30027DDA6 /* DBInit.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = DBInit.mm; path = Comm/DBInit.mm; sourceTree = ""; }; + 44F1167E2DE479270027DDA6 /* SecureFileExceptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureFileExceptions.swift; sourceTree = ""; }; + 44F1167F2DE479270027DDA6 /* SecureStoreModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SecureStoreModule.h; sourceTree = ""; }; + 44F116802DE479270027DDA6 /* SecureStoreModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureStoreModule.swift; sourceTree = ""; }; + 44F116812DE479270027DDA6 /* SecureStoreOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureStoreOptions.swift; sourceTree = ""; }; 71142A7526C2650A0039DCBD /* CommSecureStoreIOSWrapper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CommSecureStoreIOSWrapper.h; path = Comm/CommSecureStoreIOSWrapper.h; sourceTree = ""; }; 71142A7626C2650A0039DCBD /* CommSecureStoreIOSWrapper.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = CommSecureStoreIOSWrapper.mm; path = Comm/CommSecureStoreIOSWrapper.mm; sourceTree = ""; }; 711CF80E25DC096000A00FBD /* libFolly.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libFolly.a; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -444,6 +454,18 @@ name = Comm; sourceTree = ""; }; + 44F116822DE479270027DDA6 /* CommSecureStore */ = { + isa = PBXGroup; + children = ( + 44F1167E2DE479270027DDA6 /* SecureFileExceptions.swift */, + 44F1167F2DE479270027DDA6 /* SecureStoreModule.h */, + 44F116802DE479270027DDA6 /* SecureStoreModule.swift */, + 44F116812DE479270027DDA6 /* SecureStoreOptions.swift */, + ); + name = CommSecureStore; + path = Comm/CommSecureStore; + sourceTree = ""; + }; 5F5A6FB2C6AD630620BBF58C /* NotificationService */ = { isa = PBXGroup; children = ( @@ -491,6 +513,7 @@ CB38B4782877177B00171182 /* TemporaryMessageStorage */, 71762A74270D8AAE00F565ED /* PlatformSpecificTools.mm */, 71D4D7CB26C50B1000FCDBCD /* CommSecureStore.mm */, + 44F116822DE479270027DDA6 /* CommSecureStore */, 71142A7526C2650A0039DCBD /* CommSecureStoreIOSWrapper.h */, 443650432E21F47600026241 /* CommInitializerModule.h */, 443650442E21F47600026241 /* CommInitializerModule.mm */, @@ -1425,6 +1448,9 @@ 8E18055A2DA95E7C00B772A4 /* SQLiteSchemaMigrations.cpp in Sources */, 443650452E21F47600026241 /* CommInitializerModule.mm in Sources */, DFD5E77E2B05264000C32B6A /* AESCrypto.mm in Sources */, + 44F116832DE479270027DDA6 /* SecureFileExceptions.swift in Sources */, + 44F116842DE479270027DDA6 /* SecureStoreModule.swift in Sources */, + 44F116852DE479270027DDA6 /* SecureStoreOptions.swift in Sources */, 8EA59BD62A6E8E0400EB4F53 /* DraftStore.cpp in Sources */, 71BE844B2636A944002849D2 /* SQLiteQueryExecutor.cpp in Sources */, ); @@ -1447,15 +1473,18 @@ CBCA09072A8E0E7D00F75B3E /* StaffUtils.cpp in Sources */, CB3C0A3B2A125C8F009BD4DA /* NotificationsCryptoModule.cpp in Sources */, CB90951F29534B32002F2A7F /* CommSecureStore.mm in Sources */, + 44F116872DE479980027DDA6 /* SecureStoreModule.swift in Sources */, CB38B48728771CE500171182 /* TemporaryMessageStorage.mm in Sources */, CB38B48528771CB800171182 /* EncryptedFileUtils.mm in Sources */, CB38B48328771C8300171182 /* NonBlockingLock.mm in Sources */, CB1648AF27CFBE6A00394D9D /* CryptoModule.cpp in Sources */, 443650422E21F3F200026241 /* StringUtils.cpp in Sources */, + 44F116882DE4799D0027DDA6 /* SecureStoreOptions.swift in Sources */, CB4821AE27CFB187001AB7E1 /* Tools.cpp in Sources */, CB4821AC27CFB17C001AB7E1 /* Session.cpp in Sources */, 7FDFC1002DC105E500B1D87F /* Base64.cpp in Sources */, CBB0DF612B768007008E22FF /* CommMMKV.mm in Sources */, + 44F116862DE4798F0027DDA6 /* SecureFileExceptions.swift in Sources */, CB4821A927CFB153001AB7E1 /* WorkerThread.cpp in Sources */, 7FE179032E43980200973CAF /* ServicesUtils.cpp in Sources */, CB4821AA27CFB153001AB7E1 /* Tools.mm in Sources */, diff --git a/native/ios/Comm/CommSecureStore.mm b/native/ios/Comm/CommSecureStore.mm --- a/native/ios/Comm/CommSecureStore.mm +++ b/native/ios/Comm/CommSecureStore.mm @@ -1,5 +1,4 @@ #import "CommSecureStore.h" -#import #import #import diff --git a/native/ios/Comm/CommSecureStore/SecureFileExceptions.swift b/native/ios/Comm/CommSecureStore/SecureFileExceptions.swift new file mode 100644 --- /dev/null +++ b/native/ios/Comm/CommSecureStore/SecureFileExceptions.swift @@ -0,0 +1,102 @@ +/** + File copied from expo-secure-store 14.x with some changes + https://github.com/expo/expo/blob/49c9d53cf0a9fc8179d1c8f5268beadd141f70ca/packages/expo-secure-store/ios/SecureStoreExceptions.swift + + Why we copy: https://linear.app/comm/issue/ENG-10284/migrate-expo-secure-store-related-code +*/ +import Foundation + +open class Exception: Error { + open lazy var name: String = String(describing: Self.self) + + /** + String describing the reason of the exception. + */ + open var reason: String { + "undefined reason" + } +} + +open class GenericException: Exception { + /** + The additional parameter passed to the initializer. + */ + public let param: ParamType + + /** + The default initializer that takes a param and captures the place in the code where the exception was created. + - Warning: Call it only with one argument! If you need to pass more parameters, use a tuple instead. + */ + public init(_ param: ParamType, file: String = #fileID, line: UInt = #line, function: String = #function) { + self.param = param + } +} + +internal final class InvalidKeyException: Exception { + override var reason: String { + "Invalid key" + } +} + +internal final class MissingPlistKeyException: Exception { + override var reason: String { + "You must set `NSFaceIDUsageDescription` in your Info.plist file to use the `requireAuthentication` option" + } +} + +internal final class SecAccessControlError: GenericException { + override var reason: String { + return "Unable to construct SecAccessControl: \(param.map { "code " + String($0) } ?? "unknown error")" + } +} + +internal final class KeyChainException: GenericException { + override var reason: String { + switch param { + case errSecUnimplemented: + return "Function or operation not implemented." + + case errSecIO: + return "I/O error." + + case errSecOpWr: + return "File already open with with write permission." + + case errSecParam: + return "One or more parameters passed to a function where not valid." + + case errSecAllocate: + return "Failed to allocate memory." + + case errSecUserCanceled: + return "User canceled the operation." + + case errSecBadReq: + return "Bad parameter or invalid state for operation." + + case errSecNotAvailable: + return "No keychain is available. You may need to restart your computer." + + case errSecDuplicateItem: + return "The specified item already exists in the keychain." + + case errSecItemNotFound: + return "The specified item could not be found in the keychain." + + case errSecInteractionNotAllowed: + return "User interaction is not allowed." + + case errSecDecode: + return "Unable to decode the provided data." + + case errSecAuthFailed: + return "Authentication failed. Provided passphrase/PIN is incorrect or there is no user authentication method configured for this device." + + default: + if let errorMessage = SecCopyErrorMessageString(param, nil) as? String { + return errorMessage + } + return "Unknown Keychain Error." + } + } +} diff --git a/native/ios/Comm/CommSecureStore/SecureStoreModule.h b/native/ios/Comm/CommSecureStore/SecureStoreModule.h new file mode 100644 --- /dev/null +++ b/native/ios/Comm/CommSecureStore/SecureStoreModule.h @@ -0,0 +1,29 @@ +#import + +typedef NS_ENUM(NSInteger, SecureStoreAccessible) { + SecureStoreAccessibleAfterFirstUnlock = 0, + SecureStoreAccessibleAfterFirstUnlockThisDeviceOnly = 1, + SecureStoreAccessibleAlways = 2, + SecureStoreAccessibleWhenPasscodeSetThisDeviceOnly = 3, + SecureStoreAccessibleAlwaysThisDeviceOnly = 4, + SecureStoreAccessibleWhenUnlocked = 5, + SecureStoreAccessibleWhenUnlockedThisDeviceOnly = 6, +}; + +@interface SecureStoreOptions : NSObject +@property(nonatomic, copy) NSString *_Nullable authenticationPrompt; +@property(nonatomic) SecureStoreAccessible keychainAccessible; +@property(nonatomic, copy) NSString *_Nullable keychainService; +@property(nonatomic) BOOL requireAuthentication; +@property(nonatomic, copy) NSString *_Nullable accessGroup; +- (nonnull instancetype)init; +@end + +@interface SecureStoreModule : NSObject +- (NSString *_Nullable)getValueWithKey:(NSString *_Nonnull)key + options:(SecureStoreOptions *_Nonnull)options; +- (BOOL)setValueWithKeyWithValue:(NSString *_Nonnull)value + key:(NSString *_Nonnull)key + options:(SecureStoreOptions *_Nonnull)options; +- (nonnull instancetype)init; +@end diff --git a/native/ios/Comm/CommSecureStore/SecureStoreModule.swift b/native/ios/Comm/CommSecureStore/SecureStoreModule.swift new file mode 100644 --- /dev/null +++ b/native/ios/Comm/CommSecureStore/SecureStoreModule.swift @@ -0,0 +1,184 @@ +/** + File copied from expo-secure-store 14.x with some changes + https://github.com/expo/expo/blob/49c9d53cf0a9fc8179d1c8f5268beadd141f70ca/packages/expo-secure-store/ios/SecureStoreModule.swift + + Why we copy: https://linear.app/comm/issue/ENG-10284/migrate-expo-secure-store-related-code +*/ +import LocalAuthentication +import Security + +@objc(SecureStoreModule) +public final class SecureStoreModule: NSObject { + @objc + func getValue(key: String, options: SecureStoreOptions) -> String? { + do { + return try get(with: key, options: options) + } catch let swiftError { + return nil + } + } + + @objc + func setValueWithKey(value: String, key: String, options: SecureStoreOptions) -> Bool { + guard let key = validate(for: key) else { + return false + } + do { + return try set(value: value, with: key, options: options) + } catch let swiftError { + return false + } + } + + private func get(with key: String, options: SecureStoreOptions) throws -> String? { + guard let key = validate(for: key) else { + throw InvalidKeyException() + } + + if let unauthenticatedItem = try searchKeyChain(with: key, options: options, requireAuthentication: false) { + return String(data: unauthenticatedItem, encoding: .utf8) + } + + if let authenticatedItem = try searchKeyChain(with: key, options: options, requireAuthentication: true) { + return String(data: authenticatedItem, encoding: .utf8) + } + + if let legacyItem = try searchKeyChain(with: key, options: options) { + return String(data: legacyItem, encoding: .utf8) + } + + return nil + } + + private func set(value: String, with key: String, options: SecureStoreOptions) throws -> Bool { + var setItemQuery = query(with: key, options: options, requireAuthentication: options.requireAuthentication) + + let valueData = value.data(using: .utf8) + setItemQuery[kSecValueData as String] = valueData + + let accessibility = attributeWith(options: options) + + if !options.requireAuthentication { + setItemQuery[kSecAttrAccessible as String] = accessibility + } else { + guard let _ = Bundle.main.infoDictionary?["NSFaceIDUsageDescription"] as? String else { + throw MissingPlistKeyException() + } + + var error: Unmanaged? = nil + guard let accessOptions = SecAccessControlCreateWithFlags(kCFAllocatorDefault, accessibility, .biometryCurrentSet, &error) else { + let errorCode = error.map { CFErrorGetCode($0.takeRetainedValue()) } + throw SecAccessControlError(errorCode) + } + setItemQuery[kSecAttrAccessControl as String] = accessOptions + } + + let status = SecItemAdd(setItemQuery as CFDictionary, nil) + + switch status { + case errSecSuccess: + // On success we want to remove the other key alias and legacy key (if they exist) to avoid conflicts during reads + SecItemDelete(query(with: key, options: options) as CFDictionary) + SecItemDelete(query(with: key, options: options, requireAuthentication: !options.requireAuthentication) as CFDictionary) + return true + case errSecDuplicateItem: + return try update(value: value, with: key, options: options) + default: + throw KeyChainException(status) + } + } + + private func update(value: String, with key: String, options: SecureStoreOptions) throws -> Bool { + var query = query(with: key, options: options, requireAuthentication: options.requireAuthentication) + + let valueData = value.data(using: .utf8) + let updateDictionary = [kSecValueData as String: valueData] + + if let authPrompt = options.authenticationPrompt { + query[kSecUseOperationPrompt as String] = authPrompt + } + + let status = SecItemUpdate(query as CFDictionary, updateDictionary as CFDictionary) + + if status == errSecSuccess { + return true + } else { + throw KeyChainException(status) + } + } + + private func searchKeyChain(with key: String, options: SecureStoreOptions, requireAuthentication: Bool? = nil) throws -> Data? { + var query = query(with: key, options: options, requireAuthentication: requireAuthentication) + + query[kSecMatchLimit as String] = kSecMatchLimitOne + query[kSecReturnData as String] = kCFBooleanTrue + + if let authPrompt = options.authenticationPrompt { + query[kSecUseOperationPrompt as String] = authPrompt + } + + var item: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &item) + + switch status { + case errSecSuccess: + guard let item = item as? Data else { + return nil + } + return item + case errSecItemNotFound: + return nil + default: + throw KeyChainException(status) + } + } + + private func query(with key: String, options: SecureStoreOptions, requireAuthentication: Bool? = nil) -> [String: Any] { + var service = options.keychainService ?? "app" + if let requireAuthentication { + service.append(":\(requireAuthentication ? "auth" : "no-auth")") + } + + let encodedKey = Data(key.utf8) + + var query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrGeneric as String: encodedKey, + kSecAttrAccount as String: encodedKey + ] + + if let accessGroup = options.accessGroup { + query[kSecAttrAccessGroup as String] = accessGroup + } + + return query + } + + private func attributeWith(options: SecureStoreOptions) -> CFString { + switch options.keychainAccessible { + case .afterFirstUnlock: + return kSecAttrAccessibleAfterFirstUnlock + case .afterFirstUnlockThisDeviceOnly: + return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly + case .always: + return kSecAttrAccessibleAlways + case .whenPasscodeSetThisDeviceOnly: + return kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly + case .whenUnlocked: + return kSecAttrAccessibleWhenUnlocked + case .alwaysThisDeviceOnly: + return kSecAttrAccessibleAlwaysThisDeviceOnly + case .whenUnlockedThisDeviceOnly: + return kSecAttrAccessibleWhenUnlockedThisDeviceOnly + } + } + + private func validate(for key: String) -> String? { + let trimmedKey = key.trimmingCharacters(in: .whitespaces) + if trimmedKey.isEmpty { + return nil + } + return key + } +} diff --git a/native/ios/Comm/CommSecureStore/SecureStoreOptions.swift b/native/ios/Comm/CommSecureStore/SecureStoreOptions.swift new file mode 100644 --- /dev/null +++ b/native/ios/Comm/CommSecureStore/SecureStoreOptions.swift @@ -0,0 +1,33 @@ +/** + File copied from expo-secure-store 14.x + https://github.com/expo/expo/blob/49c9d53cf0a9fc8179d1c8f5268beadd141f70ca/packages/expo-secure-store/ios/SecureStoreOptions.swift + https://github.com/expo/expo/blob/49c9d53cf0a9fc8179d1c8f5268beadd141f70ca/packages/expo-secure-store/ios/SecureStoreAccessible.swift + + Why we copy: https://linear.app/comm/issue/ENG-10284/migrate-expo-secure-store-related-code +*/ +import Foundation + +@objc +public enum SecureStoreAccessible: Int { + case afterFirstUnlock = 0 + case afterFirstUnlockThisDeviceOnly = 1 + case always = 2 + case whenPasscodeSetThisDeviceOnly = 3 + case alwaysThisDeviceOnly = 4 + case whenUnlocked = 5 + case whenUnlockedThisDeviceOnly = 6 +} + +@objc(SecureStoreOptions) +public class SecureStoreOptions: NSObject { + @objc public var authenticationPrompt: String? + @objc public var keychainAccessible: SecureStoreAccessible = .whenUnlocked + @objc public var keychainService: String? + @objc public var requireAuthentication: Bool = false + @objc public var accessGroup: String? + + @objc + public override init() { + super.init() + } +} diff --git a/native/ios/Comm/CommSecureStoreIOSWrapper.h b/native/ios/Comm/CommSecureStoreIOSWrapper.h --- a/native/ios/Comm/CommSecureStoreIOSWrapper.h +++ b/native/ios/Comm/CommSecureStoreIOSWrapper.h @@ -1,16 +1,10 @@ #pragma once -#import - #import @interface CommSecureStoreIOSWrapper : NSObject + (id)sharedInstance; - (void)set:(NSString *)key value:(NSString *)value; -- (void)set:(NSString *)key - value:(NSString *)value - withOptions:(NSDictionary *)options; - (NSString *)get:(NSString *)key; -- (NSString *)get:(NSString *)key withOptions:(NSDictionary *)options; @end diff --git a/native/ios/Comm/CommSecureStoreIOSWrapper.mm b/native/ios/Comm/CommSecureStoreIOSWrapper.mm --- a/native/ios/Comm/CommSecureStoreIOSWrapper.mm +++ b/native/ios/Comm/CommSecureStoreIOSWrapper.mm @@ -1,23 +1,12 @@ #import "CommSecureStoreIOSWrapper.h" -#import "CommSecureStoreIOSWrapper.h" -#import +#import "CommSecureStore/SecureStoreModule.h" @interface CommSecureStoreIOSWrapper () -@property(nonatomic, strong) EXSecureStore *secureStore; +@property(nonatomic, strong) SecureStoreModule *secureStore; @property(nonatomic, strong) NSDictionary *options; @end -@interface EXSecureStore (CommEXSecureStore) -- (BOOL)_setValue:(NSString *)value - withKey:(NSString *)key - withOptions:(NSDictionary *)options - error:(NSError **)error; -- (NSString *)_getValueWithKey:(NSString *)key - withOptions:(NSDictionary *)options - error:(NSError **)error; -@end - @implementation CommSecureStoreIOSWrapper #pragma mark Singleton Methods @@ -27,28 +16,20 @@ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ shared = [[self alloc] init]; - EXModuleRegistryProvider *moduleRegistryProvider = - [[EXModuleRegistryProvider alloc] init]; - EXSecureStore *secureStore = - (EXSecureStore *)[[moduleRegistryProvider moduleRegistry] - getExportedModuleOfClass:EXSecureStore.class]; - shared.secureStore = secureStore; + shared.secureStore = [SecureStoreModule new]; }); return shared; } - (void)set:(NSString *)key value:(NSString *)value - withOptions:(NSDictionary *)options { + withOptions:(SecureStoreOptions *)options { if ([self secureStore] == nil) { [NSException raise:@"secure store error" format:@"secure store has not been initialized"]; } NSError *error; - [[self secureStore] _setValue:value - withKey:key - withOptions:options - error:&error]; + [[self secureStore] setValueWithKeyWithValue:value key:key options:options]; if (error != nil) { [NSException raise:@"secure store error" format:@"error occured when setting data"]; @@ -56,29 +37,24 @@ } - (void)set:(NSString *)key value:(NSString *)value { - [self set:key - value:value - withOptions:@{ - @"keychainAccessible" : @(EXSecureStoreAccessibleAfterFirstUnlock) - }]; + SecureStoreOptions *storeOptions = [SecureStoreOptions new]; + storeOptions.keychainAccessible = SecureStoreAccessibleAfterFirstUnlock; + [self set:key value:value withOptions:storeOptions]; } -- (NSString *)get:(NSString *)key withOptions:(NSDictionary *)options { +- (NSString *)get:(NSString *)key withOptions:(SecureStoreOptions *)options { if ([self secureStore] == nil) { [NSException raise:@"secure store error" format:@"secure store has not been initialized"]; } NSError *error; - return [[self secureStore] _getValueWithKey:key - withOptions:options - error:&error]; + return [[self secureStore] getValueWithKey:key options:options]; } - (NSString *)get:(NSString *)key { - return [self get:key - withOptions:@{ - @"keychainAccessible" : @(EXSecureStoreAccessibleAfterFirstUnlock) - }]; + SecureStoreOptions *storeOptions = [SecureStoreOptions new]; + storeOptions.keychainAccessible = SecureStoreAccessibleAfterFirstUnlock; + return [self get:key withOptions:storeOptions]; } @end