Skip to main content
Keptex

Specification (V1).

This document is the normative cryptographic specification. Independent re-implementations should produce identical outputs from the published test vectors.

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.