blitztext-app-linux/BlitztextMac/Services/KeychainService.swift
2026-05-23 07:28:46 +02:00

92 lines
2.6 KiB
Swift

import Foundation
import Security
enum KeychainKey: String, CaseIterable, Codable {
case openAIAPIKey = "openAIAPIKey"
var label: String {
switch self {
case .openAIAPIKey: return "OpenAI API Key"
}
}
}
/// Stores preview credentials in the user's macOS Keychain.
enum KeychainService {
private static let service = "app.blitztext.preview.credentials"
static func save(key: KeychainKey, value: String) throws {
let data = Data(value.utf8)
var query = baseQuery(for: key)
query[kSecValueData as String] = data
query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
let status = SecItemAdd(query as CFDictionary, nil)
if status == errSecDuplicateItem {
let updateStatus = SecItemUpdate(
baseQuery(for: key) as CFDictionary,
[kSecValueData as String: data] as CFDictionary
)
guard updateStatus == errSecSuccess else {
throw KeychainError.saveFailed(updateStatus)
}
return
}
guard status == errSecSuccess else {
throw KeychainError.saveFailed(status)
}
}
static func load(key: KeychainKey) -> String? {
var query = baseQuery(for: key)
query[kSecMatchLimit as String] = kSecMatchLimitOne
query[kSecReturnData as String] = true
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status == errSecSuccess,
let data = item as? Data,
let value = String(data: data, encoding: .utf8),
!value.isEmpty else {
return nil
}
return value
}
static func delete(key: KeychainKey) {
SecItemDelete(baseQuery(for: key) as CFDictionary)
}
/// Force the next `load` to re-read credentials.
static func invalidateCache() {
// Kept for call-site compatibility. Keychain reads do not use an in-memory cache.
}
static var isConfigured: Bool {
load(key: .openAIAPIKey) != nil
}
private static func baseQuery(for key: KeychainKey) -> [String: Any] {
[
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key.rawValue
]
}
}
enum KeychainError: LocalizedError {
case saveFailed(OSStatus)
var errorDescription: String? {
switch self {
case .saveFailed(let status):
return "Zugangsdaten konnten nicht im macOS Keychain gespeichert werden. Status: \(status)"
}
}
}