1 import ABI49_0_0ExpoModulesCore 2 import LocalAuthentication 3 import Security 4 5 public final class SecureStoreModule: Module { 6 public func definition() -> ModuleDefinition { 7 Name("ExpoSecureStore") 8 9 Constants([ 10 "AFTER_FIRST_UNLOCK": SecureStoreAccessible.afterFirstUnlock.rawValue, 11 "AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY": SecureStoreAccessible.afterFirstUnlockThisDeviceOnly.rawValue, 12 "ALWAYS": SecureStoreAccessible.always.rawValue, 13 "WHEN_PASSCODE_SET_THIS_DEVICE_ONLY": SecureStoreAccessible.whenPasscodeSetThisDeviceOnly.rawValue, 14 "ALWAYS_THIS_DEVICE_ONLY": SecureStoreAccessible.alwaysThisDeviceOnly.rawValue, 15 "WHEN_UNLOCKED": SecureStoreAccessible.whenUnlocked.rawValue, 16 "WHEN_UNLOCKED_THIS_DEVICE_ONLY": SecureStoreAccessible.whenPasscodeSetThisDeviceOnly.rawValue 17 ]) 18 19 AsyncFunction("getValueWithKeyAsync") { (key: String, options: SecureStoreOptions) -> String? in 20 guard let key = validate(for: key) else { 21 throw InvalidKeyException() 22 } 23 24 let data = try searchKeyChain(with: key, options: options) 25 26 guard let data = data else { 27 return nil 28 } 29 30 return String(data: data, encoding: .utf8) 31 } 32 33 AsyncFunction("setValueWithKeyAsync") { (value: String, key: String, options: SecureStoreOptions) -> Bool in 34 guard let key = validate(for: key) else { 35 throw InvalidKeyException() 36 } 37 38 return try set(value: value, with: key, options: options) 39 } 40 41 AsyncFunction("deleteValueWithKeyAsync") { (key: String, options: SecureStoreOptions) in 42 var searchDictionary = query(with: key, options: options) 43 SecItemDelete(searchDictionary as CFDictionary) 44 } 45 } 46 47 private func set(value: String, with key: String, options: SecureStoreOptions) throws -> Bool { 48 var query = query(with: key, options: options) 49 50 let valueData = value.data(using: .utf8) 51 query[kSecValueData as String] = valueData 52 53 let accessibility = attributeWith(options: options) 54 55 if !options.requireAuthentication { 56 query[kSecAttrAccessible as String] = accessibility 57 } else { 58 let accessOptions = SecAccessControlCreateWithFlags(kCFAllocatorDefault, accessibility, SecAccessControlCreateFlags.biometryCurrentSet, nil) 59 query[kSecAttrAccessControl as String] = accessOptions 60 } 61 62 let status = SecItemAdd(query as CFDictionary, nil) 63 64 switch status { 65 case errSecSuccess: 66 return true 67 case errSecDuplicateItem: 68 return try update(value: value, with: key, options: options) 69 default: 70 throw KeyChainException(status) 71 } 72 } 73 74 private func update(value: String, with key: String, options: SecureStoreOptions) throws -> Bool { 75 var query = query(with: key, options: options) 76 77 let valueData = value.data(using: .utf8) 78 let updateDictionary = [kSecValueData as String: valueData] 79 80 if let authPrompt = options.authenticationPrompt { 81 query[kSecUseOperationPrompt as String] = authPrompt 82 } 83 84 let status = SecItemUpdate(query as CFDictionary, updateDictionary as CFDictionary) 85 86 if status == errSecSuccess { 87 return true 88 } else { 89 throw KeyChainException(status) 90 } 91 } 92 93 private func searchKeyChain(with key: String, options: SecureStoreOptions) throws -> Data? { 94 var query = query(with: key, options: options) 95 96 query[kSecMatchLimit as String] = kSecMatchLimitOne 97 query[kSecReturnData as String] = kCFBooleanTrue 98 99 if let authPrompt = options.authenticationPrompt { 100 query[kSecUseOperationPrompt as String] = authPrompt 101 } 102 103 var item: CFTypeRef? 104 let status = SecItemCopyMatching(query as CFDictionary, &item) 105 106 switch status { 107 case errSecSuccess: 108 guard let item = item as? Data else { 109 return nil 110 } 111 112 return item 113 case errSecItemNotFound: 114 return nil 115 default: 116 throw KeyChainException(status) 117 } 118 } 119 120 private func query(with key: String, options: SecureStoreOptions) -> [String: Any] { 121 let service = options.keychainService ?? "app" 122 let encodedKey = key.data(using: .utf8) 123 124 return [ 125 kSecClass as String: kSecClassGenericPassword, 126 kSecAttrService as String: service, 127 kSecAttrGeneric as String: encodedKey, 128 kSecAttrAccount as String: encodedKey 129 ] 130 } 131 132 private func attributeWith(options: SecureStoreOptions) -> CFString { 133 switch options.keychainAccessible { 134 case .afterFirstUnlock: 135 return kSecAttrAccessibleAfterFirstUnlock 136 case .afterFirstUnlockThisDeviceOnly: 137 return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly 138 case .always: 139 return kSecAttrAccessibleAlways 140 case .whenPasscodeSetThisDeviceOnly: 141 return kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly 142 case .whenUnlocked: 143 return kSecAttrAccessibleWhenUnlocked 144 case .alwaysThisDeviceOnly: 145 return kSecAttrAccessibleAlwaysThisDeviceOnly 146 case .whenUnlockedThisDeviceOnly: 147 return kSecAttrAccessibleWhenUnlockedThisDeviceOnly 148 default: 149 return kSecAttrAccessibleWhenUnlocked 150 } 151 } 152 153 private func validate(for key: String) -> String? { 154 let trimmedKey = key.trimmingCharacters(in: .whitespaces) 155 if trimmedKey.isEmpty { 156 return nil 157 } 158 return key 159 } 160 } 161