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 let 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 guard let _ = Bundle.main.infoDictionary?["NSFaceIDUsageDescription"] as? String else { 59 throw MissingPlistKeyException() 60 } 61 let accessOptions = SecAccessControlCreateWithFlags(kCFAllocatorDefault, accessibility, SecAccessControlCreateFlags.biometryCurrentSet, nil) 62 query[kSecAttrAccessControl as String] = accessOptions 63 } 64 65 let status = SecItemAdd(query as CFDictionary, nil) 66 67 switch status { 68 case errSecSuccess: 69 return true 70 case errSecDuplicateItem: 71 return try update(value: value, with: key, options: options) 72 default: 73 throw KeyChainException(status) 74 } 75 } 76 77 private func update(value: String, with key: String, options: SecureStoreOptions) throws -> Bool { 78 var query = query(with: key, options: options) 79 80 let valueData = value.data(using: .utf8) 81 let updateDictionary = [kSecValueData as String: valueData] 82 83 if let authPrompt = options.authenticationPrompt { 84 query[kSecUseOperationPrompt as String] = authPrompt 85 } 86 87 let status = SecItemUpdate(query as CFDictionary, updateDictionary as CFDictionary) 88 89 if status == errSecSuccess { 90 return true 91 } else { 92 throw KeyChainException(status) 93 } 94 } 95 96 private func searchKeyChain(with key: String, options: SecureStoreOptions) throws -> Data? { 97 var query = query(with: key, options: options) 98 99 query[kSecMatchLimit as String] = kSecMatchLimitOne 100 query[kSecReturnData as String] = kCFBooleanTrue 101 102 if let authPrompt = options.authenticationPrompt { 103 query[kSecUseOperationPrompt as String] = authPrompt 104 } 105 106 var item: CFTypeRef? 107 let status = SecItemCopyMatching(query as CFDictionary, &item) 108 109 switch status { 110 case errSecSuccess: 111 guard let item = item as? Data else { 112 return nil 113 } 114 115 return item 116 case errSecItemNotFound: 117 return nil 118 default: 119 throw KeyChainException(status) 120 } 121 } 122 123 private func query(with key: String, options: SecureStoreOptions) -> [String: Any] { 124 let service = options.keychainService ?? "app" 125 let encodedKey = Data(key.utf8) 126 127 return [ 128 kSecClass as String: kSecClassGenericPassword, 129 kSecAttrService as String: service, 130 kSecAttrGeneric as String: encodedKey, 131 kSecAttrAccount as String: encodedKey 132 ] 133 } 134 135 private func attributeWith(options: SecureStoreOptions) -> CFString { 136 switch options.keychainAccessible { 137 case .afterFirstUnlock: 138 return kSecAttrAccessibleAfterFirstUnlock 139 case .afterFirstUnlockThisDeviceOnly: 140 return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly 141 case .always: 142 return kSecAttrAccessibleAlways 143 case .whenPasscodeSetThisDeviceOnly: 144 return kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly 145 case .whenUnlocked: 146 return kSecAttrAccessibleWhenUnlocked 147 case .alwaysThisDeviceOnly: 148 return kSecAttrAccessibleAlwaysThisDeviceOnly 149 case .whenUnlockedThisDeviceOnly: 150 return kSecAttrAccessibleWhenUnlockedThisDeviceOnly 151 default: 152 return kSecAttrAccessibleWhenUnlocked 153 } 154 } 155 156 private func validate(for key: String) -> String? { 157 let trimmedKey = key.trimmingCharacters(in: .whitespaces) 158 if trimmedKey.isEmpty { 159 return nil 160 } 161 return key 162 } 163 } 164