Home / How it works

How it works

Status, from /shipped/: proposed Sign in and out with Google and email/password against the chosen environment · proposed Passkey with WebAuthn PRF derives the keyring wrapping key; RP ID secrets.sgit.ai · proposed Keyring v1 format: wraps per unlock method, AES-256-GCM body, known-answer tests · proposed Devices page: add and remove passkeys, regenerate the recovery code

Two things decide what you can do. The login decides which paths in the bucket you may touch. The passkey decides whether the bytes there mean anything. They are deliberately separate: an administrator of the login can fake the first and can never fake the second.

This page describes the design in section 3 of the brief. Every flow on it is proposed. The status line above is generated from data/features.json and changes when the code ships.

The parts

LayerComponentWhere it runsWhat it is trusted with
Site and appStatic HTML, JS and CSS from this repository, on GitHub Pages at secrets.sgit.aiYour browserThe boundary. Whoever controls this repository or the DNS controls the app
LoginIdentity Platform, through the vendored Firebase Auth SDKGoogleCan impersonate; cannot decrypt
StorageA Cloud Storage for Firebase bucket with Security RulesGoogleHolds ciphertext; can delete
UnlockA WebAuthn passkey with the PRF extension, RP ID secrets.sgit.aiYour authenticator: Google Password Manager, iCloud Keychain or a hardware keyThe only thing that can decrypt
AdminThe same static pages, calling GCP's own REST APIs with your Google account's tokenYour browserWorks only if your Google account has IAM on the project
InfrastructureTerraform in this repository, applied by GitHub Actions through Workload Identity FederationGitHub ActionsCan reconfigure or delete; cannot read

First run

sign in ──▶ no keyring at users/{uid}/keyring.json
        ──▶ create a passkey with the PRF extension       (your authenticator asks for a gesture)
        ──▶ generate: KEK (32 random bytes), a key pair, a recovery code
        ──▶ wrap the KEK under the passkey's PRF output and under the recovery code
        ──▶ encrypt the body under the KEK; write keyring.json and meta.json to the bucket
        ──▶ show the recovery code once, behind "I have written it down"

What leaves the browser: ciphertext, two wrapped copies of the KEK, a 32-byte PRF salt, and meta.json with the passkey's credential id and public key. What never leaves: the PRF output, the KEK, the recovery code, the private keys, any entry.

Returning

sign in ──▶ fetch keyring.json (and remember its generation number)
        ──▶ navigator.credentials.get with prf.eval.first = prfSalt   (one gesture)
        ──▶ HKDF-SHA256(prf output, salt, "sgit-secrets/v1/wrap/<credentialId>") → wrapping key
        ──▶ unwrap the KEK; decrypt the body into memory
        ──▶ the vault list

Nothing decrypted is written anywhere: not to localStorage, sessionStorage, IndexedDB, the URL or a log. The gate checks the code for that on every release (check 8 on the admin page). Keys are cleared from memory on sign-out, on an environment change, when the tab has been hidden for five minutes, and on beforeunload.

New device

sign in ──▶ fetch keyring.json
        ──▶ unlock with a synced passkey, a cross-device passkey (QR), or the recovery code
        ──▶ register a new passkey on this device
        ──▶ add a wrapped-KEK entry for it; bump rev; write with an if-generation-match precondition

Lose every passkey and the recovery code, and the data is gone. There is no reset, because nothing that could reset it exists anywhere but your authenticator and your note of the code. The site will say this on the setup page in the same words.

Admin

open /admin/ ──▶ choose an environment
             ──▶ "Sign in with Google for admin" (OAuth, scope cloud-platform, token held in memory only)
             ──▶ each check calls a GCP API: exists / enabled / configured / Fix

There is no admin role in the app. The GCP project's IAM is the role; the pages are a client for GCP's APIs. An admin page can list users, deploy rules and change CORS. It cannot open a keyring, because a keyring is ciphertext and the admin's token unlocks nothing.

What each party can see

StepGoogle (Identity Platform, bucket)GitHub (the site)Your authenticatorYour browser
Sign inYour email, the sign-in event, your uidNothing (static files)NothingAn ID token
Fetch keyringThat uid read that objectNothingNothingCiphertext
Passkey gestureNothingNothingThe PRF secret for this credential and this origin32 bytes of PRF output, briefly
UnlockNothingNothingNothingThe KEK and the plaintext body, in memory
WriteNew ciphertext, the object's size and timeNothingNothingEverything it already had

The one party absent from that table is whoever serves the JavaScript. The code your browser runs is the boundary of the whole design, which is why the security page is mostly about this repository.

Read next