SyncPadding.kt

2.4 KB · raw

1package com.peachpasswords.sync23/**4 * E7: frame padding at the relay boundary. Port of peach-browser5 * `src/shared/sync-padding.ts` — the two MUST bucket identically.6 *7 * A relay forwards only Noise ciphertext it cannot read, so the one thing it8 * can still learn is traffic SHAPE. Sizes alone distinguish a SYNC_HELLO from a9 * SYNC_ENTRIES carrying a whole vault, and track how a vault grows over time.10 *11 * Wire format of a padded plaintext, BEFORE Noise encryption:12 *13 *     [4-byte big-endian payload length][payload][zero padding to bucket size]14 *15 * The length header is what makes unpadding unambiguous. A bucketing mismatch16 * between platforms fails loudly, not silently: the receiver reads a length17 * that does not fit its frame and rejects it.18 */19object SyncPadding {20    private const val HEADER_BYTES = 42122    /** Below this every frame looks the same — covers all small control frames. */23    private const val MIN_BUCKET = 2562425    /**26     * Above this, buckets grow linearly instead of doubling: doubling a 12 MiB27     * frame would blow past [MAX_SYNC_FRAME_BYTES] for no extra privacy.28     */29    private const val LINEAR_BUCKET_STEP = 64 * 10243031    fun bucket(totalBytes: Int): Int {32        if (totalBytes <= MIN_BUCKET) return MIN_BUCKET33        if (totalBytes <= LINEAR_BUCKET_STEP) {34            var b = MIN_BUCKET35            while (b < totalBytes) b *= 236            return b37        }38        return ((totalBytes + LINEAR_BUCKET_STEP - 1) / LINEAR_BUCKET_STEP) * LINEAR_BUCKET_STEP39    }4041    fun pad(payload: ByteArray): ByteArray {42        val out = ByteArray(bucket(HEADER_BYTES + payload.size))43        val len = payload.size44        out[0] = (len ushr 24).toByte()45        out[1] = (len ushr 16).toByte()46        out[2] = (len ushr 8).toByte()47        out[3] = len.toByte()48        payload.copyInto(out, HEADER_BYTES)49        return out50    }5152    fun unpad(padded: ByteArray): ByteArray {53        require(padded.size >= HEADER_BYTES) {54            "padded sync frame is shorter than its length header"55        }56        val length = ((padded[0].toInt() and 0xFF) shl 24) or57            ((padded[1].toInt() and 0xFF) shl 16) or58            ((padded[2].toInt() and 0xFF) shl 8) or59            (padded[3].toInt() and 0xFF)60        require(length >= 0 && length <= padded.size - HEADER_BYTES) {61            "padded sync frame declares more payload than it carries"62        }63        return padded.copyOfRange(HEADER_BYTES, HEADER_BYTES + length)64    }65}