Your node's seed & entropy
How 32 random bytes become every key this node owns — where the randomness comes from, what protects it, and exactly what your 24 words can and cannot bring back.
Overview
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 of randomness drawn once, shown to you once as a 24-word recovery phrase, and never stored in raw form anywhere.
Think of the seed as an acorn. Every branch of the tree — your identity, your wallet, your mesh radio's name — grows from it in a fixed, repeatable pattern. Plant the same acorn on new hardware by typing your 24 words and the same tree grows back, branch for branch. That is why those words are the most valuable thing your node ever shows you, and why anyone who copies them owns your tree.
Linux kernel CSPRNG (interrupt timing, jitter, CPU RNG) │ │ getrandom(2) — via an explicitly named OsRng, nothing else allowed ▼ 32 bytes raw entropy ──▶ degenerate-draw check ──▶ refuse & wipe if suspicious │ │ BIP-39 encoding ▼ 24-word recovery phrase ←── the only form you ever see or back up │ │ PBKDF2-HMAC-SHA512 × 2048 ▼ 64-byte master seed ←── lives only in RAM, never written to disk │ ├─ HKDF "archipelago/node/ed25519/v1" ──▶ Node identity key + DID ├─ HKDF "archipelago/nostr-node/…/v1" ──▶ Node Nostr key (npub) ├─ HKDF "archipelago/fips/secp256k1/v1" ──▶ FIPS mesh transport key ├─ HKDF "archipelago/identity/{i}/…/v1" ──▶ Personal identities ├─ BIP-32 m/44'/1237'/0'/0/{i} (NIP-06) ──▶ Personal Nostr keys ├─ HKDF "archipelago/lnd/entropy/v1" ──▶ Lightning entropy → aezeed └─ BIP-32 m/84'/0'/0' ──▶ Bitcoin xprv (dormant) │ │ and from the node key, second-order: ▼ ├─ Reticulum / LXMF mesh identity ├─ Message-store + contacts encryption └─ Credential-store key
One rule to remember
If it is on the diagram above, your 24 words rebuild it from scratch, on any hardware, forever. If it is not on the diagram — session tokens, app passwords, WireGuard keys, Lightning channel state — it is independent randomness, protected by other backups.
How it's created
One named source. No mixing. No silent defaults.
Computers cannot invent randomness — they collect it. The Linux kernel constantly harvests unpredictable physical noise (the exact nanosecond a network card interrupts, timing jitter between CPU cores, the CPU's hardware random generator) into a cryptographic pool. Archipelago rolls its dice by asking that pool directly, and only that pool. There is deliberately no blending of other sources: a single, named, well-studied source is auditable, whereas a blend is a place for bugs to hide.
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 CSPRNG (same source as /dev/urandom,
but immune to file-descriptor exhaustion and chroot tricks).
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 raw bytes
- 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 generates key material with a default or implicit RNG.
- The RNG type is compiler-enforced. Key generation only accepts RNGs on a sealed allowlist (
KeyGenRng) whose single production member isOsRng. - The buffer is zeroized on every path, success or failure.
The words are then stretched into the 64-byte master seed by standard BIP-39: PBKDF2-HMAC-SHA512, 2048 rounds, empty passphrase. That 64-byte value is a 512-bit expansion of the same 256 bits of entropy — not extra randomness. It exists only in memory, is recomputed from the words when needed, and never touches disk.
When the seed is born
At onboarding — not at first boot.
- First boot: a placeholder. A freshly flashed node boots with a random temporary identity key so services can start. It is not seed-derived and is about to be thrown away.
- Onboarding: the real draw. At the "Recovery phrase" step, the
seed.generateRPC performs the guarded 32-byte draw and shows you the 24 words. - Derivation. Node key, DID, Nostr key, FIPS mesh key and your first identity are derived and written to
/var/lib/archipelago/identity/at mode 0600, overwriting the placeholder. - Password setup: the backup is sealed. The words are encrypted under your login password and stored as
master_seed.enc, so you can reveal them again later.
Generation is idempotent for 10 minutes and serialised behind a lock: a browser refresh returns the same words rather than minting a second seed.
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. 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 checked for three 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 node probes whether the kernel pool is fully initialised and appends the verdict to an append-only log at security/csprng-readiness.jsonl (0600). You can audit the entropy conditions your seed was born under, forever.
4 Build-time lint bans
CI bans rand::random() and rand::thread_rng() across the workspace — the two convenient entry points behind real-world wallet disasters. 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 does not linger in freed RAM or crash dumps.
Why so paranoid about one function?
In 2026 a well-known hardware wallet shipped a bug where a refactor quietly switched seed generation to a predictable random source — no error, no warning, and seeds that looked perfectly normal. Predictable randomness is invisible: the words look random, the wallet works, and months later someone who can predict the generator drains it. Archipelago's answer is to make that entire class of bug impossible to compile, and to log the health of the random pool at the moment your seed was created.
What the degenerate check does and doesn't do
It is deliberately closed-form: it recognises exactly three catastrophic shapes (all-zero, all-identical, ±1 counter). It is not a statistical entropy estimator — those cannot distinguish good randomness from a cleverly broken RNG and add false positives. The security load is carried by guardrails 1, 3 and 4; this is a tripwire for total RNG failure, such as a buffer that was never filled.
Recent hardening
This system was audited and rebuilt in early August 2026. The headline finding: the mnemonic library 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 with no diff in Archipelago's own code.
| Date | Change |
|---|---|
| Jul 30 | Kernel CSPRNG readiness probe; non-determinism regression test (64 consecutive mnemonics must 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 migrated to explicit OsRng, including AEAD nonces and ecash key material. |
| Aug 2 | Onboarding RPCs gated — seed.restore now refuses on a provisioned node (previously an unauthenticated restore could hijack a live node; fixed before any release shipped it). |
| Aug 2 | KEY-05 layer landed: sealed allowlist, guarded draws, readiness ledger, clippy bans, supply-chain pinning of the rand crate. |
| Aug 2 | Legacy Bitcoin Core wallet-import path deleted — the master xprv is no longer handed to any external wallet process. |
Stored on disk
The words, encrypted — and the derived keys. Never the raw seed.
| File | Contents | Protection |
|---|---|---|
identity/master_seed.enc | Your 24 words, encrypted | Argon2(login password) + ChaCha20-Poly1305, 0600 |
identity/node_key | Node Ed25519 identity key | 0600, seed-derived |
identity/nostr_secret | Node Nostr keypair | 0600, seed-derived |
identity/fips_key | FIPS mesh transport key (bech32 nsec) | 0600, seed-derived |
identity/identity_index | Next unused derivation index | Plain integer, not secret |
identities/<uuid>.json | Identity records: keys + metadata | 0600; keys seed-derived, metadata is not |
identity/lnd_aezeed.enc | Lightning wallet's own seed | Encrypted under the LND wallet password |
security/csprng-readiness.jsonl | Append-only entropy audit trail | 0600; outside identity/ so restores never touch it |
The raw seed never touches disk
What is stored is the encrypted words and the derived keys. The 64-byte master seed is recomputed in RAM from the words when needed and wiped afterwards.
The encrypted envelope
login password ──▶ Argon2id (memory-hard) ──▶ 256-bit file key ▲ 16-byte random salt 24 words ──▶ ChaCha20-Poly1305 (authenticated, 12-byte random nonce) │ ▼ ┌───────────┬────────────┬───────────────────────────┐ │ salt (16) │ nonce (12) │ ciphertext + auth tag │ = master_seed.enc └───────────┴────────────┴───────────────────────────┘
Your words are locked in a digital safe whose combination is your login password, run through a deliberately slow, memory-hungry grinder (Argon2) so guessing billions of passwords per second is impractical even for someone who steals the file. The authentication tag means the safe also notices tampering: a modified file fails loudly rather than yielding wrong words.
Revealing the words later (Settings → Backup → Reveal) requires an authenticated session, re-entering your password, and your 2FA code if enabled. It is rate-limited, and the words go only to your browser — never to logs.
Derivation tree
Every key, its exact derivation, and where it lands.
The node never uses the master seed directly as a key. It uses HKDF — think of a locksmith who, given one master blank and a label ("node key", "mesh key", "Lightning entropy"), cuts a completely different, unrelated key for each label. Knowing one cut key tells you nothing about the others or about the blank. The labels are fixed strings baked into the code, which is what lets the identical tree regrow on new hardware.
| Key | Method | Label / path |
|---|---|---|
| Node identity (Ed25519) — signs everything, forms your DID | HKDF-SHA256 | archipelago/node/ed25519/v1 |
| Node Nostr key — the node's npub | HKDF-SHA256 | archipelago/nostr-node/secp256k1/v1 |
| FIPS mesh transport key | HKDF-SHA256 | archipelago/fips/secp256k1/v1 |
| Personal identity #i (Ed25519) | HKDF-SHA256 | archipelago/identity/{i}/ed25519/v1 |
| Personal Nostr key #i — NIP-06 standard, portable to other Nostr apps | BIP-32 | m/44'/1237'/0'/0/{i} |
| Lightning wallet entropy — 16 bytes | HKDF-SHA256 | archipelago/lnd/entropy/v1 |
| Bitcoin BIP-84 xprv — dormant, reserved for a future cold vault | BIP-32 | m/84'/0'/0' |
| Release-root signing key — never on a node; derived offline by the publisher | HKDF-SHA256 | archipelago/release/root/ed25519/v1 |
All HKDF derivations are HKDF-SHA256 with a distinct, versioned label — the /v1
suffix means a future migration can introduce /v2 without ambiguity. Personal
Nostr keys deliberately use the NIP-06 standard path instead of HKDF, so the same 24 words
typed into any NIP-06 Nostr client reproduce the same npub: your social identity is portable
beyond Archipelago.
The Lightning special case
master seed ──HKDF──▶ 16 bytes ──▶ LND generates its own "aezeed" ──▶ wallet │ │ ⚠ one-way: the aezeed cannot be │ recomputed from your 24 words ▼ captured ONCE at init, stored encrypted as identity/lnd_aezeed.enc
LND uses its own seed format, aezeed, which is not BIP-39. Archipelago derives deterministic entropy from your master seed and hands it to LND at wallet creation — but LND wraps it with its own internal salt, so the resulting aezeed cannot be re-derived from your 24 words afterwards. The node captures it once and stores it encrypted alongside your other identity files.
Back up the Lightning seed separately
Your 24 words restore your node identity and on-chain derivations, but not an already-initialised Lightning wallet, and never off-chain channel balances (those need channel backups, as Lightning requires by design). Treat the aezeed in the Lightning backup screen as a second phrase worth writing down. It restores into LND-based wallets such as Zeus, Blixt or another Archipelago node — hardware wallets cannot import it.
Second-order keys
Some subsystems derive from the node identity key rather than the master seed
directly. Since the node key is itself seed-derived, these still regrow from your words:
words → master seed → node key → subsystem key. Each prefixes a unique fixed
string before hashing (domain separation), so compromising one never exposes another.
| Subsystem | Derivation from node_key |
|---|---|
| Reticulum / LXMF mesh identity (LoRa long-range mesh) | HKDF-SHA256, salt archipelago-reticulum-identity-v1, separate X25519 + Ed25519 labels — a stable address that survives reinstalls |
| Message store (chats at rest) | SHA-256("archipelago-message-store-v1" ‖ node_key) |
| Mesh contacts | 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
Plenty of secrets are freshly random instead. That is intentional: things that should die with a session, rotate freely, or belong to a third-party app must not be recoverable from your words.
Ephemeral by design
Session tokens, device pairing tokens, federation invites, TOTP secrets and backup codes, all encryption nonces, X3DH ephemeral mesh keys, anonymous marketplace and 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 keypairs (via wg genkey), SSH host keys and the TLS certificate (created by the installer image at first boot), the machine-id.
Opt-outs from derivability
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 your words, and says so.
Failures
What happens when something goes wrong, at every stage.
| Scenario | Behaviour | Outcome |
|---|---|---|
| RNG returns a degenerate pattern | Draw refused and wiped, never retried; error logged; onboarding fails loudly | No seed created |
| Kernel pool not yet initialised | getrandom(2) blocks until seeded — an unseeded pool cannot produce a seed. The probe logs a warning and records the verdict |
Waits, then proceeds |
| Onboarding page refreshed mid-generation | Same words returned for 10 minutes, mutex-serialised; no second seed can be minted | Idempotent |
master_seed.enc missing |
Node runs normally — derived keys are already on disk. Only Reveal and future re-derivation are unavailable, and the UI says so | Degraded, functional |
| Seed file corrupt, or wrong password | Authenticated decryption fails closed with an explicit error — no fallback, no partial output, no auto-regeneration | Fails loudly |
| Restore attempted on a provisioned node | The onboarding gate refuses identity-mutating RPCs once set up — a live node cannot be hijacked or accidentally re-seeded | Refused |
| Legacy or corrupt FIPS key format | Self-heals: the legacy raw-byte format is detected and migrated in place to bech32 | Auto-migrated |
| Readiness-ledger write fails | Warns and continues — the audit trail is best-effort and can never block key generation | Non-blocking |
The one true single point of failure is you
Every software failure above fails safe. The only unrecoverable scenario is losing the 24 words and the node's disk together. Write the words down, store them offline, and never type them into anything except a node you are restoring. Anyone holding them can rebuild your entire identity tree — which is exactly what makes them a perfect backup and a perfect target.
Restore
Typing 24 words into a fresh node, step by step.
- Gate check. Restore only proceeds on an un-onboarded node. This gate is load-bearing and runs before anything else.
- Validation. Exactly 24 words, checked against the BIP-39 wordlist and its checksum — a typo is caught here, before anything is written.
- Identity regrowth. Node key, DID, node Nostr key and FIPS mesh key are re-derived byte-identically, because the HKDF labels are fixed.
- Personal identity #0. The index resets to 0 and your default identity (Ed25519 + NIP-06 Nostr key) is recreated. Further seed-derived identities re-derive as the index walks forward, but their names and avatars were metadata, not key material.
- Mesh reactivation. FIPS auto-activation starts in the background; the Reticulum identity re-derives from the restored node key, so your LXMF address returns too.
- Password and re-seal. Setting the new login password re-encrypts the words into a fresh
master_seed.enc, so Reveal works on the restored node.
What comes back — and what doesn't
Restored by the words
Node identity and DID · node npub · FIPS mesh key · Reticulum/LXMF address · personal identity keys and npubs · message-store, contacts and credential encryption keys · the dormant Bitcoin xprv · the ability to reveal the phrase again.
Needs its own backup
Lightning wallet (aezeed — one-way gate) and channel state · chat history and app data (node backup) · identity names and avatars · app secrets, which regenerate on reinstall.
Gone by design
Sessions and device pairings (log in, re-pair) · 2FA secret (re-enrol) · WireGuard peers (re-pair) · rotated-away node keys · anonymous throwaway Nostr keys.
SeedQR
Wherever the phrase is shown, a QR tab sits beside the words. For the BIP-39 phrase the default is SeedQR: each word becomes its 4-digit position in the official wordlist (24 words → 96 digits) as a compact numeric QR. Passport, SeedSigner and Keystone import this directly, so you can move your on-chain identity to cold storage without typing. A plain-text QR fallback exists for wallets that read the phrase as text.
- The QR holds exactly the same secret as the words — treat a printout or screenshot identically.
- The Lightning aezeed is never SeedQR-encoded: it is not BIP-39, hardware wallets cannot import it, and pretending otherwise would be dishonest. It gets a plain-text QR with an explanation.
- Restore is by typed or pasted words; there is no camera-based SeedQR scanner on the restore path today.
Verify it yourself
Don't trust — recompute.
Because every derivation is deterministic and label-fixed, you can independently confirm that this node's keys really do come from your words:
- Independent re-derivation:
scripts/verify-seed-derivation.pyin the Archipelago source — pure standard-library Python, no Archipelago code. On a trusted offline machine it recomputesnode_key,nostr_secretandfips_keyfrom your mnemonic and byte-compares them against/var/lib/archipelago/identity/. - Known-answer tests: the test suite pins the exact expected keys for a fixed test mnemonic, so any change to the derivation math turns the build red.
- Non-determinism test: 64 consecutive generated mnemonics are asserted unique — a canary against the predictable-RNG failure class.
- Your own 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.
Honest edges
The audit that produced this system also tracked what it did not fix. Naming the edges is part of the point:
- The words cross the RPC boundary. During onboarding the phrase travels to your browser to be displayed, sits in session storage for the wizard's duration, and is held in server memory for the 10-minute idempotence window — the price of a refresh-proof, display-once flow.
- Argon2 uses library defaults (≈19 MiB, 2 passes) rather than the heavier profile the design doc specifies. Still memory-hard; scheduled for tightening.
- 2FA backup codes carry slight modulo bias — cosmetically imperfect, cryptographically irrelevant at their length, queued for cleanup.
- The lint ban covers the main workspace, but one small helper crate outside it is not reached yet.
- Best-effort sealing: if writing
master_seed.encfails during setup, the node continues (keys exist, only Reveal is lost). Whether that should fail loudly instead is under review.
The whole story in one paragraph
Your node asked the Linux kernel for 32 bytes of hardware-grade randomness through a single, named, compiler-enforced channel; refused to proceed unless the bytes looked alive; wrote down the health of the random pool as evidence; turned the bytes into 24 words it showed you exactly once; locked an encrypted copy behind your password; and then grew every identity and key it owns from those words along fixed, versioned, independently verifiable paths — so the words in your drawer are, and will remain, a complete blueprint of who your node is.