import Foundation import Security final class CredentialVault { private let service = "app.luma.chat.xmpp" func save(password: String, for jid: String) throws { let account = jid.lowercased() let passwordData = Data(password.utf8) let baseQuery: [CFString: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrService: service, kSecAttrAccount: account ] let updateStatus = SecItemUpdate( baseQuery as CFDictionary, [kSecValueData: passwordData] as CFDictionary ) if updateStatus == errSecSuccess { return } guard updateStatus == errSecItemNotFound else { throw CredentialVaultError.unhandled(updateStatus) } var insertion = baseQuery insertion[kSecValueData] = passwordData #if !os(macOS) insertion[kSecAttrAccessible] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly #endif let addStatus = SecItemAdd(insertion as CFDictionary, nil) guard addStatus == errSecSuccess else { throw CredentialVaultError.unhandled(addStatus) } } func password(for jid: String) throws -> String? { let query: [CFString: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrService: service, kSecAttrAccount: jid.lowercased(), kSecReturnData: true, kSecMatchLimit: kSecMatchLimitOne ] var item: CFTypeRef? let status = SecItemCopyMatching(query as CFDictionary, &item) if status == errSecItemNotFound { return nil } guard status == errSecSuccess, let data = item as? Data, let value = String(data: data, encoding: .utf8) else { throw CredentialVaultError.unhandled(status) } return value } func deletePassword(for jid: String) throws { let query: [CFString: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrService: service, kSecAttrAccount: jid.lowercased() ] let status = SecItemDelete(query as CFDictionary) guard status == errSecSuccess || status == errSecItemNotFound else { throw CredentialVaultError.unhandled(status) } } } enum CredentialVaultError: LocalizedError { case unhandled(OSStatus) var errorDescription: String? { switch self { case .unhandled(let status): let message = SecCopyErrorMessageString(status, nil) as String? return message ?? "Не удалось обратиться к Keychain (\(status))." } } }