this repo has no description
at main 179 lines 7.4 kB view raw
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}