Last active
September 28, 2019 22:27
-
-
Save humblehacker/f4d57e272440ad4c3331a72b8e84f356 to your computer and use it in GitHub Desktop.
Chris Eidhof's KeychainItem property wrapper made generic
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
// | |
// KeychainItem.swift | |
// | |
// Created by David Whetstone on 9/28/19. | |
// | |
// Original code by Chris Eidhof | |
// https://github.com/objcio/keychain-item | |
// As yet unlicensed | |
import Foundation | |
import Security | |
@propertyWrapper | |
final public class KeychainItem<T: Codable> { | |
private let account: String | |
public init(account: String) { | |
self.account = account | |
} | |
private var baseDictionary: [String:AnyObject] { | |
return [ | |
kSecClass as String: kSecClassGenericPassword, | |
kSecAttrAccount as String: account as AnyObject | |
] | |
} | |
private var query: [String:AnyObject] { | |
return baseDictionary.adding(key: kSecMatchLimit as String, value: kSecMatchLimitOne) | |
} | |
public var wrappedValue: T? { | |
get { | |
try! read() | |
} | |
set { | |
if let v = newValue { | |
if try! read() == nil { | |
try! add(v) | |
} else { | |
try! update(v) | |
} | |
} else { | |
try! delete() | |
} | |
} | |
} | |
private func delete() throws { | |
// SecItemDelete seems to fail with errSecItemNotFound if the item does not exist in the keychain. Is this expected behavior? | |
let status = SecItemDelete(baseDictionary as CFDictionary) | |
guard status != errSecItemNotFound else { return } | |
try throwIfNotZero(status) | |
} | |
private func read() throws -> T? { | |
let query = self.query.adding(key: kSecReturnData as String, value: true as AnyObject) | |
var result: AnyObject? = nil | |
let status = SecItemCopyMatching(query as CFDictionary, &result) | |
guard status != errSecItemNotFound else { return nil } | |
try throwIfNotZero(status) | |
guard let data = result as? Data else { throw KeychainError.invalidData } | |
return try decode(data) | |
} | |
private func update(_ secret: T) throws { | |
let dictionary: [String:AnyObject] = [ | |
kSecValueData as String: try encode(secret) as AnyObject | |
] | |
try throwIfNotZero(SecItemUpdate(baseDictionary as CFDictionary, dictionary as CFDictionary)) | |
} | |
private func add(_ secret: T) throws { | |
let dictionary = baseDictionary.adding(key: kSecValueData as String, value: try encode(secret) as AnyObject) | |
try throwIfNotZero(SecItemAdd(dictionary as CFDictionary, nil)) | |
} | |
private func encode(_ secret: T) throws -> Data { try JSONEncoder().encode(secret) } | |
private func decode(_ data: Data) throws -> T { try JSONDecoder().decode(T.self, from: data) } | |
} | |
private func throwIfNotZero(_ status: OSStatus) throws { | |
guard status != 0 else { return } | |
throw KeychainError.keychainError(status: status) | |
} | |
public enum KeychainError: Error { | |
case invalidData | |
case keychainError(status: OSStatus) | |
} | |
extension Dictionary { | |
func adding(key: Key, value: Value) -> Dictionary { | |
var copy = self | |
copy[key] = value | |
return copy | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment