Skip to main content
HUMΛN
Passport
Passport

Server-blind social recovery: HMAC proofs and completion modes

HUMΛN Team··16 min·Security engineers

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_proof session 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_id in 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_proof request → 409 WRONG_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 carries proof_of_reconstruction and 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