60 lines
2.4 KiB
Swift
60 lines
2.4 KiB
Swift
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)]
|
|
}
|
|
}
|