import Foundation /// Message padding for metadata privacy — hides plaintext length. /// Matches Python: crypto_utils.py pad_plaintext / unpad_plaintext enum MessagePadding { /// Magic byte prefix to distinguish padded from legacy unpadded messages. private static let padMagic: UInt8 = 0x01 /// Bucket sizes for length hiding (64B to 64KB). private static let padBuckets = [64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536] /// Pad plaintext to nearest bucket size to hide message length. /// /// Format: `0x01 + plaintext + random_padding + pad_length(4B big-endian)` /// Prefix 0x01 distinguishes padded messages from legacy unpadded (which start with '{'). static func pad(_ plaintext: Data) -> Data { var content = Data([padMagic]) content.append(plaintext) // +4 for the length suffix let minSize = content.count + 4 let target = padBuckets.first(where: { $0 >= minSize }) ?? minSize let padLen = target - content.count // random_padding (padLen - 4 bytes) + pad_length (4 bytes big-endian) var result = content result.append(Data.randomBytes(padLen - 4)) result.append(UInt32(padLen).bigEndianData) return result } /// Remove padding. Returns raw plaintext for both padded and legacy unpadded messages. static func unpad(_ data: Data) -> Data { guard !data.isEmpty else { return data } // Legacy unpadded message (starts with '{' for JSON) guard data[data.startIndex] == padMagic else { return data } // Too short to be validly padded (magic + at least 4 bytes for length) guard data.count >= 5 else { return data } // Read pad_length from last 4 bytes (big-endian UInt32) let padLenOffset = data.count - 4 let padLen = data.withUnsafeBytes { ptr -> UInt32 in var value: UInt32 = 0 withUnsafeMutableBytes(of: &value) { dest in dest.copyBytes(from: UnsafeRawBufferPointer(rebasing: ptr[padLenOffset...])) } return UInt32(bigEndian: value) } // Validate padding metadata guard padLen >= 4, padLen <= data.count - 1 else { return data } // Strip: skip magic byte (index 0), take up to (data.count - padLen) return data[data.startIndex + 1 ..< data.startIndex + data.count - Int(padLen)] } }