1/**2 * Plaintext padding for encrypted columns: payloads are padded to bucket3 * lengths before sealing, so the stored ciphertext reveals a size class4 * rather than an exact content length.5 *6 * The bucket schedule is Padmé with a floor: every payload up to the floor7 * pads to the floor (all short scalars share one indistinguishable class),8 * and larger payloads round up to a Padmé bucket, which bounds overhead at9 * roughly 12% while leaking only O(log log n) bits of length.10 *11 * The padded form is self-describing — a 4-byte big-endian payload length,12 * the payload, then zero fill — so unpadding needs no knowledge of the13 * schedule. The schedule can therefore evolve without a format version bump:14 * values padded under an older schedule still open.15 *16 * @module17 */1819/** Byte length of the payload-length prefix inside a padded plaintext. */20export const PADDING_PREFIX_LENGTH = 4;2122/** Every padded plaintext is at least this long. */23export const PADDING_FLOOR = 64;2425/**26 * The bucket length for a raw length `n` (prefix included): the floor for27 * short payloads, a Padmé bucket above it.28 */29export function paddedLength(n: number): number {30 if (n <= PADDING_FLOOR) return PADDING_FLOOR;31 const exponent = Math.floor(Math.log2(n));32 const secondExponent = Math.floor(Math.log2(exponent)) + 1;33 const step = 2 ** (exponent - secondExponent);34 return Math.ceil(n / step) * step;35}3637/**38 * Pads a payload to its bucket: 4-byte big-endian length, payload, zero fill.39 */40export function padPayload(payload: Uint8Array): Uint8Array {41 if (payload.length > 0xffff_ffff) {42 throw new RangeError("payload exceeds the maximum padded size");43 }44 const total = paddedLength(PADDING_PREFIX_LENGTH + payload.length);45 const padded = new Uint8Array(total);46 new DataView(padded.buffer).setUint32(0, payload.length);47 padded.set(payload, PADDING_PREFIX_LENGTH);48 return padded;49}5051/**52 * Recovers the payload from a padded plaintext. Throws `RangeError` when the53 * declared length cannot fit — the caller maps this to its corrupt-value54 * error, though under an AEAD it indicates a codec bug rather than tampering.55 */56export function unpadPayload(padded: Uint8Array): Uint8Array {57 if (padded.length < PADDING_PREFIX_LENGTH) {58 throw new RangeError("padded plaintext is shorter than its length prefix");59 }60 const length = new DataView(padded.buffer, padded.byteOffset).getUint32(0);61 if (PADDING_PREFIX_LENGTH + length > padded.length) {62 throw new RangeError("padded plaintext declares a length past its end");63 }64 return padded.slice(PADDING_PREFIX_LENGTH, PADDING_PREFIX_LENGTH + length);65}