import Foundation import CryptoKit // MARK: - Data ↔ Hex extension Data { /// Convert data to lowercase hex string var hexString: String { map { String(format: "%02x", $0) }.joined() } /// Initialize Data from a hex string init?(hexString: String) { let hex = hexString.lowercased() guard hex.count % 2 == 0 else { return nil } var data = Data(capacity: hex.count / 2) var index = hex.startIndex while index < hex.endIndex { let nextIndex = hex.index(index, offsetBy: 2) guard let byte = UInt8(hex[index.. Data { var data = Data(count: count) data.withUnsafeMutableBytes { ptr in _ = SecRandomCopyBytes(kSecRandomDefault, count, ptr.baseAddress!) } return data } } // MARK: - Data ↔ Base64 (Protocol Wire Format) extension Data { /// Encode to standard base64 string (matching Python's base64.b64encode) func base64EncodedString() -> String { self.base64EncodedString(options: []) } /// Decode from base64 string static func fromBase64(_ string: String) throws -> Data { // Try standard base64 first, then URL-safe if let data = Data(base64Encoded: string, options: .ignoreUnknownCharacters) { return data } throw CryptoError.invalidBase64 } } // MARK: - UInt32 Big-Endian extension UInt32 { var bigEndianData: Data { var value = self.bigEndian return Data(bytes: &value, count: 4) } } // MARK: - CryptoKit Key → Data extension Curve25519.KeyAgreement.PublicKey { nonisolated var rawData: Data { Data(rawRepresentation) } } extension Curve25519.KeyAgreement.PrivateKey { nonisolated var rawData: Data { Data(rawRepresentation) } } extension Curve25519.Signing.PublicKey { nonisolated var rawData: Data { Data(rawRepresentation) } } extension Curve25519.Signing.PrivateKey { nonisolated var rawData: Data { Data(rawRepresentation) } } // MARK: - String helpers extension String { /// Trim whitespace and newlines var trimmed: String { trimmingCharacters(in: .whitespacesAndNewlines) } } // MARK: - Date Parsing (server sends ISO8601 with or without timezone) enum DateParsing { private static let iso8601WithTZ: ISO8601DateFormatter = { let f = ISO8601DateFormatter() f.formatOptions = [.withInternetDateTime, .withFractionalSeconds] return f }() private static let iso8601Basic: ISO8601DateFormatter = { let f = ISO8601DateFormatter() f.formatOptions = [.withInternetDateTime] return f }() private static let noTZ: DateFormatter = { let f = DateFormatter() f.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" f.timeZone = TimeZone(identifier: "UTC") f.locale = Locale(identifier: "en_US_POSIX") return f }() /// Parse ISO8601 date string — handles with/without timezone, with/without fractional seconds static func parse(_ string: String) -> Date? { iso8601WithTZ.date(from: string) ?? iso8601Basic.date(from: string) ?? noTZ.date(from: string) } /// Format Date to ISO8601 string (for after_ts / since_ts parameters) static func format(_ date: Date) -> String { iso8601WithTZ.string(from: date) } } // MARK: - Dictionary merge helper extension Dictionary where Key == String, Value == Any { nonisolated func string(for key: String) -> String? { self[key] as? String } nonisolated func int(for key: String) -> Int? { if let i = self[key] as? Int { return i } if let s = self[key] as? String, let i = Int(s) { return i } return nil } nonisolated func dict(for key: String) -> [String: Any]? { self[key] as? [String: Any] } nonisolated func array(for key: String) -> [[String: Any]]? { self[key] as? [[String: Any]] } nonisolated func data(for key: String) -> Data? { if let hex = self[key] as? String { return Data(hexString: hex) } return nil } nonisolated func bool(for key: String) -> Bool? { if let b = self[key] as? Bool { return b } if let i = self[key] as? Int { return i != 0 } return nil } }