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