-
-
Save pauljohanneskraft/4652fbeae67a2206ad6b4296675e9bb5 to your computer and use it in GitHub Desktop.
struct KeychainItem { | |
// MARK: Nested Types | |
enum KeychainError: Error { | |
case noPassword | |
case unexpectedPasswordData | |
case unexpectedItemData | |
case unhandledError(status: OSStatus) | |
} | |
// MARK: Stored Properties | |
let service: String | |
let account: String | |
let accessGroup: String? | |
// MARK: Initialization | |
init(service: String, account: String, accessGroup: String? = nil) { | |
self.service = service | |
self.account = account | |
self.accessGroup = accessGroup | |
} | |
// MARK: Methods | |
func get() -> String? { | |
/* | |
Build a query to find the item that matches the service, account and | |
access group. | |
*/ | |
var query = KeychainItem.query(service: service, account: account, accessGroup: accessGroup) | |
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 { return nil } | |
guard status == noErr else { return nil } | |
// 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 { | |
return nil | |
} | |
return password | |
} | |
func set(_ value: String?) { | |
if let value = value { | |
save(value) | |
} else { | |
delete() | |
} | |
} | |
func save(_ password: String) { | |
// Encode the password into an Data object. | |
let encodedPassword = Data(password.utf8) | |
if get() != nil { | |
var attributesToUpdate = [String: AnyObject]() | |
attributesToUpdate[kSecValueData as String] = encodedPassword as AnyObject? | |
let query = KeychainItem.query(service: service, | |
account: account, | |
accessGroup: accessGroup) | |
_ = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary) | |
} else { | |
var newItem = KeychainItem.query(service: service, | |
account: account, | |
accessGroup: accessGroup) | |
newItem[kSecValueData as String] = encodedPassword as AnyObject? | |
_ = SecItemAdd(newItem as CFDictionary, nil) | |
} | |
} | |
func delete() { | |
// Delete the existing item from the keychain. | |
let query = KeychainItem.query(service: service, account: account, accessGroup: accessGroup) | |
_ = SecItemDelete(query as CFDictionary) | |
} | |
// MARK: Helpers | |
private static func items(forService service: String, accessGroup: String? = nil) throws -> [KeychainItem] { | |
// Build a query for all items that match the service and access group. | |
var query = KeychainItem.query(service: service, accessGroup: accessGroup) | |
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 `KeychainItem` for each dictionary in the query result. | |
var items = [KeychainItem]() | |
for result in resultData { | |
guard let account = result[kSecAttrAccount as String] as? String else { | |
continue | |
} | |
let item = KeychainItem(service: service, account: account, accessGroup: accessGroup) | |
items.append(item) | |
} | |
return items | |
} | |
private static func query(service: String, | |
account: String? = nil, | |
accessGroup: 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? | |
} | |
return query | |
} | |
} |
ah, I checked it in Xcode and there was a small typo, since it is called Data(base64Encoded:)
instead of Data(base64encoded:)
. Fixed it in the version above.
I had fixed that as well but still get the error...
For me it does compile, so can you please check, if you have really copied the whole snippet? For example, the type of the wrappedValue
property is C?
instead of String?
now. If I change it back to String?
, I get the same compiler error.
Right. I had left it to String?
Thank you so much for your help & responsiveness. You rock!
Perfect - you're welcome! 😊 I have also just updated the code snippet with some increased readability.
I am working on a music player at the moment. You will be the first to get a free copy once it is available!
I wish property wrappers could conform to Observable Object. It would make it much easier to implement in SwiftUI
@akillamac Great, thanks - looking forward to it!
Have a look at the DynamicProperty
protocol, that might just be what you need.
Unfortunately it does not work, XCode complains with the following error "type of expression is ambiguous without more context" on the line