Server-blind social recovery: HMAC proofs and completion modes
Threat model (concise)
- Server compromise / insider: must not learn master recovery secret S from network traffic or long-lived database fields. Option B never sends S to the server after initiation.
- Downgrade: an attacker must not force a
client_proofsession back to plaintext shard upload once the request is committed to proof-based completion.
Proof protocol
At initiation, the server transiently holds S only long enough to split into encrypted shares and compute a verifier:
proof_verifier = base64url( HMAC-SHA256(key=S, message=UTF8(request_id)) )
Persist proof_verifier (and the usual recovery metadata). Discard S from memory after split + verifier persistence. The legacy SHA-256(S) hash may remain for Option A compatibility—do not reuse it as the proof verifier; mixed migrations are a footgun if you compare the wrong bytes.
Completion (Option B): the recovering client reconstructs S locally, then sends:
proof_of_reconstruction = base64url( HMAC-SHA256(key=S, message=UTF8(request_id)) )
The server compares decoded bytes to the stored verifier with constant-time equality. Timing leaks and oracle behavior around partial proofs are in scope for review—treat comparison as security-sensitive.
Completion modes
| Mode | Behavior |
|---|---|
server_assembly |
Legacy / fallback: guardians may submit plaintext shards; server may reconstruct S to complete recovery for clients that cannot run native proof flows. |
client_proof |
Proof-only completion path. Reject plaintext shard submissions that would reintroduce server-side S with 409 WRONG_COMPLETION_MODE (or equivalent structured conflict). |
The downgrade guard is not optional: if client_proof is chosen, an attacker with network access must not force the weaker path mid-session.
Test checklist (minimum)
- Valid proof → success path; session/credential rotation matches existing complete semantics.
- Wrong
request_idin HMAC input → structured client error (no secret leakage). - Tampered proof → failure without revealing which bit failed beyond “invalid.”
- Lock period not elapsed →
423-class or domain-appropriate locked response. - Threshold not met → 409 / conflict with explicit code
THRESHOLD_NOT_MET(or equivalent). - Downgrade: POST plaintext shard to
client_proofrequest →409WRONG_COMPLETION_MODE.
Implementation pointers
apps/api/src/lib/guardian-recovery.ts— canonical behavior and deviation documentation until native proof path is default in production.- Route:
POST .../complete-with-proof— body carriesproof_of_reconstructionand device attestation material per your WebAuthn integration.
Why this essay stays normative
This post is a sketch aligned to the guardian recovery plan, not a substitute for reading the handler. When code and blog diverge, code wins—open an issue for docs.
Companion: Shamir cross-language alignment. Narrative: Recovery where the math matches the marketing.
— Part of