1 import ABI49_0_0ExpoModulesCore
2 import LocalAuthentication
3 import Security
4 
5 public final class SecureStoreModule: Module {
definitionnull6   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 
setnull47   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 
updatenull77   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 
searchKeyChainnull96   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       return item
115     case errSecItemNotFound:
116       return nil
117     default:
118       throw KeyChainException(status)
119     }
120   }
121 
querynull122   private func query(with key: String, options: SecureStoreOptions) -> [String: Any] {
123     let service = options.keychainService ?? "app"
124     let encodedKey = Data(key.utf8)
125 
126     return [
127       kSecClass as String: kSecClassGenericPassword,
128       kSecAttrService as String: service,
129       kSecAttrGeneric as String: encodedKey,
130       kSecAttrAccount as String: encodedKey
131     ]
132   }
133 
attributeWithnull134   private func attributeWith(options: SecureStoreOptions) -> CFString {
135     switch options.keychainAccessible {
136     case .afterFirstUnlock:
137       return kSecAttrAccessibleAfterFirstUnlock
138     case .afterFirstUnlockThisDeviceOnly:
139       return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
140     case .always:
141       return kSecAttrAccessibleAlways
142     case .whenPasscodeSetThisDeviceOnly:
143       return kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
144     case .whenUnlocked:
145       return kSecAttrAccessibleWhenUnlocked
146     case .alwaysThisDeviceOnly:
147       return kSecAttrAccessibleAlwaysThisDeviceOnly
148     case .whenUnlockedThisDeviceOnly:
149       return kSecAttrAccessibleWhenUnlockedThisDeviceOnly
150     default:
151       return kSecAttrAccessibleWhenUnlocked
152     }
153   }
154 
validatenull155   private func validate(for key: String) -> String? {
156     let trimmedKey = key.trimmingCharacters(in: .whitespaces)
157     if trimmedKey.isEmpty {
158       return nil
159     }
160     return key
161   }
162 }
163