How identity is derived¶
An Omxus identity is a key, not a row in a table. The same inputs make the
same key on any device, so nothing about the person has to be stored to sign
them in. Sites never see that key. Each site gets its own key, and each
signed-in app gets its own sub.
This page follows one person from what they type to the sub your app
receives.
three inputs ─┐
phrase ───────┼─► seed ─┬─► master key m/44'/60'/0'/0/0
passkey ──────┘ └─► site key m/44'/60'/1'/a'/b'/c'
│
▼
sub = pairwise(site address, sector)
1. Making a seed¶
There are three ways to a seed. They end in the same place, and a site cannot tell which one was used.
| Way in | What becomes the seed |
|---|---|
Three inputsderiveIdentity(name, dob, words) |
name + " " + dob + " " + words, run through BIP-39's PBKDF2 (SHA-512, 2,048 rounds, salt "mnemonic"), 64 bytes |
Recovery phraseimportMnemonic(phrase) |
A 12- or 24-word BIP-39 phrase from generateMnemonic(), checked by validateMnemonic(), same PBKDF2 |
PasskeyidentityFromSeed(seed) |
The authenticator's PRF output, used as the seed directly (no PBKDF2). At least 16 bytes |
The three inputs are cleaned first. Name and words are trimmed, lowercased
and have runs of spaces collapsed. The date is only trimmed, so its format
matters: the sign-in pages use a date field, which sends YYYY-MM-DD. Two
people typing Ada Lovelace and ada lovelace get the same key;
1815-12-10 and 10/12/1815 do not.
Only the words are secret. Name and date of birth are public, so they act
as a salt. The words must score at least 60 bits before any key is made
(validateWords()); Security model explains the scoring and
its limits. Because the public key travels with every sign-in, a weak secret
can be guessed offline. Offer passkeys or a
recovery phrase to anyone who will take one.
2. The master key¶
From the seed, standard BIP-32 derivation along m/44'/60'/0'/0/0 gives the
master key: an ordinary Ethereum key and address. It is what signs
on-chain actions, such as vouching. It is never used to sign in
to a site.
3. A key for each site¶
Every website gets its own key from the same seed, on a sibling branch:
a, b and c come from keccak256("omxus-site:v1:" + origin): the first
93 bits, split into three 31-bit indices. Every step is hardened, so leaking
one site's key cannot be walked back to the seed or across to another site.
The origin is canonicalised first, so client and server hash the same string:
lowercase scheme and host, default port dropped, path dropped, https://
assumed when no scheme is given.
| Input | Canonical origin |
|---|---|
https://auth.omxus.com |
https://auth.omxus.com |
HTTPS://Auth.Omxus.com:443/path |
https://auth.omxus.com |
auth.omxus.com |
https://auth.omxus.com |
http://localhost:8080 |
http://localhost:8080 |
Two consequences:
- Sites cannot link people. Two sites see two unrelated addresses. Joining them needs the seed.
- Signatures do not travel. The signed challenge names the origin (see Security model), so a signature made for one site is worthless at another.
Nothing is stored for this. The site key is a pure function of seed and origin, so every device holding the seed gets the same answer.
4. What Sign in with Omxus hands your app¶
When a person signs in through auth.omxus.com, the door derives the site key
for https://auth.omxus.com. That address is the one the provider knows.
Your app then receives a pairwise sub:
addressis the auth.omxus.com site address, lowercased.saltis a provider secret. Without it, anyone holding an address and a hostname could recompute thesuband match people across sites.sectoris your client's approvedsector_identifier_uri, or else the host of its first redirect URI, or else itsclient_id.
So the same person has a different sub at every site, and the same sub
every time at yours. Clients sharing a sector share a sub: that is how a
website and its iOS app see one person (Registering a
site).
Standard-door accounts (email or phone and password, Your own sign-in
page) have no key. Their sub uses the same formula
with "account:" + account_id in place of the address, so it can never equal
a key-derived sub.
omxus_address. With the profile scope, the id_token for a key-derived
identity also carries omxus_address, the auth.omxus.com site address. It is
the same at every site that asks for it. Key accounts on sub, never on
omxus_address.
5. Email is a handle, never a key¶
A key made from an email address could be made by anyone who knows the address. So email is only ever a lookup handle:
It hides the address from someone skimming a database dump. A list of known addresses breaks it, so it is not a secret and never proves who someone is.
Try it¶
These calls run the engine's own code (checked 30 Oct 2026 with Node 22
against engine.bundle.mjs):
import '/path/to/engine.bundle.mjs'; // sets the functions on globalThis
const me = await deriveIdentity('Ada Lovelace', '1815-12-10', 'your secret words');
const site = await deriveSiteIdentity(me.mnemonic, 'https://example.com');
me.address // master address, m/44'/60'/0'/0/0
site.address // this site only; another origin gives an unrelated address
canonicalOrigin('HTTPS://Example.com:443/x') // 'https://example.com'
Not built yet¶
- Online guessing. The three inputs can be guessed offline today. The planned fix makes the provider take part in every derivation (an OPRF), so each guess goes through a rate limit and a stolen database cannot be ground. It would mean a new device needs the network the first time.
- More rounds. PBKDF2 stays at the BIP-39 count of 2,048. Raising it needs a migration for existing keys.
Until then, prefer passkeys and recovery phrases for anything that matters.