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. public enum Key: String, Sendable { case accessToken = "access_token" case refreshToken = "refresh_token" case email } 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) } public func wipe() { remove(for: .accessToken) remove(for: .refreshToken) remove(for: .email) } 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 } }