92 lines
2.6 KiB
Swift
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)"
|
|
}
|
|
}
|
|
}
|