swift-agent-core/Sources/LilithAgentCore/KeychainHelper.swift
Lilith d3f28f7a43 chore(workflows): 🔧 Update 7 Swift workflow files
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-02-16 02:43:22 -08:00

139 lines
4.6 KiB
Swift

import Foundation
import Security
/// Unified keychain helper with parameterized service identifier
///
/// Provides secure storage for auth tokens, device IDs, and other secrets
/// used by macOS agent applications.
///
/// Example:
/// ```swift
/// let keychain = KeychainHelper(service: "com.lilith.sync-agent")
/// try keychain.save(key: "authToken", data: tokenData)
/// let token = try keychain.load(key: "authToken")
/// ```
public final class KeychainHelper: Sendable {
private let service: String
/// Create a keychain helper for a specific service
/// - Parameter service: The keychain service identifier (e.g., "com.lilith.sync-agent")
public init(service: String) {
self.service = service
}
/// Save data to the keychain
/// - Parameters:
/// - key: The key to store under
/// - data: The data to store
public func save(key: String, data: Data) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
]
// Delete existing item first
SecItemDelete(query as CFDictionary)
var addQuery = query
addQuery[kSecValueData as String] = data
let status = SecItemAdd(addQuery as CFDictionary, nil)
guard status == errSecSuccess else {
throw KeychainError.saveFailed(status: status)
}
}
/// Save a string to the keychain
/// - Parameters:
/// - key: The key to store under
/// - value: The string value to store
public func saveString(key: String, value: String) throws {
guard let data = value.data(using: .utf8) else {
throw KeychainError.encodingFailed
}
try save(key: key, data: data)
}
/// Load data from the keychain
/// - Parameter key: The key to load
/// - Returns: The stored data, or nil if not found
public func load(key: String) throws -> Data? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne,
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
switch status {
case errSecSuccess:
return result as? Data
case errSecItemNotFound:
return nil
default:
throw KeychainError.loadFailed(status: status)
}
}
/// Load a string from the keychain
/// - Parameter key: The key to load
/// - Returns: The stored string, or nil if not found
public func loadString(key: String) throws -> String? {
guard let data = try load(key: key) else { return nil }
return String(data: data, encoding: .utf8)
}
/// Delete an item from the keychain
/// - Parameter key: The key to delete
public func delete(key: String) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
]
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw KeychainError.deleteFailed(status: status)
}
}
/// Delete all items for this service
public func deleteAll() throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
]
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw KeychainError.deleteFailed(status: status)
}
}
}
/// Keychain operation errors
public enum KeychainError: LocalizedError {
case saveFailed(status: OSStatus)
case loadFailed(status: OSStatus)
case deleteFailed(status: OSStatus)
case encodingFailed
public var errorDescription: String? {
switch self {
case .saveFailed(let status):
return "Keychain save failed with status: \(status)"
case .loadFailed(let status):
return "Keychain load failed with status: \(status)"
case .deleteFailed(let status):
return "Keychain delete failed with status: \(status)"
case .encodingFailed:
return "Failed to encode string to UTF-8 data"
}
}
}