import Foundation import Security struct StoredCredentials: Codable { let serverURL: String let username: String let token: String } enum KeychainError: Error { case osStatus(OSStatus) case encodingFailed } enum KeychainStore { private static let service = "com.local.Audiobookshelf-swift.auth" private static let account = "primary" static func save(_ creds: StoredCredentials) throws { let data = try JSONEncoder().encode(creds) let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account, ] SecItemDelete(query as CFDictionary) var attributes = query attributes[kSecValueData as String] = data attributes[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock let status = SecItemAdd(attributes as CFDictionary, nil) guard status == errSecSuccess else { throw KeychainError.osStatus(status) } } static func load() -> StoredCredentials? { 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) guard status == errSecSuccess, let data = item as? Data else { return nil } return try? JSONDecoder().decode(StoredCredentials.self, from: data) } static func delete() { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account, ] SecItemDelete(query as CFDictionary) } }