What is protected, by what, and what is not.
Biometric systems attract a lot of hand-waving. This page is the opposite: the exact construction, and the exact places it can fail.
A face descriptor is never identical twice — lighting, angle and expression move it around. A cryptographic key must be bit-exact. FaceGUID bridges the two with a fuzzy commitment over reliably-selected random projections.
| Key | Derived from | Protects |
|---|---|---|
| Face Key | HKDF(secret ‖ optional passphrase, salt) | Wraps the two below. |
| Vault Master Key | Random 256-bit, wrapped by the Face Key | Record titles, tags, per-record keys, the search key. |
| Signing key | ECDSA P-256, private half wrapped by the Face Key | Signs every record and every reveal. The public half is what the server checks. |
| Search key | HKDF(vault master key) | Blind-index tokens — HMACs the server can match but not invert. |
Unlocking the vault decrypts metadata only: titles, kinds, tags, field names. Every individual secret stays sealed.
Revealing one takes a second ceremony. The server issues fresh digits bound
to that specific record id, burns them on first use, and requires an ECDSA signature over
faceguid/reveal/v1|guid|itemId|nonce. A signature for one record proves nothing
about another, and a signature from five minutes ago proves nothing now.
| Stored | Readable by the operator? |
|---|---|
| Face template (128 floats) | Encrypted at rest with a server key; decrypted in memory only during a 1:N match. |
| Fuzzy-commitment mask | Yes, and it is useless without the face. |
| Record titles, tags, secrets | No. AES-GCM under keys derived in your browser. |
| Search queries | No. Only truncated HMACs, which cannot be inverted. |
| Linked email / phone / URLs | Yes — these are claims about public identifiers, and the API releases them by scope. |
| Document ciphertext | No. Chunk-wise AES-GCM under a key wrapped by the vault master key. |
| Document names and sizes | Names no — the metadata is encrypted. Byte counts yes, since storage has to be measured. |
| Proof receipts | Yes — which client asked, when, and whether the proof was spent. Never what it was spent on. |
| Audit log | Yes. Event names and timestamps, never contents. |
A document adds one thing the vault does not have: a seal that is meaningful to someone who does not trust us, and stays meaningful if we disappear.
| Encrypted before it moves | The browser splits the file into 1 MiB chunks and encrypts each under a per-document AES-256-GCM key. That key is wrapped with the vault master key, which is derived from the face. What arrives here is ciphertext with no key attached. |
| Chunks cannot be rearranged | Chunk i is encrypted with iv = baseIv ‖ uint32(i) and additional
data naming the document, the index and the total. Drop a chunk, reorder two, or splice
one in from another file and decryption fails rather than producing something
plausible. |
| The seal is a signature, not a record | The face-bound key signs
faceguid/seal/v1|guid|document|sha256|timestamp. We check that signature
before storing anything — an unsigned document is deleted, bytes and all — but the
proof does not depend on us having done so. It is verifiable from the certificate
alone. |
| Opening is its own ceremony | Each document has its own liveness challenge and its own signature over its own id. A signed-in session lists documents; it does not open them. The bytes are released against a token that expires in two minutes and burns on first read. |
| The hash is of the plaintext | Which is what lets a stranger holding the original file verify the seal without ever seeing the ciphertext — and lets you detect, on opening, that stored bytes no longer match what was sealed. |
| Publishing is opt-in and narrow | A published certificate discloses the hash, the timestamp, the signature and the FaceGUID that made it. It never discloses the document, its name, its size in any useful sense, or its contents. Unpublished and non-existent seals answer identically. |
A proof is the smallest thing FaceGUID says about a person, and the thing most applications actually consume. Five properties do the work.
| Signed, not asserted | ES256 over the same key as id_tokens, verifiable against the same JWKS.
The header typ is faceguid-proof+jwt, so a token minted for one
purpose cannot be spent as the other. |
| Bound to one client | aud is the client_id, and only that client’s credentials can
verify it. A proof lifted from one integration is worthless at the next. |
| Bound to one challenge | Your nonce travels inside the signature. Check it and a proof cannot be
moved between sessions of your own application. |
| Bound to one origin | The browser can only mint a proof for a web origin registered on that client, and the
ceremony posts it to that exact origin — never to *. A leaked
client_id cannot be used to harvest proofs from a page you do not control. |
| Spendable once | Every mint writes a receipt. Verification redeems it; the second attempt is a
409. Verify offline if you prefer speed — then redemption is your call to
/api/proof/redeem, not something the signature can do for you. |
An identity provider's consent screen is a credibility machine. If anyone can point one at anywhere, the provider becomes phishing infrastructure — a genuine URL, a real camera prompt, and a destination chosen by the attacker.
| Destinations must prove themselves | Before a client may redirect to a host, or run the SDK on it, the registering identity
must publish faceguid-verification=… — derived from its own GUID — at
https://host/.well-known/faceguid.txt. Unproven hosts are refused at
registration and on every edit, so a client cannot be quietly repointed later. |
| Loopback is exempt | localhost and 127.0.0.1 need no proof. A developer on their
own machine cannot publish anything, and cannot phish anyone with an address that
only resolves on the victim's own computer. |
| Nothing may claim to be us | A client cannot be named after FaceGUID. The consent screen also shows the exact origin you will be returned to, next to the name the developer chose. |
| Proofs are origin-bound | A proof of human is minted only for a registered origin and posted only to that exact
origin — never to *. A leaked client_id cannot harvest
proofs from a page its owner does not control. |
Deleting is one operation, it is available to you without asking us, and it costs a fresh face ceremony — a challenge bound to deletion and a signature by the face key — so a stolen session cannot spend it.
It removes the face template, the helper data, every wrapped key, every vault record and its search tokens, every sealed document including the encrypted bytes in object storage, every linked identifier, every grant and access token, every proof receipt, the activity log, and any developer credentials or organisations created by that identity. Object storage does not cascade from the database, so the bytes are removed first and explicitly — deleting rows before objects would leave the objects unreachable and permanent.
Afterwards the same face enrols as a new GUID with no link to the old one. Nothing is retained, so nothing can be restored. The full inventory →
Stated plainly, because a security page that only lists strengths is marketing.
- Liveness is client-side. Analysis runs in the browser and reports a verdict. Server-issued single-use digits defeat replay; they do not defeat a modified client. What a modified client cannot do is produce the face-derived key, which is where the vault’s security actually rests.
- The descriptor is the crown jewel. Anyone holding both your descriptor and your mask can recover your secret. The descriptor is transmitted for 1:N identification, so it is protected by transport security, rate limits, and the liveness gate — not by cryptography. The optional passphrase closes this gap, and is the right choice for a vault holding anything serious.
- Faces are not revocable. A leaked password is replaced in seconds; a face is not. This is inherent to every biometric system.
- Identical twins. A known limitation of face recognition as a class. Two people who look sufficiently alike may resolve to one GUID.
- Lose the face, lose the vault. There is deliberately no recovery path — a reset mechanism would be a second, weaker way in. Keep independent backups of anything you cannot afford to lose.
- A proof is evidence, not proof. It says a browser passed the ceremony for your client, within five minutes, once. It cannot say the browser was unmodified. It is enough to gate a signup or rate-limit by person; it is not enough to be the only control on a payment.
| Descriptor | 128-D, L2-normalised (FaceNet-style embedding) |
| Match threshold | 0.45 L2 · adaptive template update below 0.35 |
| Projections | 6,144 candidates, 1,920 selected |
| Secret | 128 bits, repetition code R=15, majority decode |
| KDF | HKDF-SHA256 · PBKDF2-SHA256 310,000 iterations for the passphrase |
| Record encryption | AES-256-GCM, unique key per record, 96-bit random IV |
| Signatures | ECDSA P-256 / SHA-256 |
| Liveness challenge | 3 digits, 120-second TTL, single use, server-issued |
| Sessions | HMAC-SHA256, 30 minutes, HttpOnly + Secure + SameSite=Lax |
| Document chunks | 1 MiB plaintext, AES-256-GCM per chunk, IV = 8-byte base ‖ uint32 index, AAD binds document id, index and count |
| Document seal | ECDSA P-256 / SHA-256 over the plaintext hash and timestamp |
| Document download token | 120-second TTL, single use, bound to one document and one identity |
| Proof token | ES256, typ: faceguid-proof+jwt, 5-minute default TTL (60–900s), single redemption |
| Authorization codes | 120-second TTL, single use, PKCE S256 required for public clients |