ROBIN VAULT

KDF & Derivation Pipeline

The exact sequence from a card tap to three fully-derived wallets — every step named precisely, no simplification.

The pipeline

  1. NFC tap — raw UID bytes are read from the card.
  2. accountId = SHA-256(uid) — a 64-character lowercase-hex hash, computed client-side.
  3. The client POSTs accountId to the backend.
  4. The backend returns a 32-byte random server salt, base64-encoded.
  5. argon2Salt (16 bytes) = SHA-256(uid ++ serverSalt)[0:16] — truncated to exactly 16 bytes because libsodium's Argon2id implementation requires a salt of exactly crypto_pwhash_SALTBYTES = 16.
  6. The PIN's bytes and argon2Salt are fed into Argon2id, producing 32 bytes of raw entropy.
  7. Those 32 bytes (256 bits) become a 24-word BIP39 mnemonic via entropyToMnemonic.
  8. The mnemonic is expanded into a 64-byte BIP39 seed.
  9. Three independent chain derivations run from that same seed — see Multi-Chain Support for the exact paths.

Version-tagged, on purpose

The salt-derivation scheme above is frozen and tagged as saltSchemeVersion = 1. Changing it later would silently change every existing user's derived address with no way to detect it after the fact — the version tag exists purely as a tripwire for future maintainers.

Argon2id parameters

ParameterValue
Memory limit (default, before calibration)128 MiB
Memory limit (calibrated floor)64 MiB
Memory limit (calibrated ceiling)512 MiB
Passes / opsLimit (default, before calibration)3
Passes / opsLimit (calibrated floor)2
Parallelism (p)1 — hard-coded by libsodium, not app-configurable
Output length32 bytes (256 bits)
Salt length16 bytes (crypto_pwhash_SALTBYTES)
Calibration target (wall-clock)1.5–3 seconds

Adaptive calibration

Argon2id's memory and pass count aren't fixed globally — they're calibrated once per device on first run, targeting 1.5–3 seconds of wall-clock time, then clamped into the floor/ceiling bounds above (so a slow device never drops to a cheaply-brute-forceable cost, and a fast device never runs so long or uses so much memory that the OS kills the app for memory pressure). The calibrated integers are persisted locally and never silently re-calibrated — doing so would change a user's derived wallet address without warning.

Secure-memory handling

Every intermediate buffer — the Argon2 salt, the KDF entropy output, the BIP39 seed, and each chain's raw private key — is explicitly zeroed in memory once it's no longer needed. See Auto-Lock & Secure Memory for the full wipe semantics and one disclosed residual exposure.