Problem or Motivation
A MeshCore identity today is a 64-byte Ed25519 key that exists in exactly one
place: the radio's flash. Lose the radio and the identity is gone — every
contact's address book entry for you is dead, and there is no way back. The only
mitigation is the passphrase-encrypted backup file from #299, which requires the
user to have taken one before the loss.
There is also no way to hold more than one identity. A single operator who wants
a public persona for open channels, an admin persona for repeater work, and a
throwaway persona for a field exercise has to own three radios.
Cryptocurrency solved the first problem with seed phrases and the second with
hierarchical deterministic subkeys. Both map onto MeshCore.
Proposed Solution
Two capabilities, delivered in phases:
- Seed-born identities. Generate an identity from a BIP-39 phrase, write it
to the radio, and let the user keep the phrase offline. A lost radio is then
recoverable onto a replacement with the same public key — every peer's
contact entry keeps working.
- Sub-identities. Derive N unlinkable personas from one phrase via
SLIP-0010 hardened derivation, and let the user switch the live one.
Research findings that shape the work
All verified before filing — see the linked tickets for detail.
- The firmware supports it, on by default.
ENABLE_PRIVATE_KEY_IMPORT=1 and
ENABLE_PRIVATE_KEY_EXPORT=1 are set in [arduino_base] in the firmware's
platformio.ini, which every platform base and every board variant inherits.
The flags are opt-out ("comment these out for more secure firmware").
- The derivation is one-way.
ed25519_create_keypair stores
clamp(SHA-512(seed)) — the expanded key, not the seed. So seed → radio
works and radio → seed does not. An identity the radio generated can never be
given a phrase retroactively; the user must deliberately regenerate.
- The derivation math is verified.
SHA-512(seed) + clamp matches RFC 8032
test vector 1, and the derived public key matches. The app can predict the
public key client-side before writing it, which makes a verify step possible.
- Only the keypair changes on import.
CMD_IMPORT_PRIVATE_KEY calls
saveMainIdentity → resetContacts() → loadContacts() and touches nothing
else. Contacts, channels, node name, lat/lon and radio params all survive.
This is why a persona is more than a key — see the persona-blob ticket.
- Contacts survive but go dead. Peers still hold the old public key, so
their ECDH secret no longer matches. The contact list looks full and nothing
works until every peer re-adds you.
MAX_CONTACTS is 100, so a full contact swap is ~6-20s over BLE — slow
enough to need progress and cancel, not slow enough to rule out per-persona
contact sets.
- No upstream work exists. Zero results across issues, PRs and Discussions
for mnemonic / seed phrase / bip39 / sub identity. We would be first.
Phases
Phase 0 — foundations. Hardware verification, the missing client wrappers,
a capability probe, and one real bug that blocks everything (preferences and
automation rules are lost across any public-key change).
Phase 1 — seed-born identity + recovery. The crypto core, a storage root
that no longer depends on public material, the vault, and the two user-facing
flows. Ships standalone value: recovery from paper.
Phase 2 — sub-identities. SLIP-0010 derivation, the persona blob, the switch
ceremony, and burner mode.
Phase 3 — persona hygiene. Making the live persona unmistakable.
Scope guards
- Fully client-side. No backend, no change to
output: 'export'.
- One new dependency (
@noble/ed25519) — WebCrypto cannot derive an Ed25519
public key from an imported private key.
- The seed vault is a documented exception to the per-radio encryption rule
in AGENTS.md; that file is amended as part of the vault ticket.
Tickets
Phase 0 — foundations
Phase 1 — seed-born identity + recovery
Phase 2 — sub-identities
Phase 3 — persona hygiene
Ordering notes
Problem or Motivation
A MeshCore identity today is a 64-byte Ed25519 key that exists in exactly one
place: the radio's flash. Lose the radio and the identity is gone — every
contact's address book entry for you is dead, and there is no way back. The only
mitigation is the passphrase-encrypted backup file from #299, which requires the
user to have taken one before the loss.
There is also no way to hold more than one identity. A single operator who wants
a public persona for open channels, an admin persona for repeater work, and a
throwaway persona for a field exercise has to own three radios.
Cryptocurrency solved the first problem with seed phrases and the second with
hierarchical deterministic subkeys. Both map onto MeshCore.
Proposed Solution
Two capabilities, delivered in phases:
to the radio, and let the user keep the phrase offline. A lost radio is then
recoverable onto a replacement with the same public key — every peer's
contact entry keeps working.
SLIP-0010 hardened derivation, and let the user switch the live one.
Research findings that shape the work
All verified before filing — see the linked tickets for detail.
ENABLE_PRIVATE_KEY_IMPORT=1andENABLE_PRIVATE_KEY_EXPORT=1are set in[arduino_base]in the firmware'splatformio.ini, which every platform base and every board variant inherits.The flags are opt-out ("comment these out for more secure firmware").
ed25519_create_keypairstoresclamp(SHA-512(seed))— the expanded key, not the seed. So seed → radioworks and radio → seed does not. An identity the radio generated can never be
given a phrase retroactively; the user must deliberately regenerate.
SHA-512(seed)+ clamp matches RFC 8032test vector 1, and the derived public key matches. The app can predict the
public key client-side before writing it, which makes a verify step possible.
CMD_IMPORT_PRIVATE_KEYcallssaveMainIdentity→resetContacts()→loadContacts()and touches nothingelse. Contacts, channels, node name, lat/lon and radio params all survive.
This is why a persona is more than a key — see the persona-blob ticket.
their ECDH secret no longer matches. The contact list looks full and nothing
works until every peer re-adds you.
MAX_CONTACTSis 100, so a full contact swap is ~6-20s over BLE — slowenough to need progress and cancel, not slow enough to rule out per-persona
contact sets.
for mnemonic / seed phrase / bip39 / sub identity. We would be first.
Phases
Phase 0 — foundations. Hardware verification, the missing client wrappers,
a capability probe, and one real bug that blocks everything (preferences and
automation rules are lost across any public-key change).
Phase 1 — seed-born identity + recovery. The crypto core, a storage root
that no longer depends on public material, the vault, and the two user-facing
flows. Ships standalone value: recovery from paper.
Phase 2 — sub-identities. SLIP-0010 derivation, the persona blob, the switch
ceremony, and burner mode.
Phase 3 — persona hygiene. Making the live persona unmistakable.
Scope guards
output: 'export'.@noble/ed25519) — WebCrypto cannot derive an Ed25519public key from an imported private key.
in
AGENTS.md; that file is amended as part of the vault ticket.Tickets
Phase 0 — foundations
fix(storage): preferences and automation rules are lost across an identity change (live bug today)feat(protocol): add client wrappers for advert name, lat/lon and contact upsertfeat(protocol): probe private-key import/export capability at connectPhase 1 — seed-born identity + recovery
feat(identity): add BIP-39 and seed-to-Ed25519 derivation corefeat(storage): derive the encryption root from the seed instead of channel secretsfeat(storage): add a passphrase-encrypted identity vaultfeat(ui): add a regenerate-identity-under-a-recovery-phrase wizardfeat(ui): add restore-identity-from-recovery-phrasePhase 2 — sub-identities
feat(identity): add SLIP-0010 hardened sub-identity derivationfeat(storage): add the per-persona radio-state blob with capture and applyfeat(ui): add the identity list and persona switch ceremonyfeat(identity): add burner personas with zero persistencedocs: propose the sub-identity derivation path upstream (parallel, blocks nothing)Phase 3 — persona hygiene
feat(ui): make the live persona unmistakableOrdering notes