Your Node's Seed & Entropy
+How 32 random bytes become every key your node owns — where that randomness comes from, what protects it, and exactly what your 24 words can (and cannot) bring back.
+ +Introduction
+One master secret, many keys — by design.
+ ++Almost everything cryptographic on this node — its identity, its Nostr keys, its mesh +transport keys, its Lightning wallet — grows from a single master seed: +32 bytes (256 bits) of randomness drawn once, shown to you once as a 24-word recovery +phrase, and never stored in raw form anywhere. +
+ ++This page explains, in both plain and technical language: where those 32 bytes come from, +the guardrails that make sure they're genuinely random, how they're protected on disk, +everything that is derived from them, everything that deliberately isn't, and +what happens in every failure and recovery scenario. +
+ +The Big Picture
+From kernel randomness to every key on the node.
+ +Where Randomness Comes From
+One named source. No mixing. No silent defaults.
+ +Technically
+
+The master seed is generated by MasterSeed::generate() in
+core/archipelago/src/seed.rs. It fills a 32-byte buffer using
+rand::rngs::OsRng — a thin wrapper around the
+getrandom(2) system call, which reads the kernel's CSPRNG
+(the same source as /dev/urandom, but immune to file-descriptor
+exhaustion and chroot tricks).
+
-
+
- Exactly 32 bytes / 256 bits — the maximum BIP-39 strength, encoding to 24 words. +
- The RNG is named at the call site. No function anywhere in the codebase generates key material with a "default" or implicit RNG anymore (see Recent Hardening for why this is stated so emphatically). +
- The RNG type is compiler-enforced. Key-generation functions only accept RNGs on a sealed allowlist (
KeyGenRngincore/archipelago/src/entropy.rs). The list has exactly one production member:OsRng. No other module — not even a future refactor — can add a weaker RNG without editing the allowlist file itself.
+ - The buffer is zeroized (securely wiped from memory) on every path, success or failure. +
// core/archipelago/src/seed.rs — the actual draw
+let mut entropy = [0u8; 32];
+crate::entropy::draw_key_bytes(&mut rand::rngs::OsRng, &mut entropy)?; // guarded draw
+let mnemonic = bip39::Mnemonic::from_entropy(&entropy)?; // → 24 words
+entropy.zeroize(); // wipe the raw bytes
+
+
+The 24 words are then stretched into the 64-byte master seed using standard BIP-39:
+PBKDF2-HMAC-SHA512, 2048 rounds, empty passphrase. (There is no "25th word"
+passphrase — your login password protects the stored backup instead, see
+The Encrypted Envelope.) That 64-byte seed exists
+only in memory, is re-computed from the words whenever needed, and is
+never written to disk in any form.
+
When the Seed Is Born
+At onboarding — not at first boot.
+ +First boot: a placeholder key
+A freshly-flashed node boots with a random temporary identity key so services can start. This key is NOT seed-derived and is about to be thrown away.
+Onboarding: the real draw
+When you reach the "Recovery phrase" step of setup, the seed.generate RPC performs the guarded 32-byte draw described above and shows you the 24 words — the only time they're ever displayed unprompted.
Derivation: keys are materialised
+The node identity key, DID, Nostr key, FIPS mesh key, and your first personal identity are all derived from the seed and written to /var/lib/archipelago/identity/ (each file mode 0600). The placeholder key from step 1 is overwritten.
Password setup: the backup is sealed
+When you set your login password, the 24 words are encrypted under it (Argon2 + ChaCha20-Poly1305) and stored as master_seed.enc — so you can re-reveal them later from Settings → Backup.
The Five Guardrails
+Defence in depth around a single random draw.
+ +1 Sealed RNG allowlist
+Key draws only compile against RNG types on a closed, private allowlist. Production allowlist: OsRng. Full stop. A refactor that swaps in a weak or deterministic RNG becomes a compile error, not a silent disaster.
2 Degenerate-draw refusal
+Every draw is inspected for three tell-tale broken-RNG shapes: all zeros, all bytes identical, or a counting pattern. A match is refused and wiped — never retried, because retrying would mask a broken RNG instead of exposing it.
+3 CSPRNG readiness ledger
+Before generating the seed, the node probes whether the kernel's random pool is fully initialised, and appends the verdict to a tamper-evident, append-only log at security/csprng-readiness.jsonl (0600). You can audit, forever, the entropy conditions your seed was born under.
4 Build-time lint bans
+The CI lint config bans rand::random() and rand::thread_rng() outright across the workspace — the two "convenient" RNG entry points that caused real-world wallet disasters elsewhere. Using either fails the build.
5 Zeroization everywhere
+Raw entropy, mnemonics, and derived secrets are wiped from memory on every code path — including error paths — so key material doesn't linger in freed RAM or end up in crash dumps.
+What the degenerate check does — and doesn't — do
+
+The check (is_degenerate in entropy.rs) is deliberately
+closed-form: it recognises exactly three catastrophic failure shapes
+(all-zero, all-identical, ±1 counter). It is not a statistical entropy
+estimator — those can't actually distinguish good randomness from a cleverly broken RNG,
+and they introduce false positives. The real security load is carried by guardrails 1,
+3 and 4; the degenerate check is a tripwire for total RNG failure (e.g. a zeroed buffer
+that was never filled).
+
Recent Hardening (the KEY-05 work)
+This system was audited and rebuilt in early August 2026.
+ +
+Triggered by the COLDCARD-class of entropy defects, a full entropy & seed-generation
+audit (docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md) reviewed every random
+draw in the codebase. The headline finding: the library used for mnemonic generation was
+silently choosing its own RNG via a transitive default. It happened to be a secure one —
+but nothing guaranteed that, and a dependency update could have changed it
+without any diff in Archipelago's own code.
+
| Date | Change |
|---|---|
| Jul 30 | Kernel CSPRNG readiness probe added; RNG non-determinism regression test (64 consecutive mnemonics must all be unique). |
| Jul 31 | Full entropy audit published (findings F-01…F-13). |
| Aug 1 | The pivotal fix: master-seed RNG made explicit — OsRng named at the call site, injected through a testable seam, pinned by a known-answer test. |
| Aug 2 | Audit widened: 43 defaulted-RNG call sites across 15 files found and migrated to explicit OsRng — including AEAD nonces and ecash key material. |
| Aug 2 | Onboarding RPCs gated: seed.restore and friends now refuse on an already-provisioned node (previously an unauthenticated restore call could hijack a live node — fixed before any release shipped it). |
| Aug 2 | KEY-05 enforcement layer landed: sealed KeyGenRng allowlist, guarded draws, durable readiness ledger, clippy bans, supply-chain version pinning on the rand crate. |
| Aug 2 | Legacy Bitcoin Core wallet-import path deleted — the master xprv is no longer handed to any external wallet process. |
What's Kept on Disk
+The words, encrypted — and the keys, materialised. Never the raw seed.
+ +| File | Contents | Protection |
|---|---|---|
identity/master_seed.enc | Your 24 words (as text), encrypted | Argon2(login password) + ChaCha20-Poly1305, mode 0600 |
identity/node_key / .pub | Node Ed25519 identity key | 0600, seed-derived (recoverable from words) |
identity/nostr_secret / nostr_pubkey | Node Nostr keypair | 0600, seed-derived |
identity/fips_key / .pub | FIPS mesh transport key (bech32 nsec) | 0600, seed-derived |
identity/identity_index | Next unused identity derivation index | Plain integer (not secret) |
identities/<uuid>.json | Personal identity records (keys + metadata) | 0600; keys seed-derived, metadata is not |
identity/lnd_aezeed.enc | Lightning wallet's own seed (see below) | Encrypted under the LND wallet password |
security/csprng-readiness.jsonl | Append-only entropy audit trail | 0600; deliberately outside identity/ so restores never touch it |
The Encrypted Envelope
+How master_seed.enc is built.
+Revealing the words later (Settings → Backup → Reveal) requires an authenticated +session plus re-entering your password plus your 2FA code if enabled, +is rate-limited, and the words are returned only to your browser — never written to logs. +
+ +The Derivation Tree
+Every key, its exact derivation, and where it lands.
+ +| Key | Derivation | Label / path | Materialised at |
|---|---|---|---|
| Node identity (Ed25519) — signs everything, forms your DID | HKDF-SHA256 | archipelago/node/ed25519/v1 | identity/node_key |
| Node Nostr key (secp256k1) — the node's npub | HKDF-SHA256 | archipelago/nostr-node/secp256k1/v1 | identity/nostr_secret |
| FIPS mesh transport key — federation/mesh overlay identity | HKDF-SHA256 | archipelago/fips/secp256k1/v1 | identity/fips_key |
| Personal identity #i (Ed25519) | HKDF-SHA256 | archipelago/identity/{i}/ed25519/v1 | identities/<uuid>.json |
| Personal Nostr key #i — standard NIP-06, importable into other Nostr apps | BIP-32 | m/44'/1237'/0'/0/{i} | identities/<uuid>.json |
| Lightning (LND) wallet entropy — 16 bytes | HKDF-SHA256 | archipelago/lnd/entropy/v1 | fed into LND at wallet init (see below) |
| Bitcoin BIP-84 account xprv — native-segwit | BIP-32 | m/84'/0'/0' | nowhere — dormant, reserved for a future on-node cold vault |
| Release-root signing key — fleet update signing | HKDF-SHA256 | archipelago/release/root/ed25519/v1 | never on a node — derived offline by the publisher from a separate release mnemonic; nodes only pin the public key |
+All HKDF derivations are HKDF-SHA256 with a distinct, versioned info-label (the
+/v1 suffix means a future algorithm migration can introduce
+/v2 labels without ambiguity). Personal Nostr keys intentionally use the
+NIP-06 standard path instead of HKDF so that the same 24 words typed into any
+NIP-06-compliant Nostr client reproduce the same npub — your social identity is
+portable beyond Archipelago.
+
The Lightning Special Case
+The one branch of the tree with a one-way gate in it.
+ ++LND (the Lightning node) uses its own seed format called aezeed, which is not +BIP-39. Archipelago derives deterministic entropy from your master seed and hands it to +LND at wallet-creation time — but LND then wraps it with its own internal random salt, +so the resulting aezeed cannot be re-derived from your 24 words afterwards. +The node therefore captures the aezeed exactly once, at init, and stores it encrypted +alongside your other identity files. The Lightning seed backup screen shows it to you +with the same tap-to-reveal flow as the main phrase. +
+ +Second-Order Keys
+Derived from the node key — so still fully recoverable from the words.
+ +
+Several subsystems derive their keys from the node identity key rather than the
+master seed directly. Since the node key is itself seed-derived, these all regrow from
+your 24 words too — the chain is words → master seed → node key → subsystem key.
+
| Subsystem | Derivation from node_key |
|---|---|
| Reticulum / RNS mesh identity (LoRa long-range mesh) | HKDF-SHA256, salt archipelago-reticulum-identity-v1, two labels for the X25519 + Ed25519 halves — yields a stable LXMF address that survives reinstalls |
| Message-store encryption (chats at rest) | SHA-256("archipelago-message-store-v1" ‖ node_key) |
| Mesh contacts encryption | SHA-256("archipelago-mesh-contacts-v1" ‖ node_key) |
| Credential store (saved app credentials) | Same domain-separated SHA-256 pattern |
What Is NOT Derived From the Seed
+Independent randomness — deliberately outside the tree.
+ ++Plenty of secrets are freshly random rather than seed-derived. That's intentional: +things that should die with a session, rotate freely, or belong to a third-party app must +not be recoverable from your words (and mostly you wouldn't want them to be). +
+ +Ephemeral by design
+Session tokens, device/companion pairing tokens, federation invites, TOTP/2FA secrets and backup codes, all encryption nonces, X3DH ephemeral mesh keys, anonymous marketplace/discovery Nostr keys.
+App-owned secrets
+Every manifest-declared generated_secret (app database passwords, API keys), Bitcoin RPC credentials, the LND wallet password (distinct from its seed), Home Assistant tokens.
Host-level material
+WireGuard VPN keypairs (generated by wg genkey), SSH host keys and the TLS certificate (created by the installer image at first boot), the machine-id.
Opt-outs from derivability
+Extra identities created with "new random key" instead of seed derivation, and a node key after an explicit rotate-key — rotation deliberately breaks the link to the words, and says so.
Failure Modes & Fallbacks
+What happens when something goes wrong — at every stage.
+ +| Scenario | Behaviour | Outcome |
|---|---|---|
| RNG returns a degenerate pattern (all-zero / repeated / counter) | +Draw is refused and wiped, never retried; error logged; onboarding fails loudly | +No seed created | +
| Kernel random pool not yet initialised (exotic first-boot case) | +getrandom(2) blocks until the pool is seeded — an unseeded pool physically cannot produce a seed. The advisory probe logs a warning and records the verdict in the readiness ledger |
+ Waits, then proceeds | +
| Onboarding page refreshed / retried mid-generation | +Same words returned for 10 minutes (mutex-serialised); no second seed can be minted | +Idempotent | +
master_seed.enc missing (e.g. backup write failed during setup) |
+ Node runs normally — every derived key is already materialised on disk. Only the Reveal feature and future re-derivation are unavailable; the UI says so explicitly | +Degraded, functional | +
master_seed.enc corrupt, or wrong password at reveal |
+ Authenticated decryption fails closed with an explicit error — no fallback, no partial output, no auto-regeneration | +Fails loudly | +
| Restore attempted on an already-provisioned node | +The onboarding gate refuses identity-mutating RPCs once the node is set up — a live node cannot be hijacked or accidentally re-seeded | +Refused | +
| Legacy / corrupt on-disk FIPS key format | +Self-heals: legacy raw-byte format is detected and migrated in place to the current bech32 format | +Auto-migrated | +
| Readiness-ledger write fails (disk full, permissions) | +Warns and continues — the audit trail is best-effort and can never block key generation | +Non-blocking | +
The Restore Flow
+Typing 24 words into a fresh node, step by step.
+ +Gate check
+Restore only proceeds on an un-onboarded node. A provisioned node refuses — this gate is load-bearing and runs before anything else.
+Validation
+Exactly 24 words, checked against the BIP-39 wordlist and its built-in checksum — a typo is caught here, before anything is written.
+Identity regrowth
+Node Ed25519 key + DID, node Nostr key, and FIPS mesh key are re-derived and written — byte-identical to the originals, because the HKDF labels are fixed.
+Personal identity #0
+The identity index resets to 0 and your default "Personal" identity (Ed25519 + NIP-06 Nostr key) is recreated. If you had created more seed-derived identities, their keys re-derive on demand as the index walks forward — but their names, avatars and profiles were metadata, not key material, and come from a node backup instead.
+Mesh reactivation
+FIPS federation auto-activation kicks off in the background; the Reticulum identity re-derives from the restored node key, so your LXMF mesh address comes back too.
+Password & re-seal
+When you set the (new) login password, the words are re-encrypted into a fresh master_seed.enc — the reveal feature works on the restored node just like the original.
What Comes Back — and What Doesn't
+ +Restored by the words
+Node identity & DID · node npub · FIPS mesh key · Reticulum/LXMF address · personal identity keys & npubs · message-store / contacts / credential encryption keys · the (dormant) Bitcoin xprv · the ability to reveal the phrase again.
+Needs its own backup
+Lightning wallet (aezeed — one-way gate, see above) and channel state · chat history & app data (node backup) · identity names/avatars (metadata) · app secrets (regenerate on reinstall).
+Gone by design
+Sessions & device pairings (re-login, re-pair) · 2FA secret (re-enrol) · WireGuard peers (re-pair) · rotated-away node keys · anonymous throwaway Nostr keys.
+SeedQR
+Your words as a scannable code — using the open SeedSigner standard.
+ ++Wherever the phrase is displayed, a QR code tab is offered alongside the +words. For the main (BIP-39) phrase the default format is SeedQR: each +word becomes its 4-digit position in the official wordlist (24 words → 96 digits), +encoded as a compact numeric QR. Hardware wallets like Passport, SeedSigner and Keystone +import this format directly — so you can move your on-chain identity into cold storage +without ever typing the words. A plain-text QR fallback exists for wallets that read the +phrase as text. +
+ +-
+
- The QR contains exactly the same secret as the words — treat a printout or screenshot of it with identical care. +
- The Lightning aezeed is deliberately never SeedQR-encoded — it isn't BIP-39, hardware wallets can't import it, and pretending otherwise would be dishonest. It gets a plain-text QR with an explanation instead. +
- Restore is by typed/pasted words; there's no camera-based SeedQR scanner on the restore path today. +
Honest Edges
+Known trade-offs and open hardening items, stated plainly.
+ ++The audit that produced this system also tracked what it didn't fix. None of +these are secrets — honest security means naming the edges: +
+ +-
+
- The words cross the RPC boundary. During onboarding the phrase travels (over HTTPS/localhost) to your browser to be displayed, sits in the browser's session storage for the wizard's duration, and is held in server memory for the 10-minute idempotence window. This is the deliberate price of a refresh-proof, display-once flow. +
- Argon2 parameters are the library defaults (≈19 MiB, 2 passes) rather than the heavier profile the design doc calls for. Still memory-hard and slow for attackers; scheduled for tightening. +
- 2FA backup codes carry a slight statistical bias from a modulo operation — cosmetically imperfect, cryptographically irrelevant at their length, and queued for cleanup. +
- The lint ban covers the main workspace, but one small helper crate outside the workspace isn't reached by it yet. +
- Best-effort backup sealing: if writing
master_seed.encfails during setup, the node continues (keys exist; only reveal is lost) — a loud failure might arguably be better, and this trade-off is under review.
+
Verify It Yourself
+Don't trust — recompute.
+ ++Because every derivation is deterministic and label-fixed, you can independently confirm +that your node's keys really do come from your words: +
+ +-
+
- Independent re-derivation script:
scripts/verify-seed-derivation.pyin the Archipelago source — pure-standard-library Python, no Archipelago code. Paste your mnemonic (on a trusted, offline machine) and it recomputesnode_key,nostr_secretandfips_key, byte-comparing them against/var/lib/archipelago/identity/.
+ - Known-answer tests: the test suite pins the exact expected keys for a fixed test mnemonic; any change to the derivation math, however subtle, turns the build red. +
- Non-determinism regression test: 64 consecutive generated mnemonics are asserted unique — a canary against the "predictable RNG" failure class. +
- Your entropy audit trail:
/var/lib/archipelago/security/csprng-readiness.jsonlrecords, append-only, the kernel randomness verdict at every key-generation event on this node — including the moment your seed was born.
+
+ +
+ Archipelago — Seed & Entropy Guide · reflects the KEY-05 hardened implementation (August 2026) · + see also the LoRa & Mesh Guide +
+ +