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.

256-bit entropy BIP-39 · 24 words HKDF-SHA256 Kernel CSPRNG only KEY-05 hardened

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 is OsRng.
  • 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.

  1. 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.
  2. Onboarding: the real draw. At the "Recovery phrase" step, the seed.generate RPC performs the guarded 32-byte draw and shows you the 24 words.
  3. 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.
  4. 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.

DateChange
Jul 30Kernel CSPRNG readiness probe; non-determinism regression test (64 consecutive mnemonics must be unique).
Jul 31Full entropy audit published (findings F-01…F-13).
Aug 1The 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 2Audit widened: 43 defaulted-RNG call sites across 15 files migrated to explicit OsRng, including AEAD nonces and ecash key material.
Aug 2Onboarding 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 2KEY-05 layer landed: sealed allowlist, guarded draws, readiness ledger, clippy bans, supply-chain pinning of the rand crate.
Aug 2Legacy 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.

FileContentsProtection
identity/master_seed.encYour 24 words, encryptedArgon2(login password) + ChaCha20-Poly1305, 0600
identity/node_keyNode Ed25519 identity key0600, seed-derived
identity/nostr_secretNode Nostr keypair0600, seed-derived
identity/fips_keyFIPS mesh transport key (bech32 nsec)0600, seed-derived
identity/identity_indexNext unused derivation indexPlain integer, not secret
identities/<uuid>.jsonIdentity records: keys + metadata0600; keys seed-derived, metadata is not
identity/lnd_aezeed.encLightning wallet's own seedEncrypted under the LND wallet password
security/csprng-readiness.jsonlAppend-only entropy audit trail0600; 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.

KeyMethodLabel / path
Node identity (Ed25519) — signs everything, forms your DIDHKDF-SHA256archipelago/node/ed25519/v1
Node Nostr key — the node's npubHKDF-SHA256archipelago/nostr-node/secp256k1/v1
FIPS mesh transport keyHKDF-SHA256archipelago/fips/secp256k1/v1
Personal identity #i (Ed25519)HKDF-SHA256archipelago/identity/{i}/ed25519/v1
Personal Nostr key #i — NIP-06 standard, portable to other Nostr appsBIP-32m/44'/1237'/0'/0/{i}
Lightning wallet entropy — 16 bytesHKDF-SHA256archipelago/lnd/entropy/v1
Bitcoin BIP-84 xprv — dormant, reserved for a future cold vaultBIP-32m/84'/0'/0'
Release-root signing key — never on a node; derived offline by the publisherHKDF-SHA256archipelago/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.

SubsystemDerivation 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 contactsSHA-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.

ScenarioBehaviourOutcome
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.

  1. Gate check. Restore only proceeds on an un-onboarded node. This gate is load-bearing and runs before anything else.
  2. Validation. Exactly 24 words, checked against the BIP-39 wordlist and its checksum — a typo is caught here, before anything is written.
  3. Identity regrowth. Node key, DID, node Nostr key and FIPS mesh key are re-derived byte-identically, because the HKDF labels are fixed.
  4. 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.
  5. 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.
  6. 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.py in the Archipelago source — pure standard-library Python, no Archipelago code. On a trusted offline machine it recomputes node_key, nostr_secret and fips_key from 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.jsonl records, 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.enc fails 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.