import { a as SFrameKeyResolver, b as KidCodec, c as SFrameKey, P as PeerIndex, I as IdentityKeyPair, d as PeerIdentity, C as CipherSuite, K as KidFormat, M as MlsKidConfig, E as EpochAnnouncement, e as MemberChange } from './ratchet-crypto-Bu7ZATDd.js'; export { f as CHAIN_KEY_BYTES, D as DEFAULT_CIPHER_SUITE, g as EpochKey, F as FIXED_KID_CODEC, h as MlsKidBitRange, i as SFRAME_SALT_BYTES, j as SFrameDecryptEvent, k as SFrameKeyLookup, S as SFrameSupport, X as X25519_KEY_BYTES, l as decodeMlsKid, m as deriveEpochKeyTable, n as deriveSenderKeys, o as deriveWrapKey, p as encodeMlsKid, q as generateX25519Keypair, r as hkdfExtractExpand, s as makeKidCodec, t as randomChainKey, u as suiteParams, v as unwrapChainKey, w as validateMlsBitRange, x as wrapChainKey, y as x25519Dh } from './ratchet-crypto-Bu7ZATDd.js'; export { A as AEADAuthError, F as FipsModeViolationError, H as HeaderParseError, K as KeyInvalidError, a as KeyNotFoundError, Q as QueueFullError, R as RatchetWindowExhaustedError, b as ReplayError, S as SFrameError, c as StaleEpochError } from './errors-BmXaR_x0.js'; export { D as DecryptStarvedInfo, E as EpochParams, F as FrameCryptor, a as FrameCryptorOptions, s as supportsSFrame } from './frame-cryptor-aRTX0FM-.js'; /** * Sliding replay window — tracks a bounded set of recently seen bigint CTR * values. `check()` returns false if the CTR has been seen (replay). * `accept()` records a CTR. `clear()` resets all state. * * NOT thread-safe; synchronous (no async state). * * CTR comparisons use {@link ctBigintEqual} (issue #49: defense-in-depth * constant-time comparison, though CTR values are not secret in the SFrame * threat model). */ declare class SlidingReplayWindow { /** Ordered record of CTR values (insertion order = eviction order). */ private readonly seen; /** FIFO queue for eviction when window is full. */ private readonly queue; private readonly windowSize; constructor(windowSize: number); /** * Check if a CTR value would be accepted. * Returns `true` if the CTR has NOT been seen (accept ok). * Returns `false` if the CTR IS in the recent set (replay detected). * Window size 0 always returns `true`. */ check(ctr: bigint): boolean; /** * Record a CTR as seen. Evicts the oldest entry if the window is full. * Must be called after a successful unseal (after check passed). * Window size 0 is a no-op. */ accept(ctr: bigint): void; /** * Reset all replay state (called on key rotation). */ clear(): void; } interface DurableReplayGuardOptions { /** Namespace isolating independent key-spaces in the shared IDB store. */ namespace: string; /** * Max distinct recent CTRs tracked per (namespace, key). Default 1024 * (matches `SlidingReplayWindow`). `0` disables the durable window * (mirrors `SlidingReplayWindow`'s `replayWindow: 0`); a negative value is * treated as invalid and falls back to the default (it does NOT disable). */ window?: number; /** Suppress the one-time no-IDB / no-WebLocks warning. */ warnIfUnavailable?: boolean; /** * Throw at construction when IndexedDB or Web Locks is unavailable, instead * of silently degrading to in-memory-only protection (issue #43). Default * `false` — the guard logs a warning and continues with `available=false`. * Set to `true` for production deployments where cross-reload replay * protection is a hard requirement. */ requireAvailable?: boolean; } /** * Durable receiver-side replay window. One instance per provider (chat) or * per worker (media); state is scoped per (namespace, key) so no cross-room / * cross-key-space replay-window confusion. * * The caller MUST pass a per-tenant `namespace` to isolate independent * deployments sharing the same origin. Two deployments sharing the same * namespace with a colliding key would share a window and could false-reject * each other. * * ## Check / accept ordering (architecture-council nit #1) * The CALLER is responsible for running the in-memory `SlidingReplayWindow` * check FIRST (cheap, synchronous) and only calling `check()` here if the * in-memory check passes. `accept()` MUST be called only AFTER a successful * AEAD verify, so a forged frame with a novel CTR cannot poison the window. */ declare class DurableReplayGuard { /** True when IndexedDB is present; when false every method is a safe no-op. */ readonly available: boolean; private readonly namespace; private readonly window; private readonly mem; private readonly hydrating; /** * Per-key count of in-flight persists. A key with a queued/running persist * has an accepted CTR that is NOT yet in the durable store, so evicting it * from `mem` and then re-hydrating from IDB would read a STALE window and * false-ACCEPT that very CTR as new. `trimMemCache` therefore never * evicts a key present here. A counter (not a Set) because two overlapping * unseals of one key can schedule two persists concurrently. */ private readonly persisting; /** Serializes persist writes so interleaved snapshots cannot clobber each other. */ private persistTail; private warnedPersistFail; private warnedReadFail; private dbPromise; constructor(opts: DurableReplayGuardOptions); private db; private storeKey; /** Read a persisted window from IDB. */ private idbRead; /** Write a persisted window to IDB. */ private idbWrite; /** Delete a persisted window from IDB. */ private idbDeleteKey; /** Load (once) the persisted window for a key into the in-memory mirror. */ private hydrate; /** * True if this (key, ctr) has NOT been accepted before — i.e. it is safe * to proceed with AEAD verification. A false result means the CTR was * already seen (replay). No-op (returns true) when unavailable or disabled. * * The caller MUST run the in-memory `SlidingReplayWindow.check()` FIRST * and only call this when the in-memory check passes. */ check(key: string, ctr: bigint): Promise; /** * Record an AEAD-authentic CTR as accepted and persist it (write-through). * MUST be called only AFTER a successful unseal, so a forged frame with a * novel CTR cannot poison the window. No-op when unavailable or disabled. */ accept(key: string, ctr: bigint): Promise; /** * Clear the durable replay window for a key (used on key rotation). * Deletes the IDB entry, evicts from the in-memory cache, and clears any * in-flight persist count for the key. No-op when unavailable or disabled, * or when the key was never hydrated. * * Architecture-council nit #2: chat `rotate(roomId)` and media * `wipeEpoch(epoch)` both need a matching durable clear so stale CTRs from * a rotated key do not false-reject fresh frames under the new key. */ clear(key: string): Promise; /** * Decrement the in-flight-persist count for a key, deleting it at zero. * Uses `?? 0` (not `?? 1`) so a `clear()` that deleted the entry mid-flight * does not underflow: `clear` removes the counter, then a stale release * reads 0, computes -1, and deletes (no-op on an already-deleted key) — * the counter never goes negative and never gets stuck. */ private releasePersisting; /** * Keep the in-memory `mem` cache bounded by REPLAY_MEM_CACHE_CAP, evicting * the OLDEST evictable entry (insertion-order FIFO). The durable IDB store * is authoritative, so an evicted entry re-hydrates on next use with no * correctness loss. * * Never evicts: * - `justHydratedKey` — the freshest read that just triggered this call; * - a key still in `hydrating` — its mem entry is mid-load; * - a key in `persisting` — it holds an accepted CTR not yet durably * written; evicting it then re-hydrating from IDB would read a stale * window and false-accept that CTR (replay). * * If every over-cap entry is protected the loop stops (temporarily over * cap); it self-heals once the in-flight persists settle and `persisting` * drains. */ private trimMemCache; /** Drop oldest CTRs until the in-memory window is within bound. */ private trim; /** * Read-merge-write the persisted window under a cross-tab exclusive lock * (when available): union the persisted CTRs (possibly from another tab) * with this tab's in-memory window, dedup, bound, persist, and reflect the * union back into the in-memory mirror so this tab immediately rejects a * CTR another tab already accepted. */ private persistMerged; private warnPersistFail; } /** * Parsed SFrame header. `bodyOffset` is the number of bytes consumed from the * input buffer — the ciphertext (including 16-byte GCM tag) starts there. */ interface SFrameHeader { kid: number; ctr: bigint; bodyOffset: number; } declare function serializeHeader(kid: number, ctr: bigint): Uint8Array; /** Parse an RFC 9605 §4.3 header. Pure. Throws HeaderParseError on malformed input. */ declare function parseHeader(buf: Uint8Array): SFrameHeader; /** * AAD construction form. See the file-level comment for the rationale and the * staged-rollout plan. * * - `'header'` — AAD = header (original format; phase-1 sender). * - `'prefix'` — AAD = prefix || header (intermediate b6ded9d form). * - `'canonical'` — AAD = be16(prefix.length) || prefix || header (phase-2 target). */ type AadForm = 'header' | 'prefix' | 'canonical'; /** * Encrypt `plaintext` under `key` at counter `ctr`. * Output layout: `[header][AES-GCM ciphertext + 16B tag]`. * AAD is the serialised header (RFC 9605 §4.4.2; spec §6.3), optionally * prepended with `aadPrefix` (the unencrypted codec prefix) for tamper-evidence. * `aadForm` controls the AAD construction (default `'prefix'`). */ declare function sframeEncrypt(plaintext: Uint8Array, key: SFrameKey, ctr: bigint, aadPrefix?: Uint8Array, aadForm?: AadForm): Promise; /** * Decrypt a full SFrame buffer. * * `resolveKey` is a context-aware callback: it receives `{ kid, epoch, * peerIndex, ctr }` so the caller can enforce the stale-epoch gate (spec §7.4) * BEFORE any decrypt attempt. Return `null` to reject the frame with * "key not found" (caller may log + drop); throwing inside the resolver also * rejects the frame and propagates its message. `meta.ctr_hint` is accepted * for API parity with out-of-band CTR recovery schemes but unused in v1. */ declare function sframeDecrypt(sframe: Uint8Array, resolveKey: SFrameKeyResolver, _meta?: { ctr_hint?: bigint; kidCodec?: KidCodec; aadPrefix?: Uint8Array; aadForm?: AadForm; }): Promise; /** Codecs for which the partial-encryption prefix table is defined. */ type Codec = 'vp8' | 'vp9' | 'h264' | 'av1' | 'opus'; /** * Frame kind for VP8: 'key' = keyframe, 'inter' = interframe. * Only meaningful for VP8; other codecs ignore this field. * Derived from RTCEncodedVideoFrame.type in the stream transform. */ type FrameKind = 'key' | 'inter'; interface SetSifTrailerMsg { type: 'set-sif-trailer'; /** `null` disables the trailer; any `Uint8Array` enables it with that byte sequence. */ trailer: Uint8Array | null; } interface SetReplayWindowMsg { type: 'set-replay-window'; /** * Size of the per-(epoch, peerIndex) anti-replay sliding window (RFC 9605 * §9.3). 0 disables protection entirely (debug/tests only). Default 64. * Changing the size clears all existing windows (they were created with the * old size). */ size: number; } interface SetFailureToleranceMsg { type: 'set-failure-tolerance'; /** * Number of consecutive AEAD failures (per epoch+peerIndex) after which the * key is marked invalid and subsequent frames are dropped WITHOUT attempting * AEAD (issue #14, pattern from livekit ParticipantKeyHandler.ts:58). * -1 = unlimited (default; preserves current behavior). 0 = invalidate on * the first failure. Changing the value clears all existing failure counts. */ tolerance: number; } /** * Structured telemetry event posted from the worker to the main thread when * metrics are enabled. Consumers register via `onMetrics(worker, handler)`. */ type MetricsEvent = { kind: 'encrypt'; epoch: number; peerIndex: number; bytes: number; codec?: Codec; } | { kind: 'decrypt'; epoch: number; peerIndex: number; bytes: number; } | { kind: 'decrypt_fail'; code: string; epoch?: number; peerIndex?: number; } | { kind: 'ratchet_retry'; epoch: number; peerIndex: number; steps: number; succeeded: boolean; } | { kind: 'queue_drop'; reason: 'pre_epoch_full' | 'stale_epoch' | 'replay'; epoch?: number; } | { kind: 'replay_drop'; epoch: number; peerIndex: number; ctr: string; } | { kind: 'key_invalidated'; epoch: number; peerIndex: number; failures: number; } | { kind: 'epoch_advance'; from: number; to: number; } | { kind: 'encode_drop'; code: string; epoch?: number; peerIndex?: number; }; /** * Options for strict-FIPS enforcement. All flags default to `true` when * {@link enableStrictFips} is called without arguments. */ interface StrictFipsOptions { /** * Require AES-256-GCM-SHA512 (RFC 9605 suite 5). * AES-128-GCM-SHA256 (suite 4) construction throws {@link FipsModeViolationError}. * Default: `true`. */ requireSuite5?: boolean; /** * {@link SimpleKex} constructor throws {@link FipsModeViolationError}. * Production deployments must plug in their own KEX (MLS, X3DH, etc.). * Default: `true`. */ forbidSimpleKex?: boolean; /** * Document / enforce that WebCrypto `importKey` must use `extractable: false`. * The library already passes `extractable: false` everywhere via the * `importAesKey` internal helper — this flag confirms the policy is active. * Default: `true`. */ requireNonExtractable?: boolean; } /** * Enable strict-FIPS mode. All flags default to `true`. * * Calling this multiple times replaces the previous configuration. * * @example * ```ts * import { enableStrictFips } from 'sframe-ratchet'; * enableStrictFips(); // throws FipsModeViolationError on suite 4 or SimpleKex * ``` */ declare function enableStrictFips(opts?: StrictFipsOptions): void; /** Disable strict-FIPS mode. The library reverts to its default permissive behaviour. */ declare function disableStrictFips(): void; /** * Return the active strict-FIPS configuration, or `null` when disabled. * Returned object is read-only; mutating it has no effect. */ declare function getStrictFips(): Readonly> | null; /** A single emoji entry in the 64-emoji SAS table. */ interface EmojiEntry { emoji: string; name: string; } /** The full SAS representation shown to users for out-of-band comparison. */ interface SasData { /** 3 groups of 5 decimal digits (0–99999), Matrix format. */ decimal: number[]; /** 7 emoji from the 64-emoji table. */ emoji: EmojiEntry[]; } /** Number of decimal groups displayed to the user. */ declare const SAS_DECIMAL_GROUP_COUNT = 3; /** Number of digits per decimal group (zero-padded). */ declare const SAS_DECIMAL_DIGITS_PER_GROUP = 5; declare const SAS_EMOJI_TABLE: readonly EmojiEntry[]; /** * Derive 6 SAS bytes from the DH shared secret via HKDF-SHA-256. * * Delegates to the shared `hkdfExtractExpand` helper from ratchet-crypto.ts * (same HKDF used for wrap keys, sender keys, ratchet-step keys) with: * - hash: SHA-256 * - ikm: dhSecret (the SAME X25519 shared secret used to wrap the ChainKey) * - salt: empty (consistent with the rest of the ratchet) * - info: "sframe-ratchet-sas-v1" * - output: 6 bytes (48 bits) * * SECURITY: The `dhSecret` MUST be the same DH secret used in the ECIES * ChainKey wrap/unwrap. This is enforced by the caller (RoomRatchet) which * passes the `shared` variable that is used for BOTH deriveWrapKey and * computeSas. */ declare function deriveSasBytes(dhSecret: Uint8Array): Promise; /** * Generate 3 groups of 5 decimal digits from SAS bytes (Matrix format). * * Algorithm: for each of 3 groups, take 2 bytes as big-endian uint16, * mod 100000, yielding a 5-digit number (0–99999, zero-padded for display). * Uses all 6 SAS bytes (3 groups × 2 bytes). * * @returns Array of 3 numbers, each in [0, 99999]. */ declare function generateDecimalSas(sasBytes: Uint8Array): number[]; /** * Generate 7 emoji indices from 6 SAS bytes via bit-slicing. * * 6 bytes = 48 bits. We extract 7 groups of 6 bits (42 bits used, 6 unused) * from the most-significant end, big-endian. Each 6-bit value (0–63) indexes * into the 64-emoji table. * * This is the Matrix/Olm bit-slicing algorithm: it packs 7 emoji indices * into 6 bytes without wasting a full byte per index. * * @returns Array of 7 {@link EmojiEntry} from the 64-emoji table. */ declare function generateEmojiSas(sasBytes: Uint8Array): EmojiEntry[]; /** * Compute the full SAS (decimal + emoji) from a DH shared secret. * * This is the top-level entry point. It derives 6 bytes from `dhSecret` via * HKDF-SHA-256, then generates both the decimal and emoji representations. * * SECURITY: `dhSecret` MUST be the same DH secret used to wrap/unwrap the * ChainKey. The caller (RoomRatchet) enforces this by passing the `shared` * variable that feeds both `deriveWrapKey` and this function. */ declare function computeSas(dhSecret: Uint8Array): Promise; /** * Build a 32-bit KID per spec §6.1: * KID = (epoch_version << 16) | (peer_index & 0xFFFF) * * High 16 bits: monotonic epoch counter per room. * Low 16 bits : per-sender index within the epoch. * * Both fields are 16-bit unsigned. The `>>> 0` cast forces JS's 32-bit * unsigned semantics (otherwise `|` would yield a signed int32). */ declare function makeKid(epoch: number, peerIndex: PeerIndex): number; /** Inverse of makeKid — extract (epoch, peer_index) from a 32-bit KID. */ declare function splitKid(kid: number): { epoch: number; peerIndex: PeerIndex; }; type PeerIndexMapValidation = { valid: true; } | { valid: false; reason: 'empty' | 'duplicate_index' | 'gap_or_out_of_range' | 'bad_value'; }; /** * Enforce the spec §7.8 invariant on `peer_index_map`: * - Non-empty. * - Integer indices in `[0, N)` where `N = Object.keys(map).length`. * - All values distinct. * - Value set equals exactly `{0, 1, …, N-1}` (no gaps, no duplicates, * no out-of-range). * * Records cannot have duplicate keys in JS, so key-distinctness is implicit. * * Fail-closed: the caller MUST reject the `epoch_new` on any `valid: false` * result and degrade to transit-only per spec §7.8 / §7.2. */ declare function validatePeerIndexMap(map: Record): PeerIndexMapValidation; /** Create a fresh IdentityKeyPair for the current session (ephemeral per call). */ declare function newIdentity(peerId: string): IdentityKeyPair; /** * Compute the canonical peer_index assignment for an epoch: sort peer_ids * lexicographically and enumerate 0..N−1. Matches spec §4.3 step 6 and §4.4. */ declare function buildPeerIndexMap(peerIds: string[]): Record; interface RoomRatchetOptions { identity: IdentityKeyPair; /** Known members at construction time (excluding self). */ initialPeers?: PeerIdentity[]; /** * RFC 9605 §4.5 cipher suite. Defaults to `AES_128_GCM_SHA256` (suite 4). * All members of a room MUST use the same suite. */ suite?: CipherSuite; /** * KID encoding format (RFC 9605 §5.2 / §6.1). Defaults to `'fixed'` * (the historical 32-bit `(epoch << 16) | peerIndex` split). When set to * `'mls'`, the KID is encoded/decoded per the §5.2 MLS Key ID layout * using `mlsConfig`. * * SECURITY: all parties in a room MUST agree on kidFormat + bit widths * (signaling concern — same as suite agreement). No auto-negotiation. */ kidFormat?: KidFormat; /** * MLS Key ID configuration (RFC 9605 §5.2). Required when `kidFormat` is * `'mls'`; ignored when `kidFormat` is `'fixed'` or unset. */ mlsConfig?: MlsKidConfig; } /** * The per-room ratchet. Single instance per room per client. * * Caller coordinates signaling: after `startNewEpoch()` the caller sends each * returned `EpochAnnouncement` on DC id:1, and on receipt of a peer's * announcement calls `consumeEpochAnnouncement()`. The ratchet does not do * any I/O itself. */ declare class RoomRatchet { private readonly identity; private readonly suite; private readonly _kidCodec; private peers; private epochs; private currentEpoch; private sasState; private sasReadyCallbacks; constructor(opts: RoomRatchetOptions); /** True if this node's peer_id is lex-smallest across all epoch members (§4.3 step 6). */ private isAuthoritativeAuthor; /** Mint or re-wrap ChainKey_e, build peer_index_map, emit one announcement per recipient (§4.3). */ startNewEpoch(members: PeerIdentity[], options?: { version?: number; viaChainKey?: Uint8Array; }): Promise; /** Consume inbound EpochAnnouncement; throws on decrypt fail or bad map (caller emits epoch_error, §4.2/§7.8). */ consumeEpochAnnouncement(msg: EpochAnnouncement): Promise; private installEpoch; /** This node's per-sender SFrame key for the current epoch (idempotent; no per-frame ratchet in v1). */ advanceSending(): SFrameKey; /** * Look up a receiving key by (epoch, peerIndex) extracted from a KID. * Returns null if the epoch is unknown (wiped after the 2 s grace) or the * peer_index is outside the epoch's map. */ getReceivingKey(epoch: number, peerIndex: PeerIndex): SFrameKey | null; /** * Handle a membership change by bumping to a new epoch. See spec §§ 4.3, 4.4. * Returns announcements to broadcast (may be empty if this node is not the * authoritative author). */ rotateOnMemberChange(delta: MemberChange): Promise; /** Drop an epoch's key material (spec §7.4 2 s grace expiry). * Zeroizes the ChainKey before dropping the reference so the raw bytes * don't linger in the JS heap until GC (repo-review-council #31). */ forgetEpoch(epoch: number): void; /** Current epoch number, for diagnostics / signaling. */ get epoch(): number; /** The KID codec in use (fixed or mls). Read-only accessor for diagnostics. */ get kidCodec(): KidCodec; /** Peer-index map for the current epoch. Returns empty object before first epoch. * Used by debug diagnostics (installGroupCallDebugGetters) and by kxConsumerEpochs * in the diag-503 debug getters — acceptable production use, not in a hot loop. */ get currentPeerIndexMap(): Record; /** * Peer-index map for a SPECIFIC installed epoch. Returns null if the epoch is * unknown (never installed, or already wiped after the grace window). Returns a * defensive copy so callers cannot mutate internal state. Public accessor so * consumers stop narrow-casting into the private `epochs` map. */ getEpochPeerIndexMap(epoch: number): Record | null; /** Self's peer_index in the current epoch (undefined before first epoch). */ get selfPeerIndex(): PeerIndex | undefined; /** Read-only identity (used by M3.3 `KeyExchange` to advertise kpub). */ getIdentity(): Readonly; /** ChainKey bytes for an installed epoch; null if unknown/wiped. Sensitive — do not log. */ getEpochChainKey(epoch: number): Uint8Array | null; /** Re-wrap current ChainKey for `peer` (spec §7.2 epoch_request retry). Null if not author. */ rewrapCurrentEpochFor(peer: PeerIdentity): Promise; /** * Internal: compute SAS from a DH secret and store it for `peerId`. * * SECURITY INVARIANT: `dhSecret` MUST be the same X25519 shared secret * that was used to derive the wrap key for this peer in this epoch. The * callers (startNewEpoch, consumeEpochAnnouncement, rewrapCurrentEpochFor) * enforce this by passing the `shared` / `dhSecret` variable that fed * `deriveWrapKey` — the very same bytes. */ private installSas; /** * Wipe SAS state for all peers whose SAS belongs to an epoch older than * `newEpoch`. Called on epoch rotation (issue #11: SAS is per-peer, * per-epoch; rotation wipes it). */ private wipeSasForRotation; /** * Returns the SAS (decimal + emoji) for an established peer session, or * `null` if no session has been established for `peerId` in the current * epoch. * * The SAS is displayed locally and compared out-of-band. It is NEVER sent * over the signaling channel. */ getSas(peerId: string): SasData | null; /** * Record the user's out-of-band SAS verification result for `peerId`. * Call with `true` after the user confirms the SAS matches, `false` to * revoke (e.g. mismatch detected or session re-keyed). */ markSasVerified(peerId: string, verified: boolean): void; /** * Returns the SAS verification state for `peerId`. `false` if no session * is established or the user has not yet verified. */ isSasVerified(peerId: string): boolean; /** * Subscribe to a callback that fires when a peer session is established * and SAS is available for out-of-band comparison. * * @returns An unsubscribe function. Call it to remove the listener. */ onSasReady(callback: (peerId: string) => void): () => void; } /** * Returns the number of plaintext bytes at the front of an encoded frame * that must remain unencrypted. N=0 means full encryption (default path). * * The exposed prefix is the MINIMUM byte count that lets a depacketizer * identify a keyframe — no more, no less. One byte short is a silent no-op * (the SFU cannot see the keyframe); one byte long is a gratuitous * plaintext leak. * * Per-codec justification: * * - VP8 (RFC 6386 §9.1): The first 3 bytes are the frame tag * `frame_tag = key_frame(1b) | version(3b) | show_frame(1b) | first_part_size(19b)`. * Byte 0 bit 7 is the keyframe indicator. For keyframes, bytes 3-9 carry * the start code (0x9d 0x01 0x2a) and width/height — leaving these in the * clear lets the SFU extract resolution for SVC routing and lets decoders * fail gracefully on key mismatch (garbage instead of fatal parse error). * N=10 (key) / 3 (inter). * * - H.264 (RFC 7798): Byte 0 is the FUA/NALU header. The NAL type (bits 4-0 * for single-NAL, or the inner byte for FUA) identifies an IDR slice * (type 5 = keyframe). N=1. * * - VP9 (draft-ietf-payload-vp9-16 / VP9 bitstream spec §9.1): Byte 0 is the * uncompressed frame header start: * `frame_marker(2b) | profile_low(1b) | profile_high(1b) | show_existing(1b) | frame_type(1b) | ...` * - frame_marker must be 0b10 (bits 7-6, MSB-first) * - frame_type: 0 = KEY_FRAME, 1 = NON_KEY_FRAME. The bit position depends * on profile: bit 2 for profile 0 (profile_low=0, profile_high=0, * show_existing=0), bit 1 for profile 2/3 (profile_low=0, profile_high=1). * Profile 1 (profile_low=1) uses bit 2 as well because the profile_high * bit is 0. * - show_existing_frame = 1 references a previously decoded frame, not a * real keyframe * An observer of byte 0 learns: the frame marker (confirms VP9), the * profile, whether it's a show_existing_frame, and the frame type * (keyframe vs inter). * * PHASE 1: N=0 (full encryption). The SFU cannot see VP9 keyframes in * phase 1 — this is the pre-fix behaviour. Phase 2 will set N=1 so byte 0 * (frame marker + frame type) is visible to the SFU. * * - AV1 (AV1 RTP spec v1.0.0 / AV1 spec §6.2): Byte 0 is the first OBU * (Open Bitstream Unit) header: * `obu_forbidden(1b) | obu_type(4b) | obu_extension_flag(1b) | obu_has_size(1b) | obu_reserved(1b)` * A keyframe (new coded video sequence) starts with a SequenceHeader OBU * (obu_type = 1). The RTP aggregation header's N bit is set when the * first OBU is a SequenceHeader. An observer of byte 0 learns: the OBU * type (SequenceHeader = keyframe vs Frame/FrameHeader = inter), whether * there's an extension byte, and whether there's a size field. * * PHASE 1: N=0 (full encryption). The SFU cannot see AV1 keyframes in * phase 1 — this is the pre-fix behaviour. Phase 2 will set N=1 so byte 0 * (OBU header with obu_type) is visible to the SFU. * * - Opus (RFC 6716 §3.1): Byte 0 is the TOC (Table of Contents) byte * `config(5b) | s(1b) | c(2b)`. Leaving it unencrypted lets the SFU * route by Opus mode. N=1. * * @param codec Per-track codec, set via StreamsMsg. Undefined → full encrypt. * @param frameKind 'key' or 'inter'; only relevant for VP8. */ declare function getUnencryptedBytes(codec: Codec | undefined, frameKind: FrameKind | undefined): number; /** * SIF (Secure Interoperable Frame) trailer support. * * The SIF trailer is a fixed byte sequence appended to every SFrame-encrypted * frame. A receiver with the same trailer configured can distinguish E2EE * frames from plain frames before attempting AEAD — enabling mixed-room * deployments where some participants run E2EE and some do not. * * SECURITY NOTE: The trailer is NOT a security boundary. It is a routing hint. * Any adversary can append the trailer bytes to a plain frame and cause the * receiver to attempt AEAD (which will fail — AEAD failure, frame drop, no * confidentiality breach). False positives due to plaintext accidentally ending * in the trailer pattern are possible; see docs/SECURITY.md for the trade-off. */ /** * Default 9-byte SIF trailer. Chosen to match LiveKit's `SifTrailerMessage` * length for cross-implementation interoperability. These bytes are fixed — * they are NOT random per call; callers who need isolation should supply a * custom trailer via `set-sif-trailer`. * * Do NOT mutate this constant. Use `getDefaultSifTrailer()` for a safe copy. */ declare const DEFAULT_SIF_TRAILER: Readonly; /** * Returns a fresh copy of `DEFAULT_SIF_TRAILER` safe to pass to * `set-sif-trailer` without risk of the caller mutating the library constant. */ declare function getDefaultSifTrailer(): Uint8Array; /** * Subscribe to telemetry events posted by the sframe worker. * * The worker must have `set-metrics-enabled` sent with `enabled: true` before * events are emitted. This helper adds a `message` listener on `worker` and * filters for `data.type === 'metrics'`. It wraps the user handler in a * try/catch so a buggy handler cannot suppress subsequent events. * * @returns An unsubscribe function. Call it to remove the listener. * * @example * ```ts * worker.postMessage({ type: 'set-metrics-enabled', enabled: true }); * const off = onMetrics(worker, (ev) => { * if (ev.kind === 'encrypt') encryptCounter++; * }); * // later: * off(); * ``` */ declare function onMetrics(worker: { addEventListener(type: 'message', listener: (ev: MessageEvent) => void): void; removeEventListener(type: 'message', listener: (ev: MessageEvent) => void): void; }, handler: (ev: MetricsEvent) => void): () => void; export { CipherSuite, type Codec, DEFAULT_SIF_TRAILER, DurableReplayGuard, type DurableReplayGuardOptions, type EmojiEntry, EpochAnnouncement, type FrameKind, IdentityKeyPair, KidCodec, KidFormat, SlidingReplayWindow as MediaReplayWindow, MemberChange, type MetricsEvent, MlsKidConfig, PeerIdentity, PeerIndex, type PeerIndexMapValidation, RoomRatchet, type RoomRatchetOptions, SAS_DECIMAL_DIGITS_PER_GROUP, SAS_DECIMAL_GROUP_COUNT, SAS_EMOJI_TABLE, type SFrameHeader, SFrameKey, SFrameKeyResolver, type SasData, type SetFailureToleranceMsg, type SetReplayWindowMsg, type SetSifTrailerMsg, type StrictFipsOptions, buildPeerIndexMap, computeSas, deriveSasBytes, disableStrictFips, enableStrictFips, generateDecimalSas, generateEmojiSas, getDefaultSifTrailer, getStrictFips, getUnencryptedBytes, makeKid, newIdentity, onMetrics, parseHeader, serializeHeader, sframeDecrypt, sframeEncrypt, splitKid, validatePeerIndexMap };