Last active
November 19, 2021 10:30
-
-
Save tadija/76dceb3a6a52158f0a73b83bda27c481 to your computer and use it in GitHub Desktop.
AEKeychain
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* https://gist.github.com/tadija/76dceb3a6a52158f0a73b83bda27c481 | |
* Revision 4 | |
* Copyright © 2020-2021 Marko Tadić | |
* Licensed under the MIT license | |
*/ | |
import Foundation | |
open class AEKeychain<Key: RawRepresentable> where Key.RawValue == String { | |
public let service: String | |
public let group: String? | |
public let accessibility: String? | |
public init( | |
service: String, | |
group: String? = nil, | |
accessibility: String? = nil | |
) { | |
self.service = service | |
self.group = group | |
self.accessibility = accessibility | |
} | |
public func get(_ key: Key) throws -> String { | |
try keychainItem(key.rawValue).readPassword() | |
} | |
public func set(_ value: String?, for key: Key) throws { | |
if let value = value { | |
try keychainItem(key.rawValue).savePassword(value) | |
} else { | |
try remove(key) | |
} | |
} | |
public func remove(_ key: Key) throws { | |
try keychainItem(key.rawValue).deleteItem() | |
} | |
private func keychainItem(_ key: String) -> KeychainPasswordItem { | |
KeychainPasswordItem( | |
service: service, | |
account: key, | |
accessGroup: group, | |
accessibility: accessibility | |
) | |
} | |
} | |
/// - See: https://developer.apple.com/library/ios/samplecode/GenericKeychain | |
/* | |
Copyright (C) 2016 Apple Inc. All Rights Reserved. | |
See LICENSE.txt for this sample’s licensing information | |
Abstract: | |
A struct for accessing generic password keychain items. | |
*/ | |
/// - Note: add `accessibility` for `kSecAttrAccessible` attribute (2021) | |
public struct KeychainPasswordItem { | |
// MARK: Types | |
public enum KeychainError: Error { | |
case noPassword | |
case unexpectedPasswordData | |
case unexpectedItemData | |
case unhandledError(status: OSStatus) | |
} | |
// MARK: Properties | |
public let service: String | |
public private(set) var account: String | |
public let accessGroup: String? | |
public var accessibility: String? | |
// MARK: Intialization | |
public init( | |
service: String, | |
account: String, | |
accessGroup: String? = nil, | |
accessibility: String? = nil | |
) { | |
self.service = service | |
self.account = account | |
self.accessGroup = accessGroup | |
self.accessibility = accessibility | |
} | |
// MARK: Keychain access | |
public func readPassword() throws -> String { | |
// Build a query to find the item that matches the service, account and access group. | |
var query = KeychainPasswordItem.keychainQuery( | |
withService: service, | |
account: account, | |
accessGroup: accessGroup, | |
accessibility: accessibility | |
) | |
query[kSecMatchLimit as String] = kSecMatchLimitOne | |
query[kSecReturnAttributes as String] = kCFBooleanTrue | |
query[kSecReturnData as String] = kCFBooleanTrue | |
// Try to fetch the existing keychain item that matches the query. | |
var queryResult: AnyObject? | |
let status = withUnsafeMutablePointer(to: &queryResult) { | |
SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0)) | |
} | |
// Check the return status and throw an error if appropriate. | |
guard status != errSecItemNotFound else { throw KeychainError.noPassword } | |
guard status == noErr else { throw KeychainError.unhandledError(status: status) } | |
// Parse the password string from the query result. | |
guard let existingItem = queryResult as? [String : AnyObject], | |
let passwordData = existingItem[kSecValueData as String] as? Data, | |
let password = String(data: passwordData, encoding: String.Encoding.utf8) | |
else { | |
throw KeychainError.unexpectedPasswordData | |
} | |
return password | |
} | |
public func savePassword(_ password: String) throws { | |
// Encode the password into an Data object. | |
let encodedPassword = password.data(using: String.Encoding.utf8)! | |
do { | |
// Check for an existing item in the keychain. | |
try _ = readPassword() | |
// Update the existing item with the new password. | |
var attributesToUpdate = [String : AnyObject]() | |
attributesToUpdate[kSecValueData as String] = encodedPassword as AnyObject? | |
let query = KeychainPasswordItem.keychainQuery( | |
withService: service, | |
account: account, | |
accessGroup: accessGroup, | |
accessibility: accessibility | |
) | |
let status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary) | |
// Throw an error if an unexpected status was returned. | |
guard status == noErr else { throw KeychainError.unhandledError(status: status) } | |
} catch KeychainError.noPassword { | |
/* | |
No password was found in the keychain. Create a dictionary to save | |
as a new keychain item. | |
*/ | |
var newItem = KeychainPasswordItem.keychainQuery( | |
withService: service, | |
account: account, | |
accessGroup: accessGroup, | |
accessibility: accessibility | |
) | |
newItem[kSecValueData as String] = encodedPassword as AnyObject? | |
// Add a the new item to the keychain. | |
let status = SecItemAdd(newItem as CFDictionary, nil) | |
// Throw an error if an unexpected status was returned. | |
guard status == noErr else { throw KeychainError.unhandledError(status: status) } | |
} | |
} | |
public mutating func renameAccount(_ newAccountName: String) throws { | |
// Try to update an existing item with the new account name. | |
var attributesToUpdate = [String : AnyObject]() | |
attributesToUpdate[kSecAttrAccount as String] = newAccountName as AnyObject? | |
let query = KeychainPasswordItem.keychainQuery( | |
withService: service, | |
account: self.account, | |
accessGroup: accessGroup, | |
accessibility: accessibility | |
) | |
let status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary) | |
// Throw an error if an unexpected status was returned. | |
guard status == noErr || status == errSecItemNotFound else { | |
throw KeychainError.unhandledError(status: status) | |
} | |
self.account = newAccountName | |
} | |
public func deleteItem() throws { | |
// Delete the existing item from the keychain. | |
let query = KeychainPasswordItem.keychainQuery( | |
withService: service, | |
account: account, | |
accessGroup: accessGroup, | |
accessibility: accessibility | |
) | |
let status = SecItemDelete(query as CFDictionary) | |
// Throw an error if an unexpected status was returned. | |
guard status == noErr || status == errSecItemNotFound else { | |
throw KeychainError.unhandledError(status: status) | |
} | |
} | |
public static func passwordItems( | |
forService service: String, | |
accessGroup: String? = nil, | |
accessibility: String? = nil | |
) throws -> [KeychainPasswordItem] { | |
// Build a query for all items that match the service and access group. | |
var query = KeychainPasswordItem.keychainQuery( | |
withService: service, | |
accessGroup: accessGroup, | |
accessibility: accessibility | |
) | |
query[kSecMatchLimit as String] = kSecMatchLimitAll | |
query[kSecReturnAttributes as String] = kCFBooleanTrue | |
query[kSecReturnData as String] = kCFBooleanFalse | |
// Fetch matching items from the keychain. | |
var queryResult: AnyObject? | |
let status = withUnsafeMutablePointer(to: &queryResult) { | |
SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0)) | |
} | |
// If no items were found, return an empty array. | |
guard status != errSecItemNotFound else { return [] } | |
// Throw an error if an unexpected status was returned. | |
guard status == noErr else { throw KeychainError.unhandledError(status: status) } | |
// Cast the query result to an array of dictionaries. | |
guard let resultData = queryResult as? [[String : AnyObject]] else { | |
throw KeychainError.unexpectedItemData | |
} | |
// Create a `KeychainPasswordItem` for each dictionary in the query result. | |
var passwordItems = [KeychainPasswordItem]() | |
for result in resultData { | |
guard let account = result[kSecAttrAccount as String] as? String else { | |
throw KeychainError.unexpectedItemData | |
} | |
let passwordItem = KeychainPasswordItem( | |
service: service, | |
account: account, | |
accessGroup: accessGroup, | |
accessibility: accessibility | |
) | |
passwordItems.append(passwordItem) | |
} | |
return passwordItems | |
} | |
// MARK: Convenience | |
private static func keychainQuery( | |
withService service: String, | |
account: String? = nil, | |
accessGroup: String? = nil, | |
accessibility: String? = nil | |
) -> [String : AnyObject] { | |
var query = [String : AnyObject]() | |
query[kSecClass as String] = kSecClassGenericPassword | |
query[kSecAttrService as String] = service as AnyObject? | |
if let account = account { | |
query[kSecAttrAccount as String] = account as AnyObject? | |
} | |
if let accessGroup = accessGroup { | |
query[kSecAttrAccessGroup as String] = accessGroup as AnyObject? | |
} | |
if let accessibility = accessibility { | |
query[kSecAttrAccessible as String] = accessibility as AnyObject? | |
} | |
return query | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment