fix(relay): close F6 replay K_content nonce reuse via per-generation epoch-in-key
The recoverable replay key was stable per (hostContentSecret, sessionId) while the agent-side sealer resets its deterministic-nonce seq to 0 on every restart/re-attach → two sealer generations sealed distinct plaintext under the SAME (key, nonce). Add a required 'epoch' to ReplayKeyParams, fold it into the K_content HKDF salt (sessionId U+001F epoch), mint a fresh epoch per createReplaySealer generation and expose it, and thread it through ReplaySource so the browser re-derives the matching key. Fresh epoch per generation ⇒ fresh key ⇒ seq=0 can never collide; recoverability within a generation is preserved. Touches relay-contracts/relay-e2e/agent/relay-web. Green: contracts 81, e2e 78, agent 133, web 99; tsc clean. Regression proves same seq-0 nonce, different key.
This commit is contained in:
@@ -117,10 +117,23 @@ export interface AuthorizedDeviceContext {
|
||||
readonly hostContentSecret: Uint8Array // §4.5 host-scoped secret; NEVER logged/sent to relay
|
||||
}
|
||||
|
||||
/** FIX 3 replay content-key derivation params (§4.4). */
|
||||
/**
|
||||
* FIX 3 replay content-key derivation params (§4.4).
|
||||
*
|
||||
* FIX 3b / F6 (anti-nonce-reuse): K_content = HKDF(hostContentSecret, salt=sessionId, info=const)
|
||||
* was byte-identical for a given (host, sessionId), yet the agent-side sealer resets its
|
||||
* deterministic-nonce seq to 0 on every reconstruction (restart / re-attach). Two sealer
|
||||
* GENERATIONS would therefore seal distinct plaintext under the SAME (key, nonce) — a
|
||||
* catastrophic AEAD reuse. The per-generation `epoch` is folded into K_content so a restart /
|
||||
* new generation yields a FRESH key even for the same (hostContentSecret, sessionId); seq=0 can
|
||||
* never collide across generations. Recoverability WITHIN one generation (same epoch ⇒ same key)
|
||||
* is preserved.
|
||||
*/
|
||||
export interface ReplayKeyParams {
|
||||
readonly hostContentSecret: Uint8Array
|
||||
readonly sessionId: string
|
||||
readonly alg: AeadAlg
|
||||
readonly epoch: string // FIX 3b / F6: fresh per-sealer-generation diversifier folded into K_content;
|
||||
// NON-SECRET, carried with the ring buffer so the browser re-derives the key.
|
||||
}
|
||||
export const REPLAY_KDF_INFO = 'relay-e2e/replay/v1' as const
|
||||
|
||||
Reference in New Issue
Block a user