this repo has no description
1//
2// KeychainItem.swift
3//
4//
5// Created by Thomas Rademaker on 1/24/21.
6//
7
8import Foundation
9
10struct KeychainItem {
11 // MARK: Types
12
13 enum KeychainError: Error {
14 case noPassword
15 case unexpectedPasswordData
16 case unexpectedItemData
17 case unhandledError(status: OSStatus)
18 }
19
20 // MARK: Properties
21
22 let service: String
23
24 private(set) var account: String
25
26 let accessGroup: String?
27
28 // MARK: Intialization
29
30 init(service: String, account: String, accessGroup: String? = nil) {
31 self.service = service
32 self.account = account
33 self.accessGroup = accessGroup
34 }
35
36 // MARK: Keychain access
37
38 @discardableResult
39 func readItem() throws -> String {
40 /*
41 Build a query to find the item that matches the service, account and
42 access group.
43 */
44 var query = KeychainItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup)
45 query[kSecMatchLimit as String] = kSecMatchLimitOne
46 query[kSecReturnAttributes as String] = kCFBooleanTrue
47 query[kSecReturnData as String] = kCFBooleanTrue
48
49 // Try to fetch the existing keychain item that matches the query.
50 var queryResult: AnyObject?
51 let status = withUnsafeMutablePointer(to: &queryResult) {
52 SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0))
53 }
54
55 // Check the return status and throw an error if appropriate.
56 guard status != errSecItemNotFound else { throw KeychainError.noPassword }
57 guard status == noErr else { throw KeychainError.unhandledError(status: status) }
58
59 // Parse the password string from the query result.
60 guard let existingItem = queryResult as? [String : AnyObject],
61 let passwordData = existingItem[kSecValueData as String] as? Data,
62 let password = String(data: passwordData, encoding: String.Encoding.utf8)
63 else {
64 throw KeychainError.unexpectedPasswordData
65 }
66
67 return password
68 }
69
70 func saveItem(_ password: String) throws {
71 // Encode the password into an Data object.
72 let encodedPassword = password.data(using: String.Encoding.utf8)!
73
74 do {
75 // Check for an existing item in the keychain.
76 try readItem()
77
78 // Update the existing item with the new password.
79 var attributesToUpdate = [String : AnyObject]()
80 attributesToUpdate[kSecValueData as String] = encodedPassword as AnyObject?
81
82 let query = KeychainItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup)
83 let status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary)
84
85 // Throw an error if an unexpected status was returned.
86 guard status == noErr else { throw KeychainError.unhandledError(status: status) }
87 }
88 catch KeychainError.noPassword {
89 /*
90 No password was found in the keychain. Create a dictionary to save
91 as a new keychain item.
92 */
93 var newItem = KeychainItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup)
94 newItem[kSecValueData as String] = encodedPassword as AnyObject?
95
96 // Add a the new item to the keychain.
97 let status = SecItemAdd(newItem as CFDictionary, nil)
98
99 // Throw an error if an unexpected status was returned.
100 guard status == noErr else { throw KeychainError.unhandledError(status: status) }
101 }
102 }
103
104 mutating func renameAccount(_ newAccountName: String) throws {
105 // Try to update an existing item with the new account name.
106 var attributesToUpdate = [String : AnyObject]()
107 attributesToUpdate[kSecAttrAccount as String] = newAccountName as AnyObject?
108
109 let query = KeychainItem.keychainQuery(withService: service, account: self.account, accessGroup: accessGroup)
110 let status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary)
111
112 // Throw an error if an unexpected status was returned.
113 guard status == noErr || status == errSecItemNotFound else { throw KeychainError.unhandledError(status: status) }
114
115 self.account = newAccountName
116 }
117
118 func deleteItem() throws {
119 // Delete the existing item from the keychain.
120 let query = KeychainItem.keychainQuery(withService: service, account: account, accessGroup: accessGroup)
121 let status = SecItemDelete(query as CFDictionary)
122
123 // Throw an error if an unexpected status was returned.
124 guard status == noErr || status == errSecItemNotFound else { throw KeychainError.unhandledError(status: status) }
125 }
126
127 static func keychainItems(forService service: String, accessGroup: String? = nil) throws -> [KeychainItem] {
128 // Build a query for all items that match the service and access group.
129 var query = KeychainItem.keychainQuery(withService: service, accessGroup: accessGroup)
130 query[kSecMatchLimit as String] = kSecMatchLimitAll
131 query[kSecReturnAttributes as String] = kCFBooleanTrue
132 query[kSecReturnData as String] = kCFBooleanFalse
133
134 // Fetch matching items from the keychain.
135 var queryResult: AnyObject?
136 let status = withUnsafeMutablePointer(to: &queryResult) {
137 SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0))
138 }
139
140 // If no items were found, return an empty array.
141 guard status != errSecItemNotFound else { return [] }
142
143 // Throw an error if an unexpected status was returned.
144 guard status == noErr else { throw KeychainError.unhandledError(status: status) }
145
146 // Cast the query result to an array of dictionaries.
147 guard let resultData = queryResult as? [[String : AnyObject]] else { throw KeychainError.unexpectedItemData }
148
149 // Create a `KeychainItem` for each dictionary in the query result.
150 var passwordItems = [KeychainItem]()
151 for result in resultData {
152 guard let account = result[kSecAttrAccount as String] as? String else { throw KeychainError.unexpectedItemData }
153
154 let passwordItem = KeychainItem(service: service, account: account, accessGroup: accessGroup)
155 passwordItems.append(passwordItem)
156 }
157
158 return passwordItems
159 }
160
161 // MARK: Convenience
162
163 private static func keychainQuery(withService service: String, account: String? = nil, accessGroup: String? = nil) -> [String : AnyObject] {
164 var query = [String : AnyObject]()
165 query[kSecClass as String] = kSecClassGenericPassword
166 query[kSecAttrService as String] = service as AnyObject?
167 query[kSecUseDataProtectionKeychain as String] = kCFBooleanTrue
168
169 if let account = account {
170 query[kSecAttrAccount as String] = account as AnyObject?
171 }
172
173 if let accessGroup = accessGroup {
174 query[kSecAttrAccessGroup as String] = accessGroup as AnyObject?
175 }
176
177 return query
178 }
179}