Skip to content

Instantly share code, notes, and snippets.

@humblehacker
Last active September 28, 2019 22:27
Show Gist options
  • Save humblehacker/f4d57e272440ad4c3331a72b8e84f356 to your computer and use it in GitHub Desktop.
Save humblehacker/f4d57e272440ad4c3331a72b8e84f356 to your computer and use it in GitHub Desktop.
Chris Eidhof's KeychainItem property wrapper made generic
//
// 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