// // Keychain.swift // Werkout_ios // // Created by Trey Tartt on 7/5/23. // import Foundation class KeychainInterface { enum KeychainError: Error { // Attempted read for an item that does not exist. case itemNotFound // Attempted save to override an existing item. // Use update instead of save to update existing items case duplicateItem // A read of an item in any format other than Data case invalidItemFormat // Any operation result status than errSecSuccess case unexpectedStatus(OSStatus) } static let serviceID = "werkout.fitness" static func save(password: Data, service: String = KeychainInterface.serviceID, account: String) throws { let query: [String: AnyObject] = [ // kSecAttrService, kSecAttrAccount, and kSecClass // uniquely identify the item to save in Keychain kSecAttrService as String: service as AnyObject, kSecAttrAccount as String: account as AnyObject, kSecClass as String: kSecClassGenericPassword, kSecAttrSynchronizable as String: kCFBooleanTrue, // kSecValueData is the item value to save kSecValueData as String: password as AnyObject ] // SecItemAdd attempts to add the item identified by // the query to keychain let status = SecItemAdd( query as CFDictionary, nil ) // errSecDuplicateItem is a special case where the // item identified by the query already exists. Throw // duplicateItem so the client can determine whether // or not to handle this as an error if status == errSecDuplicateItem { throw KeychainError.duplicateItem } // Any status other than errSecSuccess indicates the // save operation failed. guard status == errSecSuccess else { throw KeychainError.unexpectedStatus(status) } } static func update(password: Data, service: String = KeychainInterface.serviceID, account: String) throws { let query: [String: AnyObject] = [ // kSecAttrService, kSecAttrAccount, and kSecClass // uniquely identify the item to update in Keychain kSecAttrService as String: service as AnyObject, kSecAttrAccount as String: account as AnyObject, kSecAttrSynchronizable as String: kCFBooleanTrue, kSecClass as String: kSecClassGenericPassword ] // attributes is passed to SecItemUpdate with // kSecValueData as the updated item value let attributes: [String: AnyObject] = [ kSecValueData as String: password as AnyObject ] // SecItemUpdate attempts to update the item identified // by query, overriding the previous value let status = SecItemUpdate( query as CFDictionary, attributes as CFDictionary ) // errSecItemNotFound is a special status indicating the // item to update does not exist. Throw itemNotFound so // the client can determine whether or not to handle // this as an error guard status != errSecItemNotFound else { throw KeychainError.itemNotFound } // Any status other than errSecSuccess indicates the // update operation failed. guard status == errSecSuccess else { throw KeychainError.unexpectedStatus(status) } } static func readPassword(service: String = KeychainInterface.serviceID, account: String) throws -> Data { let query: [String: AnyObject] = [ // kSecAttrService, kSecAttrAccount, and kSecClass // uniquely identify the item to read in Keychain kSecAttrService as String: service as AnyObject, kSecAttrAccount as String: account as AnyObject, kSecClass as String: kSecClassGenericPassword, kSecAttrSynchronizable as String: kCFBooleanTrue, // kSecMatchLimitOne indicates keychain should read // only the most recent item matching this query kSecMatchLimit as String: kSecMatchLimitOne, // kSecReturnData is set to kCFBooleanTrue in order // to retrieve the data for the item kSecReturnData as String: kCFBooleanTrue ] // SecItemCopyMatching will attempt to copy the item // identified by query to the reference itemCopy var itemCopy: AnyObject? let status = SecItemCopyMatching( query as CFDictionary, &itemCopy ) // errSecItemNotFound is a special status indicating the // read item does not exist. Throw itemNotFound so the // client can determine whether or not to handle // this case guard status != errSecItemNotFound else { throw KeychainError.itemNotFound } // Any status other than errSecSuccess indicates the // read operation failed. guard status == errSecSuccess else { throw KeychainError.unexpectedStatus(status) } // This implementation of KeychainInterface requires all // items to be saved and read as Data. Otherwise, // invalidItemFormat is thrown guard let password = itemCopy as? Data else { throw KeychainError.invalidItemFormat } return password } static func deletePassword(service: String = KeychainInterface.serviceID, account: String) throws { let query: [String: AnyObject] = [ // kSecAttrService, kSecAttrAccount, and kSecClass // uniquely identify the item to delete in Keychain kSecAttrService as String: service as AnyObject, kSecAttrAccount as String: account as AnyObject, kSecAttrSynchronizable as String: kCFBooleanTrue, kSecClass as String: kSecClassGenericPassword ] // SecItemDelete attempts to perform a delete operation // for the item identified by query. The status indicates // if the operation succeeded or failed. let status = SecItemDelete(query as CFDictionary) // Any status other than errSecSuccess indicates the // delete operation failed. guard status == errSecSuccess else { throw KeychainError.unexpectedStatus(status) } } }