blob: 9dc8fe14a572003494be3026c34ae61e3a311ba4 (
plain) (
tree)
|
|
import Foundation
import Security
/// Thin wrapper over the Keychain for storing API keys. Service-scoped so
/// values never collide with other apps.
enum Keychain {
static let service = "co.soryu.makima"
enum Error: Swift.Error, Equatable {
case unhandled(OSStatus)
case invalidData
}
@discardableResult
static func set(_ value: String, for account: String) throws -> Bool {
let data = Data(value.utf8)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account
]
let attrs: [String: Any] = [
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
]
// Try update first
let updateStatus = SecItemUpdate(query as CFDictionary, attrs as CFDictionary)
if updateStatus == errSecSuccess { return true }
if updateStatus != errSecItemNotFound {
throw Error.unhandled(updateStatus)
}
var addQuery = query
for (k, v) in attrs { addQuery[k] = v }
let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
guard addStatus == errSecSuccess else {
throw Error.unhandled(addStatus)
}
return true
}
static func get(_ account: String) throws -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
if status == errSecItemNotFound { return nil }
guard status == errSecSuccess else { throw Error.unhandled(status) }
guard let data = item as? Data, let string = String(data: data, encoding: .utf8) else {
throw Error.invalidData
}
return string
}
@discardableResult
static func delete(_ account: String) throws -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account
]
let status = SecItemDelete(query as CFDictionary)
if status == errSecSuccess || status == errSecItemNotFound { return true }
throw Error.unhandled(status)
}
}
|