import Foundation import Security public protocol KeychainService: Sendable { func save(_ value: String, forKey key: String) async throws func loadValue(forKey key: String) async throws -> String? } public actor MemoryKeychainService: KeychainService { private var storage: [String: String] = [:] public init() {} public func save(_ value: String, forKey key: String) async throws { storage[key] = value } public func loadValue(forKey key: String) async throws -> String? { storage[key] } } public enum KeychainServiceError: LocalizedError, Sendable { case invalidValue case unexpectedStatus(OSStatus) public var errorDescription: String? { switch self { case .invalidValue: return "The keychain returned invalid data." case let .unexpectedStatus(status): if let message = SecCopyErrorMessageString(status, nil) as String? { return message } return "The keychain returned status \(status)." } } } public actor SystemKeychainService: KeychainService { private let service: String public init(service: String = "de.diyaa.velody") { self.service = service } public func save(_ value: String, forKey key: String) async throws { let query = baseQuery(forKey: key) let deleteStatus = SecItemDelete(query as CFDictionary) guard deleteStatus == errSecSuccess || deleteStatus == errSecItemNotFound else { throw KeychainServiceError.unexpectedStatus(deleteStatus) } var addQuery = query addQuery[kSecValueData as String] = Data(value.utf8) let addStatus = SecItemAdd(addQuery as CFDictionary, nil) guard addStatus == errSecSuccess else { throw KeychainServiceError.unexpectedStatus(addStatus) } } public func loadValue(forKey key: String) async throws -> String? { var query = baseQuery(forKey: key) query[kSecReturnData as String] = kCFBooleanTrue query[kSecMatchLimit as String] = kSecMatchLimitOne var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result) switch status { case errSecSuccess: guard let data = result as? Data, let value = String(data: data, encoding: .utf8) else { throw KeychainServiceError.invalidValue } return value case errSecItemNotFound: return nil default: throw KeychainServiceError.unexpectedStatus(status) } } private func baseQuery(forKey key: String) -> [String: Any] { [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: key, ] } }