1. Overview
Keptex derives credentials (passwords and WebAuthn passkeys) deterministically from a Signer. The pipeline has three phases: userKey derivation, siteKey derivation, and per-credential materialisation.
Pipeline pseudocode
Signer ──▶ userKey (64 B, cached per session)
│
├──▶ siteKey (32 B) ──▶ password
│
├──▶ passkeySeed (32 B) ──▶ Ed25519 / ES256 keypair
│
└──▶ credentialId (32 B) 2. Notation and primitives
Every primitive is named in a published standard. No bespoke crypto. The core uses Web Crypto exclusively; the passkey package depends on @noble/curves (Trail of Bits audited).
3. Phase 1, userKey derivation
Two paths converge to the same shape: a 64-byte userKey cached in RAM for the session.
Path A, imported wallet
Path B, Keptex-native wallet
4. Phase 2, siteKey derivation
A 32-byte siteKey is derived per (service, username, counter) tuple, using length-prefixed concatenation to prevent collisions.
5. Phase 3a, password generation
A DeterministicStream samples uniformly-distributed integers via HMAC-SHA-256 with rejection sampling on 4-byte windows. No modulo bias. Fisher-Yates shuffle ensures required-class characters are not always at the front.
Rejection sampling
Generation pseudocode
# Phase 1: one char per required class
for cls in classes:
chars.push( cls[ stream.nextIndex(len(cls)) ] )
# Phase 2: fill the remainder from the union
while len(chars) < policy.length:
chars.push( allChars[ stream.nextIndex(len(allChars)) ] )
# Phase 3: Fisher-Yates shuffle (deterministic)
for i in (len(chars), 1) downto 1:
j = stream.nextIndex(i + 1)
swap chars[i], chars[j]
password = join(chars) 6. Phase 3b, passkey derivation
A 32-byte passkeySeed feeds Ed25519 (RFC 8032 seed) or ES256 (P-256 scalar, mod n). COSE_Key encoding is canonical CBOR, hand-emitted to avoid pulling a CBOR library.
7. Phase 3c, credentialId derivation
A 32-byte credentialId is derived independently of the algorithm. This allows matching an RP’s allowCredentials list without first deriving the full key.
8. Invariants
- Pure determinism, byte-identical output across runs, platforms, implementations.
- Domain separation, different scopes yield statistically independent outputs.
- Chain-invariance, primary chain choice does not affect userKey (wallet-native).
- Cross-curve credentialId stability, switching Ed25519 ↔ ES256 does not change credentialId.
- No modulo bias, rejection sampling on every character selection.
9. Test vectors
Reference vectors live at packages/core/src/test-vectors.json. Each vector is a (Signer, options) tuple and the expected password output. Format is JSON for cross-language verification.
For the complete specification, see github.com/keptex.