SyncLimits.kt

2.8 KB · raw

1package com.peachpasswords.sync23import java.io.IOException45/**6 * Cloudflare Workers caps one WebSocket message at 32 MiB. Keep a generous7 * margin for the Noise authentication tag and platform framing overhead.8 */9const val MAX_SYNC_FRAME_BYTES: Int = 24 * 1024 * 10241011class SyncFrameTooLargeException(12    message: String = "sync frame too large (WebSocket close 1009)",13) : IOException(message)1415/**16 * A decoded frame whose declared structure exceeds the protocol's limits. An17 * IOException so it lands in the same ERRORED outcome as a transport failure.18 */19class SyncStructureTooLargeException(what: String) : IOException("sync structure exceeds limits: $what")2021fun requireSyncFrameSize(size: Int) {22    if (size > MAX_SYNC_FRAME_BYTES) {23        throw SyncFrameTooLargeException("sync frame exceeds $MAX_SYNC_FRAME_BYTES bytes")24    }25}2627/**28 * Timestamp bounds, ported from peach-browser `src/shared/sync-limits.ts`.29 * Both sides must agree, so change them together.30 */31object SyncLimits {32    const val MAX_ENTRY_COUNT = 100_00033    const val MAX_REQUEST_ENTRY_COUNT = 100_00034    const val MAX_TOMBSTONE_COUNT = 100_00035    /**36     * Entry and device identifiers are UUID strings (36 chars). The cap exists37     * so a peer cannot pad a map with multi-kilobyte keys — the count limits38     * alone leave the cost of any single key unbounded.39     *40     * Deliberately loose: every import path on both platforms preserves the id41     * a restored backup already carries (the browser's `sanitizeRestoredEntry`,42     * Android's `draft.id.ifBlank { UUID }`), and neither bounds it, so a cap43     * set tight to a UUID would turn one such restored entry into a permanent44     * sync failure. The real protection is the count cap plus the frame byte45     * cap, which together bound total bytes however long a single key is.46     */47    const val MAX_ID_LENGTH = 51248    const val MAX_ENTRY_PLAINTEXT_BYTES = 512 * 102449    const val MAX_ENTRY_ENVELOPE_BYTES = (MAX_ENTRY_PLAINTEXT_BYTES * 4) / 3 + 6450    /**51     * HV-1: maximum tolerated forward clock skew on any timestamp received from52     * a sync peer. A peer whose clock is slightly ahead still merges cleanly,53     * but a deliberately far-future `modifiedAt`/`deletedAt` can no longer54     * permanently pin a malicious value against honest local edits.55     */56    const val MAX_CLOCK_SKEW_MS = 5 * 60 * 1000L5758    /**59     * Clamp a peer-supplied timestamp to `now + MAX_CLOCK_SKEW_MS`. Applied to60     * every remote `modifiedAt` and `deletedAt` BEFORE it is compared against61     * any local timestamp.62     *63     * See the HOLE 2 coverage audit in the browser's `sync-limits.ts` for the64     * full list of peer-supplied timestamps and what bounds each one.65     */66    fun clampSyncTimestamp(ts: Long, now: Long = System.currentTimeMillis()): Long =67        minOf(ts, now + MAX_CLOCK_SKEW_MS)68}