import Foundation import Security struct ProxyCredentials: Codable { let url: String let email: String let password: String } struct UniFiCredentials: Codable { let url: String let apiKey: String let allowUntrusted: Bool } enum CredentialStore { static let proxyAccount = "nginx-proxy-manager" static let unifiAccount = "unifi-network" // Use the macOS login keychain so local, ad hoc signed builds also work // without a development team's application-identifier entitlement. private static func query(account: String) -> [String: Any] { [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: "io.github.nihaiden.ProxyStudio.credentials", kSecAttrAccount as String: account, kSecAttrSynchronizable as String: false ] } static func save(_ credentials: T, account: String) throws { let data = try JSONEncoder().encode(credentials) let query = query(account: account) let attributes = [kSecValueData as String: data] let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary) if status == errSecItemNotFound { var item = query item[kSecValueData as String] = data item[kSecAttrLabel as String] = "Proxy Studio — " + account try check(SecItemAdd(item as CFDictionary, nil)) } else { try check(status) } } static func load(_ type: T.Type, account: String) throws -> T? { var query = query(account: account) query[kSecReturnData as String] = true query[kSecMatchLimit as String] = kSecMatchLimitOne var result: CFTypeRef? let status = SecItemCopyMatching(query as CFDictionary, &result) if status == errSecItemNotFound { return nil } try check(status) guard let data = result as? Data else { throw ServiceError.message("The saved credentials could not be read. Please connect again.") } do { return try JSONDecoder().decode(type, from: data) } catch { throw ServiceError.message("The saved credentials could not be decoded. Please connect again.") } } static func delete(account: String) throws { let status = SecItemDelete(query(account: account) as CFDictionary) if status != errSecItemNotFound { try check(status) } } private static func check(_ status: OSStatus) throws { guard status == errSecSuccess else { let message = SecCopyErrorMessageString(status, nil) as String? ?? "Error \(status)" throw ServiceError.message("Keychain: \(message)") } } }