Created
May 9, 2022 14:53
-
-
Save shawnthroop/feea6483aff048bdf66cf8a6d1c54b4d to your computer and use it in GitHub Desktop.
A simple and relatively typesafe interface for the Keychain
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
import Security | |
import Foundation | |
import CoreFoundation | |
typealias KeychainQuery = [CFString: Any] | |
typealias KeychainQueryItems = [KeychainQuery] | |
struct KeychainError: Error { | |
var status: OSStatus | |
init(_ status: OSStatus) { | |
self.status = status | |
} | |
} | |
enum Keychain { | |
static func add(_ query: KeychainQuery) throws { | |
let status = SecItemAdd(query as CFDictionary, nil) | |
if status != errSecSuccess { | |
throw KeychainError(status) | |
} | |
} | |
static func update(_ query: KeychainQuery, updated: KeychainQuery) throws { | |
let status = SecItemUpdate(query as CFDictionary, updated as CFDictionary) | |
if status != errSecSuccess { | |
throw KeychainError(status) | |
} | |
} | |
static func delete(_ query: KeychainQuery) throws { | |
let status = SecItemDelete(query as CFDictionary) | |
switch status { | |
case errSecSuccess, errSecItemNotFound: | |
return | |
default: | |
throw KeychainError(status) | |
} | |
} | |
static func items(matching query: KeychainQuery) throws -> KeychainQueryItems { | |
var ref: AnyObject? | |
let status = SecItemCopyMatching(query as CFDictionary, &ref) | |
switch status { | |
case errSecItemNotFound: | |
return [] | |
case errSecSuccess: | |
break | |
default: | |
throw KeychainError(status) | |
} | |
switch ref { | |
case let item as KeychainQuery: | |
return [item] | |
case let items as KeychainQueryItems: | |
return items | |
default: | |
throw KeychainError(errSecInvalidData) | |
} | |
} | |
} | |
protocol KeychainAction { | |
var query: KeychainQuery { get throws } | |
} | |
protocol KeychainInsert: KeychainAction { | |
func onDuplicateItem(_ query: inout KeychainQuery) -> KeychainQuery? | |
} | |
protocol KeychainRemove: KeychainAction {} | |
protocol KeychainMatch: KeychainAction { | |
associatedtype Value | |
func value(from items: KeychainQueryItems) throws -> Value | |
} | |
extension Keychain { | |
static func insert<Action: KeychainInsert>(_ action: Action) throws { | |
var query = try action.query | |
do { | |
try add(query) | |
} catch let error as KeychainError where error.status == errSecDuplicateItem { | |
guard let updated = action.onDuplicateItem(&query) else { throw error } | |
try update(query, updated: updated) | |
} catch { | |
throw error | |
} | |
} | |
static func remove<Action: KeychainRemove>(_ action: Action) throws { | |
try delete(action.query) | |
} | |
static func matching<Match: KeychainMatch>(_ action: Match) throws -> Match.Value { | |
try self[action] | |
} | |
static subscript<Match: KeychainMatch>(action: Match) -> Match.Value { | |
get throws { | |
try action.value(from: items(matching: action.query)) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment