-
Notifications
You must be signed in to change notification settings - Fork 0
/
SwiftKeyChainStore.swift
225 lines (178 loc) · 6.78 KB
/
SwiftKeyChainStore.swift
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
//
// KeyChainStore.swift
// Swift Weekly Brief
//
// Created by Jeroen Leenarts on 22/03/2021.
//
import Foundation
import LocalAuthentication
enum KeyChainError: Error {
case conversionError
case securityError(status: OSStatus)
case unexpectedError
}
private enum KeychainKey: String {
case sendyApi
case secret
case productionListId
case testListId
}
extension SwiftKeyChainStore {
func sendyApi() throws -> String? {
try string(forKey: KeychainKey.sendyApi.rawValue)
}
func setSendyApi(_ newValue: String) throws {
try setString(newValue, forKey: KeychainKey.sendyApi.rawValue)
}
func productionListId() throws -> String? {
try string(forKey: KeychainKey.productionListId.rawValue)
}
func setProductionListId(_ newValue: String) throws {
try setString(newValue, forKey: KeychainKey.productionListId.rawValue)
}
func secret() throws -> String? {
try string(forKey: KeychainKey.secret.rawValue)
}
func setSecret(_ newValue: String) throws {
try setString(newValue, forKey: KeychainKey.secret.rawValue)
}
func testListId() throws -> String? {
try string(forKey: KeychainKey.testListId.rawValue)
}
func setTestListId(_ newValue: String) throws {
try setString(newValue, forKey: KeychainKey.testListId.rawValue)
}
}
class SwiftKeyChainStore {
private(set) var service: String?
private(set) var accessGroup: String?
var authenticationPrompt: String = "Please provide your ID to send newsletters."
var allItems: [AnyObject] {
var query = baseQuery()
query[kSecMatchLimit as String] = kSecMatchLimitAll
query[kSecReturnAttributes as String] = true
query[kSecReturnData as String] = true
var resultRef: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &resultRef)
guard status == errSecSuccess else {
return []
}
guard let items = resultRef as? [AnyObject] else {
return []
}
return items
}
init(service: String?, accessGroup: String? = nil) {
self.service = service
self.accessGroup = accessGroup
}
private func existsStatus(forKey key: String) -> OSStatus {
var query = baseQuery()
query[kSecAttrAccount as String] = key
let context = LAContext()
context.interactionNotAllowed = true
query[kSecUseAuthenticationContext as String] = context
var dataRef: CFTypeRef?
return SecItemCopyMatching(query as CFDictionary, &dataRef)
}
func setString(_ string: String?, forKey key: String, requireUserpresence: Bool = false) throws {
guard let string = string else {
try removeItem(forKey: key)
return
}
guard let data = string.data(using: .utf8) else {
throw KeyChainError.conversionError
}
try setData(data, forKey: key, requireUserpresence: requireUserpresence)
}
func string(forKey key: String) throws -> String? {
guard let data = try data(forKey: key) else { return nil }
guard let string = String(data: data, encoding: .utf8) else {
throw KeyChainError.conversionError
}
return string
}
func setData(_ data: Data?, forKey key: String, requireUserpresence: Bool = false) throws {
guard let data = data else {
try removeItem(forKey: key)
return
}
let status = existsStatus(forKey: key)
if status == errSecSuccess || status == errSecInteractionNotAllowed {
// Removing instead of updating prevents user presence checks from showing.
try removeItem(forKey: key)
}
if status == errSecItemNotFound || status == errSecSuccess || status == errSecInteractionNotAllowed {
var attributes = baseQuery()
attributes[kSecAttrAccount as String] = key
attributes[kSecValueData as String] = data
if requireUserpresence {
if let accessControl = SecAccessControlCreateWithFlags(
nil,
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
.biometryCurrentSet,
nil) {
attributes[kSecAttrAccessControl as String] = accessControl
}
} else {
attributes[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
}
let status = SecItemAdd(attributes as CFDictionary, nil)
guard status == errSecSuccess else {
throw KeyChainError.securityError(status: status)
}
} else {
throw KeyChainError.securityError(status: status)
}
}
func data(forKey key: String) throws -> Data? {
var query = baseQuery()
query[kSecMatchLimit as String] = kSecMatchLimitOne
query[kSecReturnData as String] = true
query[kSecAttrAccount as String] = key
query[kSecUseAuthenticationContext as String] = LAContext()
var dataRef: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &dataRef)
if status == errSecItemNotFound {
return nil
}
guard status == errSecSuccess else {
throw KeyChainError.securityError(status: status)
}
guard let data = dataRef as? Data else {
throw KeyChainError.unexpectedError
}
return data
}
func removeItem(forKey key: String) throws {
var query = baseQuery()
query[kSecAttrAccount as String] = key
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw KeyChainError.securityError(status: status)
}
}
func removeAllItems() throws {
let query = baseQuery()
// #if !TARGET_OS_IPHONE
// query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitAll;
// #endif
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw KeyChainError.securityError(status: status)
}
}
private func baseQuery() -> [String: Any] {
var query: [String: Any] = [:]
query[kSecClass as String] = kSecClassGenericPassword
query[kSecAttrSynchronizable as String] = kSecAttrSynchronizableAny
query[kSecAttrService as String] = service
#if !targetEnvironment(simulator)
if let accessGroup = accessGroup {
query[kSecAttrAccessGroup as String] = accessGroup
}
#endif
query[kSecUseOperationPrompt as String] = authenticationPrompt
return query
}
}