import Foundation import Security struct KeychainManager { private let service = "net.cleberg.rune" private let account = "api-token" func saveToken(_ token: String) throws { let encodedToken = Data(token.utf8) var query = baseQuery() query[kSecValueData as String] = encodedToken query[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly let status = SecItemAdd(query as CFDictionary, nil) if status == errSecDuplicateItem { try updateToken(token) return } guard status == errSecSuccess else { throw KeychainError.unhandled(status) } } func readToken() throws -> String? { var query = baseQuery() query[kSecReturnData as String] = true query[kSecMatchLimit as String] = kSecMatchLimitOne var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result) if status == errSecItemNotFound { return nil } guard status == errSecSuccess else { throw KeychainError.unhandled(status) } guard let data = result as? Data, let token = String(data: data, encoding: .utf8) else { throw KeychainError.invalidData } return token } func deleteToken() throws { let status = SecItemDelete(baseQuery() as CFDictionary) guard status == errSecSuccess || status == errSecItemNotFound else { throw KeychainError.unhandled(status) } } private func updateToken(_ token: String) throws { let attributes: [String: Any] = [ kSecValueData as String: Data(token.utf8), kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly ] let status = SecItemUpdate(baseQuery() as CFDictionary, attributes as CFDictionary) guard status == errSecSuccess else { throw KeychainError.unhandled(status) } } private func baseQuery() -> [String: Any] { [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account ] } } enum KeychainError: LocalizedError { case invalidData case unhandled(OSStatus) var errorDescription: String? { switch self { case .invalidData: return "Stored token data is invalid." case .unhandled(let status): return SecCopyErrorMessageString(status, nil) as String? ?? "Keychain error: \(status)" } } }