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) } }