import Foundation
#if canImport(Security)
import Security
/// Stores the token as a generic password item (this device only, after first unlock).
public struct KeychainCredentialStore: CredentialStore {
public var service: String
public var account: String
public var accessGroup: String?
public init(service: String = "ro.ird.api.credential", account: String = "default", accessGroup: String? = nil) {
self.service = service
self.account = account
self.accessGroup = accessGroup
}
private func query() -> [String: Any] {
var q: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account]
if let accessGroup { q[kSecAttrAccessGroup as String] = accessGroup }
return q
}
public func token() async throws -> String? { try load() }
/// Synchronous read, for callers that are not async.
public func load() throws -> String? {
var q = query()
q[kSecReturnData as String] = true
q[kSecMatchLimit as String] = kSecMatchLimitOne
var item: CFTypeRef?
let status = SecItemCopyMatching(q as CFDictionary, &item)
if status == errSecItemNotFound { return nil }
guard status == errSecSuccess else { throw CredentialStoreError.keychain(status: status) }
guard let data = item as? Data, let token = String(data: data, encoding: .utf8) else { throw CredentialStoreError.corrupt }
return token
}
public func save(_ token: String) throws {
let data = Data(token.utf8)
var add = query()
add[kSecValueData as String] = data
add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
let status = SecItemAdd(add as CFDictionary, nil)
if status == errSecDuplicateItem {
let update = SecItemUpdate(query() as CFDictionary, [kSecValueData as String: data] as CFDictionary)
guard update == errSecSuccess else { throw CredentialStoreError.keychain(status: update) }
return
}
guard status == errSecSuccess else { throw CredentialStoreError.keychain(status: status) }
}
public func delete() throws {
let status = SecItemDelete(query() as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else { throw CredentialStoreError.keychain(status: status) }
}
}
#endif