-
-
Notifications
You must be signed in to change notification settings - Fork 117
Expand file tree
/
Copy pathBiometricStorageImpl.swift
More file actions
256 lines (231 loc) · 8.5 KB
/
Copy pathBiometricStorageImpl.swift
File metadata and controls
256 lines (231 loc) · 8.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
// Shared file between iOS and Mac OS
// make sure they stay in sync.
import Foundation
import LocalAuthentication
typealias StorageCallback = (Any?) -> Void
typealias StorageError = (String, String?, Any?) -> Any
struct StorageMethodCall {
let method: String
let arguments: Any?
}
class InitOptions {
init(params: [String: Any]) {
authenticationValidityDurationSeconds = params["authenticationValidityDurationSeconds"] as? Int
authenticationRequired = params["authenticationRequired"] as? Bool
}
let authenticationValidityDurationSeconds: Int!
let authenticationRequired: Bool!
}
class IOSPromptInfo {
init(params: [String: Any]) {
saveTitle = params["saveTitle"] as? String
accessTitle = params["accessTitle"] as? String
}
let saveTitle: String!
let accessTitle: String!
}
private func hpdebug(_ message: String) {
print(message);
}
class BiometricStorageImpl {
init(storageError: @escaping StorageError, storageMethodNotImplemented: Any) {
self.storageError = storageError
self.storageMethodNotImplemented = storageMethodNotImplemented
}
private var stores: [String: InitOptions] = [:]
private let storageError: StorageError
private let storageMethodNotImplemented: Any
private func storageError(code: String, message: String?, details: Any?) -> Any {
return storageError(code, message, details)
}
private func baseQuery(name: String) -> [String: Any] {
return [kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: Bundle.main.bundleIdentifier as Any,
kSecAttrAccount as String: name]
}
public func handle(_ call: StorageMethodCall, result: @escaping StorageCallback) {
func requiredArg<T>(_ name: String, _ cb: (T) -> Void) {
guard let args = call.arguments as? Dictionary<String, Any> else {
result(storageError(code: "InvalidArguments", message: "Invalid arguments \(String(describing: call.arguments))", details: nil))
return
}
guard let value = args[name] else {
result(storageError(code: "InvalidArguments", message: "Missing argument \(name)", details: nil))
return
}
guard let valueTyped = value as? T else {
result(storageError(code: "InvalidArguments", message: "Invalid argument for \(name): expected \(T.self) got \(value)", details: nil))
return
}
cb(valueTyped)
return
}
if ("canAuthenticate" == call.method) {
canAuthenticate(result: result)
} else if ("init" == call.method) {
requiredArg("name") { name in
requiredArg("options") { options in
stores[name] = InitOptions(params: options)
}
}
result(true)
} else if ("dispose" == call.method) {
// nothing to dispose
result(true)
} else if ("read" == call.method) {
requiredArg("name") { name in
requiredArg("iosPromptInfo") { promptInfo in
read(name, result, IOSPromptInfo(params: promptInfo))
}
}
} else if ("write" == call.method) {
requiredArg("name") { name in
requiredArg("content") { content in
requiredArg("iosPromptInfo") { promptInfo in
write(name, content, result, IOSPromptInfo(params: promptInfo))
}
}
}
} else if ("delete" == call.method) {
requiredArg("name") { name in
requiredArg("iosPromptInfo") { promptInfo in
delete(name, result, IOSPromptInfo(params: promptInfo))
}
}
} else {
result(storageMethodNotImplemented)
}
}
private func read(_ name: String, _ result: @escaping StorageCallback, _ promptInfo: IOSPromptInfo) {
var query = baseQuery(name: name)
query[kSecMatchLimit as String] = kSecMatchLimitOne
query[kSecUseOperationPrompt as String] = promptInfo.accessTitle
query[kSecReturnAttributes as String] = true
query[kSecReturnData as String] = true
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status != errSecItemNotFound else {
result(nil)
return
}
guard status == errSecSuccess else {
handleOSStatusError(status, result, "Error retrieving item. \(status)")
return
}
guard let existingItem = item as? [String : Any],
let data = existingItem[kSecValueData as String] as? Data,
let dataString = String(data: data, encoding: String.Encoding.utf8)
else {
result(storageError(code: "RetrieveError", message: "Unexpected data.", details: nil))
return
}
result(dataString)
}
private func delete(_ name: String, _ result: @escaping StorageCallback, _ promptInfo: IOSPromptInfo) {
let query = baseQuery(name: name)
// query[kSecMatchLimit as String] = kSecMatchLimitOne
// query[kSecReturnData as String] = true
let status = SecItemDelete(query as CFDictionary)
if status == errSecSuccess {
result(true)
return
}
if status == errSecItemNotFound {
hpdebug("Item not in keychain. Nothing to delete.")
result(true)
return
}
handleOSStatusError(status, result, "writing data")
}
private func write(_ name: String, _ content: String, _ result: @escaping StorageCallback, _ promptInfo: IOSPromptInfo) {
guard let initOptions = stores[name] else {
result(storageError(code: "WriteError", message: "Storage was not initialized. \(name)", details: nil))
return
}
var query = baseQuery(name: name)
if (initOptions.authenticationRequired) {
let context = LAContext()
if initOptions.authenticationValidityDurationSeconds > 0 {
if #available(OSX 10.12, *) {
context.touchIDAuthenticationAllowableReuseDuration = Double(initOptions.authenticationValidityDurationSeconds)
} else {
// Fallback on earlier versions
hpdebug("Pre OSX 10.12 no touchIDAuthenticationAllowableReuseDuration available. ignoring.")
}
}
let access = SecAccessControlCreateWithFlags(nil, // Use the default allocator.
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
.userPresence,
nil) // Ignore any error.
query.merge([
kSecUseAuthenticationContext as String: context,
kSecAttrAccessControl as String: access as Any,
]) { (_, new) in new }
if let operationPrompt = promptInfo.saveTitle {
query[kSecUseOperationPrompt as String] = operationPrompt
}
} else {
hpdebug("No authentication required for \(name)")
}
query.merge([
// kSecMatchLimit as String: kSecMatchLimitOne,
kSecValueData as String: content.data(using: String.Encoding.utf8) as Any,
]) { (_, new) in new }
var status = SecItemAdd(query as CFDictionary, nil)
if (status == errSecDuplicateItem) {
hpdebug("Value already exists. updating.")
let update = [kSecValueData as String: query[kSecValueData as String]]
query.removeValue(forKey: kSecValueData as String)
status = SecItemUpdate(query as CFDictionary, update as CFDictionary)
}
guard status == errSecSuccess else {
handleOSStatusError(status, result, "writing data")
return
}
result(nil)
}
private func handleOSStatusError(_ status: OSStatus, _ result: @escaping StorageCallback, _ message: String) {
var errorMessage: String? = nil
if #available(iOS 11.3, OSX 10.12, *) {
errorMessage = SecCopyErrorMessageString(status, nil) as String?
}
let code: String
switch status {
case errSecUserCanceled:
code = "AuthError:UserCanceled"
default:
code = "SecurityError"
}
result(storageError(code: code, message: "Error while \(message): \(status): \(errorMessage ?? "Unknown")", details: nil))
}
private func canAuthenticate(result: @escaping StorageCallback) {
let context = LAContext()
if #available(iOS 10.0, OSX 10.12, *) {
context.localizedCancelTitle = "Checking auth support"
}
var error: NSError?
if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
result("Success")
return
}
guard let err = error else {
result("ErrorUnknown")
return
}
let laError = LAError(_nsError: err)
NSLog("LAError: \(laError)");
switch laError.code {
case .touchIDNotAvailable:
result("ErrorHwUnavailable")
break;
case .passcodeNotSet: fallthrough
case .touchIDNotEnrolled:
result("ErrorNoBiometricEnrolled")
break;
case .invalidContext: fallthrough
default:
result("ErrorUnknown")
break;
}
}
}