Skip to content

feat(identity): add BIP-39 and seed-to-Ed25519 derivation core #358

Description

@kNoAPP

Part of #353. First implementation ticket of phase 1. Pure module, no UI.

Problem or Motivation

Everything else in the epic composes on top of one capability the app does not have: turning a human-writable phrase into the exact 64 bytes MeshCore's CMD_IMPORT_PRIVATE_KEY expects, and predicting the public key the radio will derive from them.

Getting this wrong is not a recoverable bug. A derivation that differs by one byte from the firmware's produces a valid-looking key that the radio either rejects outright or accepts as a different identity than the phrase promises — which means the user's paper backup silently does not restore what they think it does.

Proposed Solution

A new pure module under lib/identity/, with no React and no store access, exporting the derivation and its inverse-checks.

What the firmware expects

LocalIdentity(RNG*) calls ed25519_create_keypair(pub_key, prv_key, seed) (orlp/ed25519), which is standard Ed25519 key generation:

prv = SHA-512(seed)            // 64 bytes
prv[0]  &= 248
prv[31] &= 63
prv[31] |= 64
pub = scalarmult_base(prv[0..32])

The stored prv_key is the expanded key, not the seed — LocalIdentity::writeTo emits 64 bytes with no public key following, and an importing radio re-derives it via ed25519_derive_pub. The seed is therefore unrecoverable from any key the radio holds, which is why this feature can only ever move seed to radio and never the reverse.

This has been verified against RFC 8032 test vector 1: the expanded key and derived public key both match. Use that vector as a fixture.

Scope

  • BIP-39: the 2048-word English wordlist as a bundled module (not fetched — the app is offline-first and static-exported), mnemonic generation, mnemonic-to-entropy with checksum validation, and PBKDF2-HMAC-SHA512 to the 64-byte BIP-39 seed. Take the first 32 bytes as the Ed25519 seed.
  • Expansion: SHA-512 and clamping as above, via WebCrypto.
  • Public key prediction: scalarmult_base. This is the one step WebCrypto cannot do — importKey accepts a PKCS#8 Ed25519 private key but offers no way to export the corresponding public key, so a dependency is required. Add @noble/ed25519 (zero-dep, audited, ~5KB).
  • Validity check mirroring the firmware's validatePrivateKey: reject a derived key whose public key starts with 0x00 or 0xFF. The firmware also runs an ECDH round-trip against a hardcoded test keypair, which any correctly clamped key passes.

Non-goals

Hierarchical derivation is deliberately out of scope — phase 1 uses a single seed-born identity and needs no path. SLIP-0010 and the index skip rule are their own ticket.

Verification

Testable headlessly with node --experimental-strip-types and a resolver hook mapping @/ to the repo root — no hardware and no browser. Cover at minimum:

  • RFC 8032 vector 1: seed to expanded key, and seed to public key.
  • BIP-39 round trip: mnemonic to entropy to mnemonic.
  • Checksum rejection: a phrase with one word altered must fail, not silently derive a different identity.
  • The 0x00 / 0xFF public key rejection path.

cspell will flag the wordlist; add that file to the ignore list rather than the dictionary.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions