forked from mattrubin/OneTimePassword
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Keychain.swift
260 lines (229 loc) · 10.5 KB
/
Keychain.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
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
257
258
259
260
//
// Keychain.swift
// OneTimePassword
//
// Copyright (c) 2014-2018 Matt Rubin and the OneTimePassword authors
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import Foundation
/// The `Keychain`'s shared instance is a singleton which represents the iOS system keychain used
/// to securely store tokens.
public final class Keychain {
/// The singleton `Keychain` instance.
public static let sharedInstance = Keychain()
// MARK: Read
/// Finds the persistent token with the given identifer, if one exists.
///
/// - parameter identifier: The persistent identifier for the desired token.
///
/// - throws: A `Keychain.Error` if an error occurred.
/// - returns: The persistent token, or `nil` if no token matched the given identifier.
public func persistentToken(withIdentifier identifier: Data) throws -> PersistentToken? {
return try keychainItem(forPersistentRef: identifier).map(PersistentToken.init(keychainDictionary:))
}
/// Returns the set of all persistent tokens found in the keychain.
///
/// - throws: A `Keychain.Error` if an error occurred.
public func allPersistentTokens() throws -> Set<PersistentToken> {
let allItems = try allKeychainItems()
// This code intentionally ignores items which fail deserialization, instead opting to return as many readable
// tokens as possible.
// TODO: Restore deserialization error handling, in a way that provides info on the failure reason and allows
// the caller to choose whether to fail completely or recover some data.
return Set(allItems.compactMap({ try? PersistentToken(keychainDictionary: $0) }))
}
// MARK: Write
/// Adds the given token to the keychain and returns the persistent token which contains it.
///
/// - parameter token: The token to save to the keychain.
///
/// - throws: A `Keychain.Error` if the token was not added successfully.
/// - returns: The new persistent token.
public func add(_ token: Token) throws -> PersistentToken {
let attributes = try token.keychainAttributes()
let persistentRef = try addKeychainItem(withAttributes: attributes)
return PersistentToken(token: token, identifier: persistentRef)
}
/// Updates the given persistent token with a new token value.
///
/// - parameter persistentToken: The persistent token to update.
/// - parameter token: The new token value.
///
/// - throws: A `Keychain.Error` if the update did not succeed.
/// - returns: The updated persistent token.
public func update(_ persistentToken: PersistentToken, with token: Token) throws -> PersistentToken {
let attributes = try token.keychainAttributes()
try updateKeychainItem(forPersistentRef: persistentToken.identifier,
withAttributes: attributes)
return PersistentToken(token: token, identifier: persistentToken.identifier)
}
/// Deletes the given persistent token from the keychain.
///
/// - note: After calling `deletePersistentToken(_:)`, the persistent token's `identifier` is no
/// longer valid, and the token should be discarded.
///
/// - parameter persistentToken: The persistent token to delete.
///
/// - throws: A `Keychain.Error` if the deletion did not succeed.
public func delete(_ persistentToken: PersistentToken) throws {
try deleteKeychainItem(forPersistentRef: persistentToken.identifier)
}
// MARK: Errors
/// An error type enum representing the various errors a `Keychain` operation can throw.
public enum Error: Swift.Error {
/// The keychain operation returned a system error code.
case systemError(OSStatus)
/// The keychain operation returned an unexpected type of data.
case incorrectReturnType
/// The given token could not be serialized to keychain data.
case tokenSerializationFailure
}
}
// MARK: - Private
private let kOTPService = "me.mattrubin.onetimepassword.token"
private let urlStringEncoding = String.Encoding.utf8
private extension Token {
func keychainAttributes() throws -> [String: AnyObject] {
let url = try self.toURL()
guard let data = url.absoluteString.data(using: urlStringEncoding) else {
throw Keychain.Error.tokenSerializationFailure
}
return [
kSecAttrGeneric as String: data as NSData,
kSecValueData as String: generator.secret as NSData,
kSecAttrService as String: kOTPService as NSString,
]
}
}
private extension PersistentToken {
enum DeserializationError: Error {
case missingData
case missingSecret
case missingPersistentRef
case unreadableData
}
init(keychainDictionary: NSDictionary) throws {
guard let urlData = keychainDictionary[kSecAttrGeneric as String] as? Data else {
throw DeserializationError.missingData
}
guard let secret = keychainDictionary[kSecValueData as String] as? Data else {
throw DeserializationError.missingSecret
}
guard let keychainItemRef = keychainDictionary[kSecValuePersistentRef as String] as? Data else {
throw DeserializationError.missingPersistentRef
}
guard let urlString = String(data: urlData, encoding: urlStringEncoding),
let url = URL(string: urlString) else {
throw DeserializationError.unreadableData
}
let token = try Token(_url: url, secret: secret)
self.init(token: token, identifier: keychainItemRef)
}
}
private func addKeychainItem(withAttributes attributes: [String: AnyObject]) throws -> Data {
var mutableAttributes = attributes
mutableAttributes[kSecClass as String] = kSecClassGenericPassword
mutableAttributes[kSecReturnPersistentRef as String] = kCFBooleanTrue
// Set a random string for the account name.
// We never query by or display this value, but the keychain requires it to be unique.
if mutableAttributes[kSecAttrAccount as String] == nil {
mutableAttributes[kSecAttrAccount as String] = UUID().uuidString as NSString
}
var result: AnyObject?
let resultCode: OSStatus = withUnsafeMutablePointer(to: &result) {
SecItemAdd(mutableAttributes as CFDictionary, $0)
}
guard resultCode == errSecSuccess else {
throw Keychain.Error.systemError(resultCode)
}
guard let persistentRef = result as? Data else {
throw Keychain.Error.incorrectReturnType
}
return persistentRef
}
private func updateKeychainItem(forPersistentRef persistentRef: Data,
withAttributes attributesToUpdate: [String: AnyObject]) throws {
let queryDict: [String: AnyObject] = [
kSecClass as String: kSecClassGenericPassword,
kSecValuePersistentRef as String: persistentRef as NSData,
]
let resultCode = SecItemUpdate(queryDict as CFDictionary, attributesToUpdate as CFDictionary)
guard resultCode == errSecSuccess else {
throw Keychain.Error.systemError(resultCode)
}
}
private func deleteKeychainItem(forPersistentRef persistentRef: Data) throws {
let queryDict: [String: AnyObject] = [
kSecClass as String: kSecClassGenericPassword,
kSecValuePersistentRef as String: persistentRef as NSData,
]
let resultCode = SecItemDelete(queryDict as CFDictionary)
guard resultCode == errSecSuccess else {
throw Keychain.Error.systemError(resultCode)
}
}
private func keychainItem(forPersistentRef persistentRef: Data) throws -> NSDictionary? {
let queryDict: [String: AnyObject] = [
kSecClass as String: kSecClassGenericPassword,
kSecValuePersistentRef as String: persistentRef as NSData,
kSecReturnPersistentRef as String: kCFBooleanTrue,
kSecReturnAttributes as String: kCFBooleanTrue,
kSecReturnData as String: kCFBooleanTrue,
]
var result: AnyObject?
let resultCode = withUnsafeMutablePointer(to: &result) {
SecItemCopyMatching(queryDict as CFDictionary, $0)
}
if resultCode == errSecItemNotFound {
// Not finding any keychain items is not an error in this case. Return nil.
return nil
}
guard resultCode == errSecSuccess else {
throw Keychain.Error.systemError(resultCode)
}
guard let keychainItem = result as? NSDictionary else {
throw Keychain.Error.incorrectReturnType
}
return keychainItem
}
private func allKeychainItems() throws -> [NSDictionary] {
let queryDict: [String: AnyObject] = [
kSecClass as String: kSecClassGenericPassword,
kSecMatchLimit as String: kSecMatchLimitAll,
kSecReturnPersistentRef as String: kCFBooleanTrue,
kSecReturnAttributes as String: kCFBooleanTrue,
kSecReturnData as String: kCFBooleanTrue,
]
var result: AnyObject?
let resultCode = withUnsafeMutablePointer(to: &result) {
SecItemCopyMatching(queryDict as CFDictionary, $0)
}
if resultCode == errSecItemNotFound {
// Not finding any keychain items is not an error in this case. Return an empty array.
return []
}
guard resultCode == errSecSuccess else {
throw Keychain.Error.systemError(resultCode)
}
guard let keychainItems = result as? [NSDictionary] else {
throw Keychain.Error.incorrectReturnType
}
return keychainItems
}