Identity & app keys
There are two kinds of “identity” in Muhkoo, and they answer different questions.
User identity (zero-knowledge)
Section titled “User identity (zero-knowledge)”A user’s identity is derived on their device from a 32-byte master seed:
seed = 32 random bytes, generated on the device at registrationsecret = HKDF(seed, "zk-secret")salt = HKDF(seed, "zk-salt")…plus deterministic ECDSA + ECDH keypairscommitment = Poseidon(secret, salt, Poseidon(ecdsaPub))Accounts created before the vault derived the seed from the password instead
(PBKDF2(password, "muhkoo-zk-v1:" + username)). Their first vault-aware login
wraps that same seed, so the commitment doesn’t change.
The accelerator stores the commitment — a one-way Poseidon hash — and, in a
per-user vault, copies of the seed wrapped under each login factor the user has
enrolled. At login the client unwraps the seed, then proves, in zero knowledge
(a Groth16 proof over the preimagePoK circuit), that it knows a
(secret, salt, …) matching the stored commitment, without revealing any of it.
Consequences:
- No password on the server. The password never leaves the device; the vault holds only wrapped copies of the seed.
- Federated login. Any device that can unlock one of the user’s factors —
usually
(username, password)— gets the same seed, and so the same identity (and commitment). There’s nothing to copy between devices by hand. - The commitment is the user’s stable id — it’s what
user.commitmentreturns and how per-user data is addressed.
Besides the password, the seed can be wrapped by a passkey, a recovery phrase, a
paired browser or machine (a device factor), an email address or a Google
account. Email and Google are gated factors: they hold no user secret, so
keys Muhkoo holds release the wrapped seed once the user proves control of the
inbox or Google account. That means Muhkoo could technically recover the seed,
and everything encrypted under it, for any account with an email or Google
factor; the other factors stay user-held. See
Email & Google factors for the
disclosure your app owes its users.
Where the seed lives
Section titled “Where the seed lives”The derived seed is held in memory and, in an ordinary app, written nowhere — so a reload has nothing to restore. Two mechanisms make signing in a once-per-browser act rather than a once-per-tab one, and neither of them stores the seed:
- A
devicefactor wraps the seed under a random key, leaving the ciphertext in the vault and the key on one browser or machine. Neither half is enough alone, and revoking the factor makes the local key inert. This is what “Keep me signed in on this browser” enrolls, and what the CLI enrolls the first time a command needs your files — see Staying signed in. - The sealed-seed handoff moves the seed from the hosted sign-in page to an app for exactly one redirect, under a one-time key that never reaches a server.
The exception is a keyboard-less device, which has to
persist the seed itself because nobody can type a password into a TV. That is why
removing a TV from the account does not lock it out, while revoking a device
factor does.
App keys
Section titled “App keys”Your app authenticates to the accelerator with an app key. Keys look like:
mk_<env>_<type>_<32 hex> │ │ │ └ sk = secret key (server-side) pk = publishable key (browser-safe) └ live | testpk(publishable) — safe to ship in client bundles. Use this innew Client({ apiKey })for browser apps.sk(secret) — server-side only. Never put it in a browser bundle or any client-exposed env var.testvslive— separate environments. Test keys exercise the full stack without touching live billing/usage.
Keys are issued per app from the developer portal
(or POST /api/apps). They’re per-deployment — a key minted against one
accelerator (e.g. staging) won’t resolve on another (e.g. production).
What the app key gates
Section titled “What the app key gates”- Billing + metering — usage is attributed to the app behind the key.
- Messaging — shared-space (space/DM) websockets are authorized with a short-lived ticket derived from the app key.
How they combine
Section titled “How they combine”A request carries the app key (which app) and, once a user signs in, the session token (which user). Authorization is the intersection: this app, acting for this user. CORS is wildcard because the API is public and multi-tenant — security rides on the keys, not the origin.