# BIP-39 Master Seed — Unified Key Derivation for Archipelago ## Context Archipelago's current identity system is broken: - Node key generated randomly at boot, before the user exists - Each identity creates independent random Ed25519 + secp256k1 keys - ADR-008 says "both keys derived from same master seed" but code doesn't do this - Backup only covers the node key, not identity keys - No seed phrase — backup is an opaque encrypted blob with a user passphrase - Restore path disabled ("Coming Soon") - No connection between node identity and Bitcoin/LND wallet keys **Goal:** One 24-word BIP-39 seed phrase derives ALL keys. User writes down 24 words, can recover everything on a fresh install. --- ## Derivation Scheme ``` BIP-39 Mnemonic (24 words, 256-bit entropy) -> PBKDF2-HMAC-SHA512 (2048 rounds, empty passphrase) -> Master Seed (64 bytes) | +-- HKDF-SHA256(seed, info="archipelago/node/ed25519/v1") -> Node Ed25519 key -> did:key +-- HKDF-SHA256(seed, info="archipelago/nostr-node/secp256k1/v1") -> Node Nostr key +-- HKDF-SHA256(seed, info="archipelago/identity/{i}/ed25519/v1") -> Identity i Ed25519 -> did:key +-- BIP-32 m/44'/1237'/0'/0/{i} -> Identity i Nostr key (NIP-06) +-- BIP-32 m/84'/0'/0' -> Bitcoin Core wallet (native segwit) +-- HKDF-SHA256(seed, info="archipelago/lnd/entropy/v1") -> 16 bytes -> LND aezeed entropy ``` --- ## Phase 1: Seed Module (foundation) ### New crates in `core/archipelago/Cargo.toml` ```toml bip39 = "=2.1.0" bitcoin = { version = "=0.32.5", features = ["rand-std"] } ``` ### New file: `core/archipelago/src/seed.rs` **`MasterSeed` struct** — wraps `Zeroizing<[u8; 64]>`, implements `ZeroizeOnDrop` Functions: - `MasterSeed::generate() -> (Mnemonic, MasterSeed)` — 256-bit entropy, 24 words - `MasterSeed::from_mnemonic(mnemonic) -> MasterSeed` — for restore - `MasterSeed::from_mnemonic_words(words: &str) -> Result<(Mnemonic, MasterSeed)>` — parse + validate - `derive_node_ed25519(&MasterSeed) -> SigningKey` — HKDF with info `archipelago/node/ed25519/v1` - `derive_identity_ed25519(&MasterSeed, index: u32) -> SigningKey` — HKDF with info `archipelago/identity/{index}/ed25519/v1` - `derive_nostr_identity_key(&MasterSeed, index: u32) -> nostr_sdk::Keys` — BIP-32 `m/44'/1237'/0'/0/{index}` - `derive_node_nostr_key(&MasterSeed) -> nostr_sdk::Keys` — HKDF with info `archipelago/nostr-node/secp256k1/v1` - `derive_bitcoin_xprv(&MasterSeed) -> Xpriv` — BIP-32 `m/84'/0'/0'` - `derive_lnd_entropy(&MasterSeed) -> [u8; 16]` — HKDF with info `archipelago/lnd/entropy/v1` - `save_seed_encrypted(data_dir, mnemonic, passphrase)` — Argon2+ChaCha20 to `master_seed.enc` - `load_seed_encrypted(data_dir, passphrase) -> Mnemonic` - `seed_exists(data_dir) -> bool` - `save_identity_index(data_dir, next_index: u32)` / `load_identity_index(data_dir) -> u32` Security: Never log seed/mnemonic. All seed types implement `ZeroizeOnDrop`. File permissions 0o600. Existing building blocks to reuse: - `mesh/crypto.rs:hkdf_sha256()` / `hkdf_sha256_32()` — already implemented - `backup/identity.rs` encryption pattern — Argon2+ChaCha20 (reuse for `save_seed_encrypted`) - `ed25519-dalek`, `sha2`, `hmac`, `hkdf`, `zeroize` — all in Cargo.toml already --- ## Phase 2: Onboarding UI ### New Vue views: **`OnboardingSeedGenerate.vue`** — calls `seed.generate`, displays 24 words in grid, "I wrote these down" checkbox **`OnboardingSeedVerify.vue`** — picks 4 random word positions, user types them back, calls `seed.verify`, shows DID + npub on success **`OnboardingSeedRestore.vue`** — 24 input fields with BIP-39 wordlist autocomplete, calls `seed.restore` ### New onboarding flow: ``` Intro -> Options (Fresh / Restore) -> [branch] FRESH: SeedGenerate -> SeedVerify -> Identity (name/purpose) -> Done RESTORE: SeedRestore -> Done ``` ### Router changes (`neode-ui/src/router/index.ts`): - Add routes: `onboarding/seed`, `onboarding/seed-verify`, `onboarding/seed-restore` - Remove: `onboarding/did`, `onboarding/backup`, `onboarding/verify` - Enable Restore path in `OnboardingOptions.vue` ### RPC client (`neode-ui/src/api/rpc-client.ts`): - `generateSeed()`, `verifySeed()`, `restoreSeed()`, `saveSeedEncrypted()`, `seedStatus()` --- ## Phase 3: Backend Integration ### `identity.rs` — add `NodeIdentity::from_seed(identity_dir, &MasterSeed)` - Derives Ed25519 node key via `seed::derive_node_ed25519()` - Writes to `node_key` / `node_key.pub` (same format as today) - Existing `load_or_create()` unchanged (loads from disk, works for both seed-derived and legacy keys) ### `identity_manager.rs` — seed-aware `create()` - When seed available: derive Ed25519 from `derive_identity_ed25519(seed, index)`, Nostr from `derive_nostr_identity_key(seed, index)` - Increment and persist `identity_index` - Add `derivation_index: Option` to `IdentityFile` (serde default, backward-compatible) - When no seed (legacy): fall back to current random generation ### `server.rs` — startup flow: ``` seed exists + node_key exists -> Normal seed-backed operation no seed + node_key exists -> Legacy node, show migration prompt no seed + no node_key -> Fresh install, await onboarding seed exists + no node_key -> Re-derive from seed (recovery) ``` - Add `seed_backed: bool` to `ServerInfo` ### New RPC endpoints in `api/rpc/seed.rs`: - `seed.generate` — generates mnemonic, derives & writes node keys, returns words (onboarding only, unauth) - `seed.verify` — validates user re-entered correct words (onboarding only) - `seed.restore` — accepts 24 words, derives all keys, writes to disk (onboarding only, unauth) - `seed.save-encrypted` — encrypts mnemonic to `master_seed.enc` (optional convenience) - `seed.status` — returns `{ has_seed, is_legacy, identity_count, next_index }` - `seed.derive-lnd-entropy` — password-protected, returns 16 bytes for LND wallet init - `seed.derive-bitcoin-xprv` — password-protected, returns xprv for Bitcoin Core import In-memory mnemonic between `seed.generate` and `seed.verify`: held in `Mutex>>` with 10-minute auto-clear timeout. --- ## Phase 4: Bitcoin/LND Integration ### LND wallet from seed: - `lnd.init-wallet-from-seed` handler — derives 16-byte entropy, calls LND REST `POST /v1/initwallet` with `seed_entropy` - Triggered during LND first-install flow ### Bitcoin Core wallet from seed: - `bitcoin.init-wallet-from-seed` handler — derives BIP-84 xprv, calls `createwallet` + `importdescriptors` via Bitcoin Core RPC - Triggered during Bitcoin Core first-install flow Both endpoints require password re-verification. --- ## Phase 5: Migration & Polish ### Legacy node migration: - Detect legacy nodes (node_key exists, no master_seed.enc) - Settings page shows prompt: "Set up seed phrase to protect future identities" - Existing keys preserved — only NEW identities use seed derivation - Optional full migration (`seed.migrate-legacy`) can be added later ### Cleanup: - Remove old `OnboardingDid.vue`, `OnboardingBackup.vue`, `OnboardingVerify.vue` - Update Settings backup section to show seed status - Update ADR-008 to reflect implementation matches description --- ## File Layout After Implementation ``` {data_dir}/identity/ node_key # 32 bytes Ed25519 secret (derived from seed or legacy) node_key.pub # 32 bytes Ed25519 public master_seed.enc # NEW: encrypted mnemonic (optional convenience backup) identity_index # NEW: next derivation index (plain text integer) {data_dir}/identities/ {uuid}.json # Same format + optional derivation_index field ``` --- ## Critical Files to Modify | File | Change | |------|--------| | `core/archipelago/Cargo.toml` | Add `bip39`, `bitcoin` crates | | `core/archipelago/src/seed.rs` | **NEW** — all seed logic | | `core/archipelago/src/identity.rs` | Add `from_seed()` constructor | | `core/archipelago/src/identity_manager.rs` | Seed-aware `create()`, add `derivation_index` | | `core/archipelago/src/server.rs` | Startup state detection (seed/legacy/fresh) | | `core/archipelago/src/api/rpc/seed.rs` | **NEW** — seed RPC handlers | | `core/archipelago/src/api/rpc/dispatcher.rs` | Register seed.* endpoints | | `neode-ui/src/views/OnboardingSeedGenerate.vue` | **NEW** — show 24 words | | `neode-ui/src/views/OnboardingSeedVerify.vue` | **NEW** — verify written words | | `neode-ui/src/views/OnboardingSeedRestore.vue` | **NEW** — enter 24 words to restore | | `neode-ui/src/views/OnboardingOptions.vue` | Enable Restore path | | `neode-ui/src/router/index.ts` | Update onboarding routes | | `neode-ui/src/api/rpc-client.ts` | Add seed RPC methods | --- ## Verification 1. **Unit tests**: Deterministic derivation (same mnemonic -> same keys), invalid mnemonic rejection, index increment, zeroization 2. **Integration**: Fresh install flow end-to-end, restore flow (generate on node A, enter words on node B, verify same DID/npub) 3. **Security**: Grep seed.rs for tracing macros that interpolate seed vars, verify file permissions 4. **LND**: Derive entropy, init wallet, verify deterministic aezeed 5. **Bitcoin Core**: Derive xprv, import descriptors, verify addresses match 6. **Legacy**: Existing node without seed starts normally, can still create identities 7. **Type check**: `cd neode-ui && npx vue-tsc -b --noEmit`