import Foundation import Security /// Generische, in der App injizierbare Keychain-Wrapper. Eine Instanz /// pro `ManaAppConfig`. public final class KeychainStore: Sendable { /// Bekannte Keys für mana-auth-Tokens. /// /// `.guestId` ist eine lokal generierte anonyme UUID für Apps mit /// Local-First-/Guest-Modus. Sie wird unabhängig von den Session- /// Tokens persistiert und vom Default-``wipe()`` *nicht* gelöscht — /// damit eine App nach einem Logout im Guest-Modus weiterlaufen /// kann, ohne dass die lokalen anonymen Daten ihre Besitzer-Spur /// verlieren. Vollständiger Reset über ``wipeAll()``. public enum Key: String, Sendable { case accessToken = "access_token" case refreshToken = "refresh_token" case email case guestId = "guest_id" } private let service: String private let accessGroup: String? public init(service: String, accessGroup: String? = nil) { self.service = service self.accessGroup = accessGroup } public func setString(_ value: String, for key: Key) throws { guard let data = value.data(using: .utf8) else { throw AuthError.encoding } var query = baseQuery(for: key) let attributes: [CFString: Any] = [ kSecValueData: data, kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock, ] let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary) switch updateStatus { case errSecSuccess: return case errSecItemNotFound: for (attrKey, attrValue) in attributes { query[attrKey] = attrValue } let addStatus = SecItemAdd(query as CFDictionary, nil) guard addStatus == errSecSuccess else { throw AuthError.keychain(addStatus) } default: throw AuthError.keychain(updateStatus) } } public func getString(for key: Key) -> String? { var query = baseQuery(for: key) query[kSecReturnData] = true query[kSecMatchLimit] = kSecMatchLimitOne var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result) guard status == errSecSuccess, let data = result as? Data else { return nil } return String(data: data, encoding: .utf8) } public func remove(for key: Key) { let query = baseQuery(for: key) SecItemDelete(query as CFDictionary) } /// Löscht Session-Daten (accessToken, refreshToken, email). Behält /// die ``Key/guestId``, damit lokale Guest-Daten nach einem Logout /// erhalten bleiben können. public func wipe() { remove(for: .accessToken) remove(for: .refreshToken) remove(for: .email) } /// Löscht *alles* inklusive der Guest-Identität. Genutzt nach /// `deleteAccount()` oder bei explizitem "anonyme Daten vergessen". public func wipeAll() { wipe() remove(for: .guestId) } private func baseQuery(for key: Key) -> [CFString: Any] { var query: [CFString: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrService: service, kSecAttrAccount: key.rawValue, ] if let accessGroup { query[kSecAttrAccessGroup] = accessGroup } return query } }