Skip to content

Instantly share code, notes, and snippets.

@shawnthroop
Created May 9, 2022 14:53
Show Gist options
  • Save shawnthroop/feea6483aff048bdf66cf8a6d1c54b4d to your computer and use it in GitHub Desktop.
Save shawnthroop/feea6483aff048bdf66cf8a6d1c54b4d to your computer and use it in GitHub Desktop.
A simple and relatively typesafe interface for the Keychain
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