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