KDF & Derivation Pipeline
The exact sequence from a card tap to three fully-derived wallets — every step named precisely, no simplification.
The pipeline
- NFC tap — raw UID bytes are read from the card.
accountId = SHA-256(uid)— a 64-character lowercase-hex hash, computed client-side.- The client
POSTsaccountIdto the backend. - The backend returns a 32-byte random server salt, base64-encoded.
argon2Salt (16 bytes) = SHA-256(uid ++ serverSalt)[0:16]— truncated to exactly 16 bytes because libsodium's Argon2id implementation requires a salt of exactlycrypto_pwhash_SALTBYTES= 16.- The PIN's bytes and
argon2Saltare fed into Argon2id, producing 32 bytes of raw entropy. - Those 32 bytes (256 bits) become a 24-word BIP39 mnemonic via
entropyToMnemonic. - The mnemonic is expanded into a 64-byte BIP39 seed.
- 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
| Parameter | Value |
|---|---|
| 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 length | 32 bytes (256 bits) |
| Salt length | 16 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.