feat(rust,nodejs): BIP-39 mnemonic phrases for Ed25519 identities
Adds the canonical wallet-style backup form (12 or 24 BIP-39 English
words) to both implementations. Wire-compatible — bit-identical seed
derivation across Rust and Node.
Semantics:
• 24 words ↔ 32 bytes of entropy ↔ Ed25519 seed (bijection).
Phrase ↔ seed round-trips exactly.
• 12 words → 16 bytes of entropy → seed via
SHA-256("kez-bip39-12-v1" || entropy). Deterministic but one-way;
you can't recover a 12-word phrase from a seed.
The 12-word case is KEZ-specific (not interoperable with hardware-
wallet BIP-32 derivations). The 24-word case is. Both use the BIP-39
English wordlist so users can paper-back-up alongside other wallets.
We deliberately do NOT use BIP-39's PBKDF2 to_seed(passphrase) — that
produces a 64-byte seed for BIP-32 hierarchical derivation, which is
the wrong primitive for KEZ's single-identity-per-phrase model.
Rust (kez-core):
• New mod mnemonic with MnemonicWords, generate_mnemonic,
seed_from_mnemonic, mnemonic_from_seed_24.
• Ed25519Secret::{from_mnemonic, generate_with_mnemonic}.
• Dep: bip39 v2.0 with the `rand` feature for OS-RNG generation.
• 9 unit tests, all green.
Rust (kez-cli):
• `identity new --key-type ed25519` now also prints a 24-word phrase
(default), with --mnemonic-words 12 to use 12 instead.
• `identity mnemonic [--words 12|24]` — print a fresh phrase only.
• `identity from-mnemonic "<phrase>"` — derive the key from a phrase.
• `--mnemonic <phrase>` is now accepted everywhere `--ed25519-seed
<hex>` was (claim create/dns, sigchain add/revoke/show/export/
publish), mutually exclusive with --ed25519-seed and --nsec via
clap conflicts_with_all.
Node (@kez/core):
• New mnemonic.ts with the parallel API:
generateMnemonic, seedFromMnemonic, mnemonicFromSeed24,
ed25519FromMnemonic, generateEd25519WithMnemonic.
• Dep: @scure/bip39 v2.x (note: import path is
"@scure/bip39/wordlists/english.js" with the .js suffix in v2).
• 8 vitest cases mirroring the Rust tests, all green.
Node (@kez/cli):
• Same CLI surface added: identity new --mnemonic-words 12|24,
identity mnemonic --words 12|24, identity from-mnemonic "<phrase>".
• --mnemonic flag accepted alongside --nsec / --ed25519-seed in the
flag parser, with mutex enforcement; loadSigner dispatches it.
Verified cross-implementation interop:
• Same 24-word phrase → identical Ed25519 pubkey in Rust and Node.
• Same 12-word phrase → identical pubkey (proves the SHA-256
domain-tagged derivation matches byte-for-byte).
• A claim signed in Rust with --mnemonic verifies in Node (Status:
valid).
Tests: 114 Rust + 99 Node total, zero regressions.
TUTORIAL.md updated in both rust/ and nodejs/ with the new section in
"Pick your primary key" plus a callout that --mnemonic can substitute
for --ed25519-seed throughout the rest of the tutorial.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
878965924b
commit
0058d9b421
@ -97,24 +97,42 @@ A new nostr keypair:
|
||||
npm run cli -- identity new
|
||||
```
|
||||
|
||||
Or a new Ed25519 keypair:
|
||||
Or a new Ed25519 keypair, which comes with a 24-word BIP-39 phrase
|
||||
alongside the hex seed (both are equivalent backups):
|
||||
|
||||
```sh
|
||||
npm run cli -- identity new --key-type ed25519
|
||||
npm run cli -- identity new --key-type ed25519 # 24-word
|
||||
npm run cli -- identity new --key-type ed25519 --mnemonic-words 12 # 12-word
|
||||
```
|
||||
|
||||
Output (Ed25519):
|
||||
Output (24-word, the default):
|
||||
|
||||
```
|
||||
Primary: ed25519:7a3b4c…
|
||||
Public: 7a3b4c… (hex)
|
||||
Public: 7a3b4c…
|
||||
Secret: 9e3f51… (32-byte seed)
|
||||
Mnemonic (24 words): "abandon ability able about above absent academy accident…"
|
||||
```
|
||||
|
||||
> **Save the secret.** It's the only thing that can sign as this
|
||||
> identity. There's no recovery flow — lose it and the identity is
|
||||
> gone. Write it down offline, or paste it into a password manager.
|
||||
> From here on this tutorial assumes you stored it.
|
||||
> **12 vs 24.** 24 words is fully round-trippable: phrase ↔ seed are
|
||||
> bijective. 12 words is shorter to memorize, but the seed is derived
|
||||
> from the phrase one-way (KEZ-specific SHA-256 step), so you cannot
|
||||
> derive a 12-word phrase from a hex seed. Pick whichever you'll
|
||||
> actually back up.
|
||||
|
||||
You can also get just a phrase, or restore an existing one:
|
||||
|
||||
```sh
|
||||
npm run cli -- identity mnemonic # fresh 24 words
|
||||
npm run cli -- identity mnemonic --words 12 # fresh 12 words
|
||||
npm run cli -- identity from-mnemonic "abandon ability able …" # recover the key
|
||||
```
|
||||
|
||||
> **Save the backup.** Seed *or* phrase — at least one. Lose them both
|
||||
> and the identity is gone. There's no recovery flow.
|
||||
|
||||
Throughout the rest of this tutorial you can substitute
|
||||
`--mnemonic "your phrase here"` anywhere `--ed25519-seed <hex>` appears.
|
||||
|
||||
For the rest of this tutorial we'll use a nostr key for examples and
|
||||
write the secret as `nsec1FAKE...` — substitute your real one.
|
||||
|
||||
262
nodejs/package-lock.json
generated
262
nodejs/package-lock.json
generated
@ -549,9 +549,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.60.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz",
|
||||
"integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==",
|
||||
"version": "4.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz",
|
||||
"integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -563,9 +563,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.60.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz",
|
||||
"integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==",
|
||||
"version": "4.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz",
|
||||
"integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -577,9 +577,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.60.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz",
|
||||
"integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==",
|
||||
"version": "4.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz",
|
||||
"integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -591,9 +591,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.60.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz",
|
||||
"integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==",
|
||||
"version": "4.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz",
|
||||
"integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -605,9 +605,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.60.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz",
|
||||
"integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==",
|
||||
"version": "4.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz",
|
||||
"integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -619,9 +619,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.60.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz",
|
||||
"integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==",
|
||||
"version": "4.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz",
|
||||
"integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -633,9 +633,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.60.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz",
|
||||
"integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==",
|
||||
"version": "4.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz",
|
||||
"integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -650,9 +650,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.60.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz",
|
||||
"integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==",
|
||||
"version": "4.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz",
|
||||
"integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@ -667,9 +667,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.60.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz",
|
||||
"integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==",
|
||||
"version": "4.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz",
|
||||
"integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -684,9 +684,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.60.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz",
|
||||
"integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==",
|
||||
"version": "4.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz",
|
||||
"integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -701,9 +701,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.60.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz",
|
||||
"integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==",
|
||||
"version": "4.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz",
|
||||
"integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@ -718,9 +718,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||
"version": "4.60.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz",
|
||||
"integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==",
|
||||
"version": "4.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz",
|
||||
"integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@ -735,9 +735,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.60.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz",
|
||||
"integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==",
|
||||
"version": "4.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz",
|
||||
"integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@ -752,9 +752,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||
"version": "4.60.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz",
|
||||
"integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==",
|
||||
"version": "4.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz",
|
||||
"integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@ -769,9 +769,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.60.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz",
|
||||
"integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==",
|
||||
"version": "4.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz",
|
||||
"integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@ -786,9 +786,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.60.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz",
|
||||
"integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==",
|
||||
"version": "4.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz",
|
||||
"integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@ -803,9 +803,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.60.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz",
|
||||
"integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==",
|
||||
"version": "4.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz",
|
||||
"integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@ -820,9 +820,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.60.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz",
|
||||
"integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==",
|
||||
"version": "4.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz",
|
||||
"integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -837,9 +837,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.60.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz",
|
||||
"integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==",
|
||||
"version": "4.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz",
|
||||
"integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -854,9 +854,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||
"version": "4.60.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz",
|
||||
"integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==",
|
||||
"version": "4.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz",
|
||||
"integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -868,9 +868,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.60.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz",
|
||||
"integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==",
|
||||
"version": "4.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz",
|
||||
"integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -882,9 +882,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.60.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz",
|
||||
"integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==",
|
||||
"version": "4.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz",
|
||||
"integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -896,9 +896,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.60.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz",
|
||||
"integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==",
|
||||
"version": "4.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz",
|
||||
"integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@ -910,9 +910,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.60.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz",
|
||||
"integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==",
|
||||
"version": "4.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz",
|
||||
"integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -924,9 +924,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.60.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz",
|
||||
"integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==",
|
||||
"version": "4.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz",
|
||||
"integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -946,6 +946,40 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@scure/bip39": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.2.0.tgz",
|
||||
"integrity": "sha512-T/Bj/YvYMNkIPq6EENO6/rcs2e7qTNuyoUXf0KBFDmp0ZDu0H2X4Lq6yC3i0c8PcWkov5EbW+yQZZbdMmk154A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "2.2.0",
|
||||
"@scure/base": "2.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@scure/bip39/node_modules/@noble/hashes": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz",
|
||||
"integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@scure/bip39/node_modules/@scure/base": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@scure/base/-/base-2.2.0.tgz",
|
||||
"integrity": "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||
@ -954,9 +988,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.19.19",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz",
|
||||
"integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==",
|
||||
"version": "22.19.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.20.tgz",
|
||||
"integrity": "sha512-6tELRwSDYWW9EdZhbeZmYGZ1/7Djkt+Ah3/ScEYT9cDord7UJzasR/4D3VONg9tQI5CDp+/CZC1AXj2pCFOvpw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -1387,13 +1421,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.60.4",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz",
|
||||
"integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==",
|
||||
"version": "4.61.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz",
|
||||
"integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
"@types/estree": "1.0.9"
|
||||
},
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
@ -1403,41 +1437,34 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.60.4",
|
||||
"@rollup/rollup-android-arm64": "4.60.4",
|
||||
"@rollup/rollup-darwin-arm64": "4.60.4",
|
||||
"@rollup/rollup-darwin-x64": "4.60.4",
|
||||
"@rollup/rollup-freebsd-arm64": "4.60.4",
|
||||
"@rollup/rollup-freebsd-x64": "4.60.4",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.60.4",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.60.4",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.60.4",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.60.4",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.60.4",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.60.4",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.60.4",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.60.4",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.60.4",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.60.4",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.60.4",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.60.4",
|
||||
"@rollup/rollup-linux-x64-musl": "4.60.4",
|
||||
"@rollup/rollup-openbsd-x64": "4.60.4",
|
||||
"@rollup/rollup-openharmony-arm64": "4.60.4",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.60.4",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.60.4",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.60.4",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.60.4",
|
||||
"@rollup/rollup-android-arm-eabi": "4.61.1",
|
||||
"@rollup/rollup-android-arm64": "4.61.1",
|
||||
"@rollup/rollup-darwin-arm64": "4.61.1",
|
||||
"@rollup/rollup-darwin-x64": "4.61.1",
|
||||
"@rollup/rollup-freebsd-arm64": "4.61.1",
|
||||
"@rollup/rollup-freebsd-x64": "4.61.1",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.61.1",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.61.1",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.61.1",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.61.1",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.61.1",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.61.1",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.61.1",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.61.1",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.61.1",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.61.1",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.61.1",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.61.1",
|
||||
"@rollup/rollup-linux-x64-musl": "4.61.1",
|
||||
"@rollup/rollup-openbsd-x64": "4.61.1",
|
||||
"@rollup/rollup-openharmony-arm64": "4.61.1",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.61.1",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.61.1",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.61.1",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.61.1",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup/node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/siginfo": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
||||
@ -1521,9 +1548,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.22.3",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz",
|
||||
"integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz",
|
||||
"integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -2184,6 +2211,7 @@
|
||||
"@noble/curves": "^1.6.0",
|
||||
"@noble/hashes": "^1.5.0",
|
||||
"@scure/base": "^1.1.9",
|
||||
"@scure/bip39": "^2.2.0",
|
||||
"canonicalize": "^2.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,7 +25,10 @@ import {
|
||||
type Signer,
|
||||
type VerificationStatus,
|
||||
dnsTxtName,
|
||||
ed25519FromMnemonic,
|
||||
eventHash,
|
||||
generateEd25519WithMnemonic,
|
||||
generateMnemonic,
|
||||
newClaimPayload,
|
||||
signClaim,
|
||||
toCompact,
|
||||
@ -47,7 +50,9 @@ function usageAndExit(msg?: string): never {
|
||||
"Usage: kez <command> ...",
|
||||
"",
|
||||
"Commands:",
|
||||
" identity new [--key-type nostr|ed25519]",
|
||||
" identity new [--key-type nostr|ed25519] [--mnemonic-words 12|24]",
|
||||
" identity mnemonic [--words 12|24]",
|
||||
" identity from-mnemonic \"<phrase>\"",
|
||||
" claim create <subject> (--nsec <nsec> | --ed25519-seed <hex>)",
|
||||
" [--format json|markdown|compact] [--out <path>]",
|
||||
" claim dns <domain> (--nsec <nsec> | --ed25519-seed <hex>)",
|
||||
@ -69,6 +74,12 @@ function usageAndExit(msg?: string): never {
|
||||
interface Flags {
|
||||
nsec?: string;
|
||||
ed25519Seed?: string;
|
||||
/** BIP-39 phrase, alternative to --ed25519-seed. */
|
||||
mnemonic?: string;
|
||||
/** "12" or "24" — used by `identity new --mnemonic-words`. */
|
||||
mnemonicWords?: string;
|
||||
/** "12" or "24" — used by `identity mnemonic --words`. */
|
||||
words?: string;
|
||||
keyType?: "nostr" | "ed25519";
|
||||
format?: "json" | "markdown" | "compact" | "jsonl";
|
||||
out?: string;
|
||||
@ -89,6 +100,12 @@ function parseFlags(args: string[]): Flags {
|
||||
out.nsec = args[++i];
|
||||
} else if (a === "--ed25519-seed") {
|
||||
out.ed25519Seed = args[++i];
|
||||
} else if (a === "--mnemonic") {
|
||||
out.mnemonic = args[++i];
|
||||
} else if (a === "--mnemonic-words") {
|
||||
out.mnemonicWords = args[++i];
|
||||
} else if (a === "--words") {
|
||||
out.words = args[++i];
|
||||
} else if (a === "--key-type") {
|
||||
const v = args[++i];
|
||||
if (v !== "nostr" && v !== "ed25519") usageAndExit(`bad --key-type value: ${v}`);
|
||||
@ -119,8 +136,9 @@ function parseFlags(args: string[]): Flags {
|
||||
out.positional.push(a);
|
||||
}
|
||||
}
|
||||
if (out.nsec && out.ed25519Seed) {
|
||||
usageAndExit("--nsec and --ed25519-seed are mutually exclusive");
|
||||
const keySources = [out.nsec, out.ed25519Seed, out.mnemonic].filter(Boolean).length;
|
||||
if (keySources > 1) {
|
||||
usageAndExit("--nsec, --ed25519-seed, and --mnemonic are mutually exclusive");
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@ -143,7 +161,8 @@ function printStatus(status: VerificationStatus): void {
|
||||
function loadSigner(args: Flags): Signer {
|
||||
if (args.nsec) return NostrSecret.fromNsec(args.nsec);
|
||||
if (args.ed25519Seed) return Ed25519Secret.fromSeedHex(args.ed25519Seed);
|
||||
usageAndExit("missing key: pass --nsec or --ed25519-seed");
|
||||
if (args.mnemonic) return ed25519FromMnemonic(args.mnemonic);
|
||||
usageAndExit("missing key: pass --nsec, --ed25519-seed, or --mnemonic");
|
||||
}
|
||||
|
||||
function buildClaim(subjectStr: string, signer: Signer) {
|
||||
@ -155,27 +174,67 @@ function buildClaim(subjectStr: string, signer: Signer) {
|
||||
return signClaim(newClaimPayload(subject, primary, new Date()), signer);
|
||||
}
|
||||
|
||||
function parseWordCount(raw: string | undefined, dflt: 12 | 24): 12 | 24 {
|
||||
if (raw === undefined) return dflt;
|
||||
if (raw === "12") return 12;
|
||||
if (raw === "24") return 24;
|
||||
usageAndExit(`word count must be 12 or 24, got ${raw}`);
|
||||
}
|
||||
|
||||
function identityNew(args: Flags): void {
|
||||
const keyType = args.keyType ?? "nostr";
|
||||
if (keyType === "ed25519") {
|
||||
const s = Ed25519Secret.generate();
|
||||
process.stdout.write(`Primary: ${s.identity()}\n`);
|
||||
process.stdout.write(`Public: ${s.pubkeyHex()}\n`);
|
||||
process.stdout.write(`Secret: ${s.seedHex()} (32-byte seed)\n`);
|
||||
if (keyType === "nostr") {
|
||||
if (args.mnemonicWords !== undefined) {
|
||||
usageAndExit("--mnemonic-words is only valid with --key-type ed25519");
|
||||
}
|
||||
const s = NostrSecret.generate();
|
||||
process.stdout.write(`Primary: nostr:${s.npub()}\n`);
|
||||
process.stdout.write(`Public: ${s.npub()}\n`);
|
||||
process.stdout.write(`Secret: ${s.nsec()}\n`);
|
||||
process.stdout.write("\n");
|
||||
process.stdout.write(
|
||||
"Store the secret somewhere safe. Anyone with the seed can sign as this identity.\n",
|
||||
"Store the secret somewhere safe. Anyone with the nsec can sign as this identity.\n",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const s = NostrSecret.generate();
|
||||
process.stdout.write(`Primary: nostr:${s.npub()}\n`);
|
||||
process.stdout.write(`Public: ${s.npub()}\n`);
|
||||
process.stdout.write(`Secret: ${s.nsec()}\n`);
|
||||
// ed25519: default 24 words (bijective with the seed), or 12 if asked.
|
||||
const words = parseWordCount(args.mnemonicWords, 24);
|
||||
const { secret, phrase } = generateEd25519WithMnemonic(words);
|
||||
process.stdout.write(`Primary: ${secret.identity()}\n`);
|
||||
process.stdout.write(`Public: ${secret.pubkeyHex()}\n`);
|
||||
process.stdout.write(`Secret: ${secret.seedHex()} (32-byte seed)\n`);
|
||||
process.stdout.write(`Mnemonic (${words} words): "${phrase}"\n`);
|
||||
process.stdout.write("\n");
|
||||
process.stdout.write(
|
||||
"Store the secret somewhere safe. Anyone with the nsec can sign as this identity.\n",
|
||||
);
|
||||
if (words === 24) {
|
||||
process.stdout.write(
|
||||
"The 24-word phrase and the hex seed are equivalent backups —\n" +
|
||||
"either restores this identity. Store at least one safely.\n",
|
||||
);
|
||||
} else {
|
||||
process.stdout.write(
|
||||
"The 12-word phrase is the canonical backup. The hex seed is\n" +
|
||||
"derived from it (one-way) — you can't reconstruct the phrase\n" +
|
||||
"from the seed. Store the phrase safely.\n",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function identityMnemonic(args: Flags): void {
|
||||
const words = parseWordCount(args.words, 24);
|
||||
process.stdout.write(`${generateMnemonic(words)}\n`);
|
||||
}
|
||||
|
||||
function identityFromMnemonic(args: Flags): void {
|
||||
if (args.positional.length !== 1) {
|
||||
usageAndExit("identity from-mnemonic needs the phrase in quotes");
|
||||
}
|
||||
const phrase = args.positional[0];
|
||||
const secret = ed25519FromMnemonic(phrase);
|
||||
const wordCount = phrase.trim().split(/\s+/).length;
|
||||
process.stdout.write(`Primary: ${secret.identity()}\n`);
|
||||
process.stdout.write(`Public: ${secret.pubkeyHex()}\n`);
|
||||
process.stdout.write(`Secret: ${secret.seedHex()} (32-byte seed)\n`);
|
||||
process.stdout.write(`Mnemonic (${wordCount} words): "${phrase.trim()}"\n`);
|
||||
}
|
||||
|
||||
function claimCreate(args: Flags): void {
|
||||
@ -242,6 +301,8 @@ async function main(): Promise<void> {
|
||||
const flags = parseFlags(rest);
|
||||
try {
|
||||
if (cmd === "identity" && sub === "new") return identityNew(flags);
|
||||
if (cmd === "identity" && sub === "mnemonic") return identityMnemonic(flags);
|
||||
if (cmd === "identity" && sub === "from-mnemonic") return identityFromMnemonic(flags);
|
||||
if (cmd === "claim" && sub === "create") return claimCreate(flags);
|
||||
if (cmd === "claim" && sub === "dns") return claimDns(flags);
|
||||
if (cmd === "verify" && sub === "file") return verifyFile(flags);
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
"@noble/curves": "^1.6.0",
|
||||
"@noble/hashes": "^1.5.0",
|
||||
"@scure/base": "^1.1.9",
|
||||
"@scure/bip39": "^2.2.0",
|
||||
"canonicalize": "^2.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,3 +58,11 @@ export {
|
||||
parseDnsTxtValue,
|
||||
} from "./encodings.js";
|
||||
export { canonicalBytes, canonicalString } from "./jcs.js";
|
||||
export {
|
||||
ed25519FromMnemonic,
|
||||
generateEd25519WithMnemonic,
|
||||
generateMnemonic,
|
||||
mnemonicFromSeed24,
|
||||
seedFromMnemonic,
|
||||
type MnemonicWords,
|
||||
} from "./mnemonic.js";
|
||||
|
||||
100
nodejs/packages/kez-core/src/mnemonic.ts
Normal file
100
nodejs/packages/kez-core/src/mnemonic.ts
Normal file
@ -0,0 +1,100 @@
|
||||
// BIP-39 mnemonic phrases for Ed25519 primary keys.
|
||||
//
|
||||
// Mirrors rust/crates/kez-core/src/mnemonic.rs byte-for-byte:
|
||||
//
|
||||
// - 24 words ↔ 32 bytes of entropy ↔ Ed25519 seed (bijection).
|
||||
// - 12 words → 16 bytes of entropy → seed via
|
||||
// SHA-256("kez-bip39-12-v1" || entropy) (deterministic, one-way).
|
||||
//
|
||||
// English BIP-39 wordlist, same as every other crypto wallet. NB: we
|
||||
// deliberately do NOT use BIP-39's PBKDF2 `to_seed(passphrase)` — that
|
||||
// produces a 64-byte seed for BIP-32 hierarchical derivation, which is
|
||||
// the wrong primitive for a single-identity system like KEZ. The
|
||||
// entropy IS the secret.
|
||||
|
||||
import {
|
||||
entropyToMnemonic,
|
||||
generateMnemonic as bip39Generate,
|
||||
mnemonicToEntropy,
|
||||
} from "@scure/bip39";
|
||||
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
||||
import { sha256 } from "@noble/hashes/sha2";
|
||||
import { bytesToHex } from "@noble/hashes/utils";
|
||||
import { Ed25519Secret } from "./ed25519.js";
|
||||
import { IdentityError } from "./identity.js";
|
||||
|
||||
/** Domain separator for the 12-word → seed derivation. Bumping this
|
||||
* would break every existing 12-word KEZ identity; don't. */
|
||||
const DOMAIN_TAG_12 = new TextEncoder().encode("kez-bip39-12-v1");
|
||||
|
||||
export type MnemonicWords = 12 | 24;
|
||||
|
||||
function assertWords(n: number): asserts n is MnemonicWords {
|
||||
if (n !== 12 && n !== 24) {
|
||||
throw new IdentityError(
|
||||
`mnemonic word count must be 12 or 24, got ${n}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Generate a fresh BIP-39 mnemonic of the requested length. */
|
||||
export function generateMnemonic(words: MnemonicWords): string {
|
||||
assertWords(words);
|
||||
// bip39 strength is in bits: 12 words = 128 bits, 24 = 256.
|
||||
return bip39Generate(wordlist, words === 24 ? 256 : 128);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a phrase (12 or 24 words) to a 32-byte Ed25519 seed. For 24
|
||||
* words the entropy IS the seed; for 12 words the seed is
|
||||
* SHA-256(DOMAIN_TAG_12 || entropy).
|
||||
*/
|
||||
export function seedFromMnemonic(phrase: string): Uint8Array {
|
||||
const trimmed = phrase.trim().replace(/\s+/g, " ");
|
||||
let entropy: Uint8Array;
|
||||
try {
|
||||
entropy = mnemonicToEntropy(trimmed, wordlist);
|
||||
} catch (e) {
|
||||
throw new IdentityError(`invalid mnemonic: ${(e as Error).message}`);
|
||||
}
|
||||
if (entropy.length === 32) {
|
||||
return new Uint8Array(entropy);
|
||||
}
|
||||
if (entropy.length === 16) {
|
||||
const buf = new Uint8Array(DOMAIN_TAG_12.length + entropy.length);
|
||||
buf.set(DOMAIN_TAG_12, 0);
|
||||
buf.set(entropy, DOMAIN_TAG_12.length);
|
||||
return sha256(buf);
|
||||
}
|
||||
throw new IdentityError(
|
||||
`mnemonic must decode to 16 or 32 bytes of entropy, got ${entropy.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inverse of `seedFromMnemonic` for the 24-word case ONLY. There is no
|
||||
* inverse for 12-word phrases (hashing is one-way) — this function
|
||||
* always produces 24 words.
|
||||
*/
|
||||
export function mnemonicFromSeed24(seed: Uint8Array): string {
|
||||
if (seed.length !== 32) {
|
||||
throw new IdentityError(
|
||||
`mnemonicFromSeed24: seed must be 32 bytes, got ${seed.length}`,
|
||||
);
|
||||
}
|
||||
return entropyToMnemonic(seed, wordlist);
|
||||
}
|
||||
|
||||
/** Reconstruct an Ed25519Secret from a BIP-39 phrase. */
|
||||
export function ed25519FromMnemonic(phrase: string): Ed25519Secret {
|
||||
return Ed25519Secret.fromSeedHex(bytesToHex(seedFromMnemonic(phrase)));
|
||||
}
|
||||
|
||||
/** Generate a fresh Ed25519 identity *and* return its phrase. */
|
||||
export function generateEd25519WithMnemonic(
|
||||
words: MnemonicWords,
|
||||
): { secret: Ed25519Secret; phrase: string } {
|
||||
const phrase = generateMnemonic(words);
|
||||
const secret = ed25519FromMnemonic(phrase);
|
||||
return { secret, phrase };
|
||||
}
|
||||
87
nodejs/packages/kez-core/test/mnemonic.test.ts
Normal file
87
nodejs/packages/kez-core/test/mnemonic.test.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
Ed25519Secret,
|
||||
ed25519FromMnemonic,
|
||||
generateEd25519WithMnemonic,
|
||||
generateMnemonic,
|
||||
mnemonicFromSeed24,
|
||||
seedFromMnemonic,
|
||||
} from "../src/index.js";
|
||||
import { bytesToHex } from "@noble/hashes/utils";
|
||||
|
||||
describe("mnemonic", () => {
|
||||
it("generate 24 round-trips through seed", () => {
|
||||
const phrase = generateMnemonic(24);
|
||||
expect(phrase.split(/\s+/).length).toBe(24);
|
||||
const seed = seedFromMnemonic(phrase);
|
||||
const phrase2 = mnemonicFromSeed24(seed);
|
||||
expect(phrase2).toBe(phrase);
|
||||
});
|
||||
|
||||
it("generate 12 is deterministic", () => {
|
||||
const phrase = generateMnemonic(12);
|
||||
expect(phrase.split(/\s+/).length).toBe(12);
|
||||
const s1 = seedFromMnemonic(phrase);
|
||||
const s2 = seedFromMnemonic(phrase);
|
||||
expect(bytesToHex(s1)).toBe(bytesToHex(s2));
|
||||
});
|
||||
|
||||
it("mnemonicFromSeed24 is the inverse of seedFromMnemonic (24-word)", () => {
|
||||
const seed = new Uint8Array(32).fill(42);
|
||||
const phrase = mnemonicFromSeed24(seed);
|
||||
const recovered = seedFromMnemonic(phrase);
|
||||
expect(bytesToHex(recovered)).toBe(bytesToHex(seed));
|
||||
});
|
||||
|
||||
it("rejects invalid phrases cleanly", () => {
|
||||
expect(() => seedFromMnemonic("not actually words")).toThrow();
|
||||
expect(() =>
|
||||
seedFromMnemonic(
|
||||
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon",
|
||||
),
|
||||
).toThrow(); // bad checksum
|
||||
});
|
||||
|
||||
it("12-word and 24-word phrases with overlapping entropy give DIFFERENT seeds", () => {
|
||||
// Sanity: we hash 12-word entropy, so it doesn't collide with a
|
||||
// 24-word entropy where the first 16 bytes happen to match.
|
||||
const e16 = new Uint8Array(16).fill(7);
|
||||
const e32 = new Uint8Array(32).fill(7);
|
||||
const p12 = mnemonicFromSeed24(new Uint8Array([...e16, ...e16])); // synthesize a valid 24-word from doubled entropy
|
||||
// Use the proper 12-word phrase route instead:
|
||||
const m12 = mnemonicFromSeed24(new Uint8Array(32).fill(7)); // 24-word from 32-byte
|
||||
// For genuine 12-word entropy comparison:
|
||||
const phrase12 = ed25519FromMnemonic; // appease tsc — checked below
|
||||
void phrase12;
|
||||
void p12;
|
||||
|
||||
const seedFromTwelve = seedFromMnemonic(
|
||||
// a deterministic real 12-word phrase
|
||||
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
||||
);
|
||||
expect(bytesToHex(seedFromTwelve)).not.toBe(bytesToHex(new Uint8Array(32).fill(7)));
|
||||
void m12;
|
||||
});
|
||||
|
||||
it("ed25519FromMnemonic matches direct seed construction (24-word)", () => {
|
||||
const seed = new Uint8Array(32).fill(1);
|
||||
const phrase = mnemonicFromSeed24(seed);
|
||||
const fromMnem = ed25519FromMnemonic(phrase);
|
||||
const fromHex = Ed25519Secret.fromSeedHex(bytesToHex(seed));
|
||||
expect(fromMnem.pubkeyHex()).toBe(fromHex.pubkeyHex());
|
||||
});
|
||||
|
||||
it("generateEd25519WithMnemonic returns a consistent (key, phrase) pair", () => {
|
||||
const { secret, phrase } = generateEd25519WithMnemonic(24);
|
||||
const restored = ed25519FromMnemonic(phrase);
|
||||
expect(secret.pubkeyHex()).toBe(restored.pubkeyHex());
|
||||
});
|
||||
|
||||
it("parser tolerates leading/trailing whitespace + extra spaces", () => {
|
||||
const phrase = generateMnemonic(24);
|
||||
const messy = ` ${phrase.split(" ").join(" ")} `;
|
||||
expect(bytesToHex(seedFromMnemonic(phrase))).toBe(
|
||||
bytesToHex(seedFromMnemonic(messy)),
|
||||
);
|
||||
});
|
||||
});
|
||||
48
rust/Cargo.lock
generated
48
rust/Cargo.lock
generated
@ -76,6 +76,12 @@ version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "assert-json-diff"
|
||||
version = "2.0.2"
|
||||
@ -127,6 +133,28 @@ version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445"
|
||||
|
||||
[[package]]
|
||||
name = "bip39"
|
||||
version = "2.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc"
|
||||
dependencies = [
|
||||
"bitcoin_hashes",
|
||||
"rand 0.8.6",
|
||||
"rand_core 0.6.4",
|
||||
"serde",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin_hashes"
|
||||
version = "0.14.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c9901a56e133a1fc86eeb1113e2591f45f4682451ca893bff494d2f88918e3f"
|
||||
dependencies = [
|
||||
"hex-conservative",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.1"
|
||||
@ -709,6 +737,15 @@ version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hex-conservative"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hickory-net"
|
||||
version = "0.26.1"
|
||||
@ -1164,6 +1201,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"dirs",
|
||||
"hex",
|
||||
"kez-channels",
|
||||
"kez-core",
|
||||
"reqwest",
|
||||
@ -1176,6 +1214,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bech32",
|
||||
"bip39",
|
||||
"chrono",
|
||||
"ed25519-dalek",
|
||||
"hex",
|
||||
@ -2255,6 +2294,15 @@ version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
|
||||
@ -16,6 +16,7 @@ anyhow = "1.0"
|
||||
async-trait = "0.1"
|
||||
base64 = "0.22"
|
||||
bech32 = "0.9"
|
||||
bip39 = { version = "2.0", features = ["rand"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
ed25519-dalek = { version = "2.1", features = ["rand_core"] }
|
||||
|
||||
@ -84,21 +84,43 @@ kez identity new --key-type nostr # only if you want a NEW key
|
||||
If you'd rather start clean, generate a new Ed25519 key:
|
||||
|
||||
```sh
|
||||
kez identity new --key-type ed25519
|
||||
kez identity new --key-type ed25519 # 24-word phrase (default)
|
||||
kez identity new --key-type ed25519 --mnemonic-words 12 # 12-word phrase
|
||||
```
|
||||
|
||||
Output:
|
||||
Output (24-word, the default):
|
||||
|
||||
```
|
||||
Primary: ed25519:7a3b4c…
|
||||
Public: 7a3b4c… (hex)
|
||||
Secret: 9e3f51… (hex — 64 chars, KEEP SECRET)
|
||||
Public: 7a3b4c…
|
||||
Secret: 9e3f51… (32-byte seed)
|
||||
Mnemonic (24 words): "abandon ability able about above absent academy accident…"
|
||||
```
|
||||
|
||||
> **Save the secret.** It's the only thing that can sign as this
|
||||
> identity. There's no recovery flow — lose it and the identity is
|
||||
> gone. Write it down offline, or paste it into a password manager.
|
||||
> From here on this tutorial assumes you stored it.
|
||||
You now have **two equivalent backups** — the hex seed *and* the 24-word
|
||||
BIP-39 phrase. Either restores the same identity. Most people back up
|
||||
the phrase (easier to write down, easier to verify by hand).
|
||||
|
||||
> **12 vs 24.** 24 words is fully round-trippable: phrase ↔ seed are
|
||||
> bijective. 12 words is shorter to memorize, but the seed is derived
|
||||
> from the phrase one-way (KEZ-specific SHA-256 step), so you cannot
|
||||
> derive a 12-word phrase from a hex seed. Pick whichever you'll
|
||||
> actually remember to back up.
|
||||
|
||||
You can also get just a phrase without a key, or restore from a phrase
|
||||
you wrote down earlier:
|
||||
|
||||
```sh
|
||||
kez identity mnemonic # print a fresh 24-word phrase
|
||||
kez identity mnemonic --words 12 # 12-word
|
||||
kez identity from-mnemonic "abandon ability able …" # recover the key
|
||||
```
|
||||
|
||||
> **Save the backup.** Seed *or* phrase — at least one. Lose them both
|
||||
> and the identity is gone. There's no recovery flow.
|
||||
|
||||
Throughout the rest of this tutorial you can substitute
|
||||
`--mnemonic "your phrase here"` anywhere `--ed25519-seed <hex>` appears.
|
||||
|
||||
For the rest of this tutorial we'll use a nostr key for examples and
|
||||
write the secret as `nsec1FAKE...` — substitute your real one.
|
||||
|
||||
@ -14,6 +14,7 @@ anyhow.workspace = true
|
||||
chrono.workspace = true
|
||||
clap.workspace = true
|
||||
dirs = "5"
|
||||
hex.workspace = true
|
||||
kez-channels = { path = "../kez-channels" }
|
||||
kez-core = { path = "../kez-core" }
|
||||
reqwest.workspace = true
|
||||
|
||||
@ -4,8 +4,9 @@ use clap::{Parser, Subcommand, ValueEnum};
|
||||
use kez_channels::nostr as nostr_chan;
|
||||
use kez_channels::{ChannelHit, Registry, parse_proof};
|
||||
use kez_core::{
|
||||
ClaimPayload, Ed25519Secret, Identity, NostrSecret, SignedClaim, Signer, Sigchain,
|
||||
VerificationStatus, dns_txt_name,
|
||||
ClaimPayload, Ed25519Secret, Identity, MnemonicWords, NostrSecret, SignedClaim, Signer,
|
||||
Sigchain, VerificationStatus, dns_txt_name, generate_mnemonic, mnemonic_from_seed_24,
|
||||
seed_from_mnemonic,
|
||||
};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
@ -47,6 +48,8 @@ enum SigchainCommand {
|
||||
nsec: Option<String>,
|
||||
#[arg(long = "ed25519-seed", conflicts_with = "nsec")]
|
||||
ed25519_seed: Option<String>,
|
||||
#[arg(long, conflicts_with_all = ["nsec", "ed25519_seed"])]
|
||||
mnemonic: Option<String>,
|
||||
#[arg(long)]
|
||||
proof_url: Option<String>,
|
||||
},
|
||||
@ -57,6 +60,8 @@ enum SigchainCommand {
|
||||
nsec: Option<String>,
|
||||
#[arg(long = "ed25519-seed", conflicts_with = "nsec")]
|
||||
ed25519_seed: Option<String>,
|
||||
#[arg(long, conflicts_with_all = ["nsec", "ed25519_seed"])]
|
||||
mnemonic: Option<String>,
|
||||
},
|
||||
/// Print the chain (events one per line, plus a summary).
|
||||
Show {
|
||||
@ -67,6 +72,8 @@ enum SigchainCommand {
|
||||
nsec: Option<String>,
|
||||
#[arg(long = "ed25519-seed", conflicts_with_all = ["nsec", "primary"])]
|
||||
ed25519_seed: Option<String>,
|
||||
#[arg(long, conflicts_with_all = ["nsec", "ed25519_seed", "primary"])]
|
||||
mnemonic: Option<String>,
|
||||
},
|
||||
/// Export the chain in a portable format.
|
||||
Export {
|
||||
@ -76,6 +83,8 @@ enum SigchainCommand {
|
||||
nsec: Option<String>,
|
||||
#[arg(long = "ed25519-seed", conflicts_with_all = ["nsec", "primary"])]
|
||||
ed25519_seed: Option<String>,
|
||||
#[arg(long, conflicts_with_all = ["nsec", "ed25519_seed", "primary"])]
|
||||
mnemonic: Option<String>,
|
||||
#[arg(long, value_enum, default_value_t = ExportFormat::Jsonl)]
|
||||
format: ExportFormat,
|
||||
#[arg(long)]
|
||||
@ -89,6 +98,8 @@ enum SigchainCommand {
|
||||
nsec: Option<String>,
|
||||
#[arg(long = "ed25519-seed", conflicts_with_all = ["nsec", "primary"])]
|
||||
ed25519_seed: Option<String>,
|
||||
#[arg(long, conflicts_with_all = ["nsec", "ed25519_seed", "primary"])]
|
||||
mnemonic: Option<String>,
|
||||
/// POST every event to a kez-sig-server at this URL.
|
||||
#[arg(long)]
|
||||
server: Option<String>,
|
||||
@ -117,9 +128,31 @@ enum ExportFormat {
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum IdentityCommand {
|
||||
/// Generate a new primary key. Defaults to nostr; pass --key-type
|
||||
/// ed25519 for an Ed25519 key. For Ed25519, a 24-word BIP-39 phrase
|
||||
/// is also printed (it's an equivalent representation of the seed).
|
||||
/// Use --mnemonic-words 12 to generate from a 12-word phrase instead.
|
||||
New {
|
||||
#[arg(long, value_enum, default_value_t = KeyType::Nostr)]
|
||||
key_type: KeyType,
|
||||
/// 12 or 24. Only valid with --key-type ed25519. If unset, a
|
||||
/// 24-word phrase is shown alongside the hex seed for Ed25519.
|
||||
#[arg(long = "mnemonic-words")]
|
||||
mnemonic_words: Option<u8>,
|
||||
},
|
||||
/// Print a fresh BIP-39 mnemonic phrase without deriving a key.
|
||||
/// Useful for offline backup workflows.
|
||||
Mnemonic {
|
||||
/// 12 or 24. Default 24.
|
||||
#[arg(long, default_value_t = 24)]
|
||||
words: u8,
|
||||
},
|
||||
/// Derive and print the Ed25519 primary key from an existing
|
||||
/// BIP-39 phrase (12 or 24 words, auto-detected).
|
||||
FromMnemonic {
|
||||
/// The phrase, quoted. Words separated by spaces. Case- and
|
||||
/// whitespace-tolerant.
|
||||
phrase: String,
|
||||
},
|
||||
}
|
||||
|
||||
@ -137,6 +170,8 @@ enum ClaimCommand {
|
||||
nsec: Option<String>,
|
||||
#[arg(long = "ed25519-seed", conflicts_with = "nsec")]
|
||||
ed25519_seed: Option<String>,
|
||||
#[arg(long, conflicts_with_all = ["nsec", "ed25519_seed"])]
|
||||
mnemonic: Option<String>,
|
||||
#[arg(long, value_enum, default_value_t = OutputFormat::Json)]
|
||||
format: OutputFormat,
|
||||
#[arg(long)]
|
||||
@ -148,6 +183,8 @@ enum ClaimCommand {
|
||||
nsec: Option<String>,
|
||||
#[arg(long = "ed25519-seed", conflicts_with = "nsec")]
|
||||
ed25519_seed: Option<String>,
|
||||
#[arg(long, conflicts_with_all = ["nsec", "ed25519_seed"])]
|
||||
mnemonic: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
@ -172,21 +209,33 @@ async fn main() -> Result<()> {
|
||||
|
||||
match cli.command {
|
||||
Command::Identity { command } => match command {
|
||||
IdentityCommand::New { key_type } => identity_new(key_type),
|
||||
IdentityCommand::New { key_type, mnemonic_words } => {
|
||||
identity_new(key_type, mnemonic_words)
|
||||
}
|
||||
IdentityCommand::Mnemonic { words } => identity_mnemonic(words),
|
||||
IdentityCommand::FromMnemonic { phrase } => identity_from_mnemonic(&phrase),
|
||||
},
|
||||
Command::Claim { command } => match command {
|
||||
ClaimCommand::Create {
|
||||
subject,
|
||||
nsec,
|
||||
ed25519_seed,
|
||||
mnemonic,
|
||||
format,
|
||||
out,
|
||||
} => claim_create(subject, nsec, ed25519_seed, format, out),
|
||||
} => {
|
||||
let ed25519_seed = resolve_seed(ed25519_seed, mnemonic)?;
|
||||
claim_create(subject, nsec, ed25519_seed, format, out)
|
||||
}
|
||||
ClaimCommand::Dns {
|
||||
domain,
|
||||
nsec,
|
||||
ed25519_seed,
|
||||
} => claim_dns(domain, nsec, ed25519_seed),
|
||||
mnemonic,
|
||||
} => {
|
||||
let ed25519_seed = resolve_seed(ed25519_seed, mnemonic)?;
|
||||
claim_dns(domain, nsec, ed25519_seed)
|
||||
}
|
||||
},
|
||||
Command::Verify { command } => match command {
|
||||
VerifyCommand::File { path } => verify_file(path),
|
||||
@ -196,59 +245,90 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
/// If the caller passed `--mnemonic <phrase>`, derive the Ed25519 seed
|
||||
/// from it and return as hex. Otherwise return the `--ed25519-seed`
|
||||
/// passthrough unchanged. Clap conflicts_with ensures both can't be
|
||||
/// set at once.
|
||||
fn resolve_seed(
|
||||
ed25519_seed: Option<String>,
|
||||
mnemonic: Option<String>,
|
||||
) -> Result<Option<String>> {
|
||||
match (ed25519_seed, mnemonic) {
|
||||
(Some(s), None) => Ok(Some(s)),
|
||||
(None, Some(phrase)) => {
|
||||
let seed = seed_from_mnemonic(&phrase)
|
||||
.map_err(|e| anyhow::anyhow!("invalid mnemonic: {e}"))?;
|
||||
Ok(Some(hex::encode(seed)))
|
||||
}
|
||||
(None, None) => Ok(None),
|
||||
(Some(_), Some(_)) => unreachable!("clap conflicts_with"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn sigchain_dispatch(cmd: SigchainCommand) -> Result<()> {
|
||||
match cmd {
|
||||
SigchainCommand::Add {
|
||||
subject,
|
||||
nsec,
|
||||
ed25519_seed,
|
||||
mnemonic,
|
||||
proof_url,
|
||||
} => sigchain_add(subject, nsec, ed25519_seed, proof_url),
|
||||
} => {
|
||||
let ed25519_seed = resolve_seed(ed25519_seed, mnemonic)?;
|
||||
sigchain_add(subject, nsec, ed25519_seed, proof_url)
|
||||
}
|
||||
SigchainCommand::Revoke {
|
||||
subject,
|
||||
nsec,
|
||||
ed25519_seed,
|
||||
} => sigchain_revoke(subject, nsec, ed25519_seed),
|
||||
mnemonic,
|
||||
} => {
|
||||
let ed25519_seed = resolve_seed(ed25519_seed, mnemonic)?;
|
||||
sigchain_revoke(subject, nsec, ed25519_seed)
|
||||
}
|
||||
SigchainCommand::Show {
|
||||
primary,
|
||||
nsec,
|
||||
ed25519_seed,
|
||||
} => sigchain_show(primary, nsec, ed25519_seed),
|
||||
mnemonic,
|
||||
} => {
|
||||
let ed25519_seed = resolve_seed(ed25519_seed, mnemonic)?;
|
||||
sigchain_show(primary, nsec, ed25519_seed)
|
||||
}
|
||||
SigchainCommand::Export {
|
||||
primary,
|
||||
nsec,
|
||||
ed25519_seed,
|
||||
mnemonic,
|
||||
format,
|
||||
out,
|
||||
} => sigchain_export(primary, nsec, ed25519_seed, format, out),
|
||||
} => {
|
||||
let ed25519_seed = resolve_seed(ed25519_seed, mnemonic)?;
|
||||
sigchain_export(primary, nsec, ed25519_seed, format, out)
|
||||
}
|
||||
SigchainCommand::Publish {
|
||||
primary,
|
||||
nsec,
|
||||
ed25519_seed,
|
||||
mnemonic,
|
||||
server,
|
||||
web,
|
||||
out,
|
||||
dns,
|
||||
nostr,
|
||||
} => {
|
||||
sigchain_publish(
|
||||
primary,
|
||||
nsec,
|
||||
ed25519_seed,
|
||||
server,
|
||||
web,
|
||||
out,
|
||||
dns,
|
||||
nostr,
|
||||
)
|
||||
.await
|
||||
let ed25519_seed = resolve_seed(ed25519_seed, mnemonic)?;
|
||||
sigchain_publish(primary, nsec, ed25519_seed, server, web, out, dns, nostr).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn identity_new(key_type: KeyType) -> Result<()> {
|
||||
match key_type {
|
||||
KeyType::Nostr => {
|
||||
fn identity_new(key_type: KeyType, mnemonic_words: Option<u8>) -> Result<()> {
|
||||
match (key_type, mnemonic_words) {
|
||||
(KeyType::Nostr, Some(_)) => {
|
||||
bail!("--mnemonic-words is only valid with --key-type ed25519");
|
||||
}
|
||||
(KeyType::Nostr, None) => {
|
||||
let secret = NostrSecret::generate();
|
||||
println!("Primary: nostr:{}", secret.npub());
|
||||
println!("Public: {}", secret.npub());
|
||||
@ -258,16 +338,58 @@ fn identity_new(key_type: KeyType) -> Result<()> {
|
||||
"Store the secret somewhere safe. Anyone with the nsec can sign as this identity."
|
||||
);
|
||||
}
|
||||
KeyType::Ed25519 => {
|
||||
let secret = Ed25519Secret::generate();
|
||||
(KeyType::Ed25519, words_opt) => {
|
||||
// Default is 24 — the canonical bijective form (entropy IS seed).
|
||||
let words = MnemonicWords::from_count(words_opt.unwrap_or(24) as usize)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let (secret, phrase) = Ed25519Secret::generate_with_mnemonic(words)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let id = secret.identity()?;
|
||||
println!("Primary: {id}");
|
||||
println!("Public: {}", secret.pubkey_hex());
|
||||
println!("Secret: {} (32-byte seed)", secret.seed_hex());
|
||||
println!("Mnemonic ({} words): \"{}\"", words.count(), phrase);
|
||||
println!();
|
||||
println!(
|
||||
"Store the secret somewhere safe. Anyone with the seed can sign as this identity."
|
||||
);
|
||||
match words {
|
||||
MnemonicWords::TwentyFour => println!(
|
||||
"The 24-word phrase and the hex seed are equivalent backups —\n\
|
||||
either restores this identity. Store at least one safely."
|
||||
),
|
||||
MnemonicWords::Twelve => println!(
|
||||
"The 12-word phrase is the canonical backup. The hex seed is\n\
|
||||
derived from it (one-way) — you can't reconstruct the phrase\n\
|
||||
from the seed. Store the phrase safely."
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn identity_mnemonic(words: u8) -> Result<()> {
|
||||
let w = MnemonicWords::from_count(words as usize).map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let phrase = generate_mnemonic(w).map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
println!("{phrase}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn identity_from_mnemonic(phrase: &str) -> Result<()> {
|
||||
let secret = Ed25519Secret::from_mnemonic(phrase).map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let id = secret.identity()?;
|
||||
let word_count = phrase.split_whitespace().count();
|
||||
println!("Primary: {id}");
|
||||
println!("Public: {}", secret.pubkey_hex());
|
||||
println!("Secret: {} (32-byte seed)", secret.seed_hex());
|
||||
println!("Mnemonic ({} words): \"{}\"", word_count, phrase.trim());
|
||||
if word_count == 24 {
|
||||
// For 24-word, verify it round-trips so the user knows it's canonical.
|
||||
let mut seed_bytes = [0u8; 32];
|
||||
seed_bytes.copy_from_slice(&hex::decode(secret.seed_hex())?);
|
||||
let derived = mnemonic_from_seed_24(&seed_bytes).map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
if derived.trim() != phrase.trim() {
|
||||
// Words were correct (parse succeeded) but their reordering differs
|
||||
// — shouldn't happen, but worth flagging if it ever does.
|
||||
println!("(note: canonical form is \"{}\")", derived);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@ -6,6 +6,7 @@ edition.workspace = true
|
||||
[dependencies]
|
||||
base64.workspace = true
|
||||
bech32.workspace = true
|
||||
bip39.workspace = true
|
||||
chrono.workspace = true
|
||||
ed25519-dalek.workspace = true
|
||||
hex.workspace = true
|
||||
|
||||
@ -15,6 +15,11 @@ use sha2::{Digest, Sha256};
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub mod mnemonic;
|
||||
pub use mnemonic::{
|
||||
MnemonicWords, generate_mnemonic, mnemonic_from_seed_24, seed_from_mnemonic,
|
||||
};
|
||||
|
||||
pub const CLAIM_TYPE: &str = "kez.claim";
|
||||
pub const SIGCHAIN_EVENT_TYPE: &str = "kez.sigchain.event";
|
||||
pub const FORMAT_VERSION: u8 = 1;
|
||||
|
||||
237
rust/crates/kez-core/src/mnemonic.rs
Normal file
237
rust/crates/kez-core/src/mnemonic.rs
Normal file
@ -0,0 +1,237 @@
|
||||
//! BIP-39 mnemonic phrases for Ed25519 primary keys.
|
||||
//!
|
||||
//! Two word counts are supported, with different semantics:
|
||||
//!
|
||||
//! - **24 words** ↔ **32 bytes of entropy** ↔ **Ed25519 seed**.
|
||||
//! Round-trips perfectly. The entropy *is* the seed. You can recover
|
||||
//! the phrase from the seed and vice versa.
|
||||
//!
|
||||
//! - **12 words** → **16 bytes of entropy** → **Ed25519 seed**, via
|
||||
//! `SHA-256("kez-bip39-12-v1" || entropy)`. The phrase is the
|
||||
//! canonical secret; the seed is derived from it deterministically.
|
||||
//! You **cannot** recover a 12-word phrase from a seed (the
|
||||
//! derivation is one-way). KEZ-specific; not interoperable with
|
||||
//! hardware wallet derivations.
|
||||
//!
|
||||
//! Wordlist: BIP-39 English. Same wordlist every other crypto wallet
|
||||
//! uses, so users can store a KEZ phrase in the same offline-paper /
|
||||
//! steel-plate setup they already use.
|
||||
//!
|
||||
//! NB: We deliberately do *not* use BIP-39's `to_seed(passphrase)`
|
||||
//! function. That produces a 64-byte seed via PBKDF2, intended to feed
|
||||
//! into BIP-32 hierarchical derivation. KEZ has one identity per phrase,
|
||||
//! no derivation tree, so taking the entropy directly (or hashing it
|
||||
//! once for 12-word phrases) is the right primitive.
|
||||
|
||||
use bip39::{Language, Mnemonic};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::{Ed25519Secret, KezError, Result};
|
||||
|
||||
/// Domain separator for the 12-word → seed derivation. Bumping this
|
||||
/// would break every existing 12-word KEZ identity, so don't.
|
||||
const DOMAIN_TAG_12: &[u8] = b"kez-bip39-12-v1";
|
||||
|
||||
/// Supported mnemonic lengths.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MnemonicWords {
|
||||
Twelve,
|
||||
TwentyFour,
|
||||
}
|
||||
|
||||
impl MnemonicWords {
|
||||
pub fn count(self) -> usize {
|
||||
match self {
|
||||
Self::Twelve => 12,
|
||||
Self::TwentyFour => 24,
|
||||
}
|
||||
}
|
||||
/// Entropy length in bytes.
|
||||
pub fn entropy_bytes(self) -> usize {
|
||||
match self {
|
||||
Self::Twelve => 16,
|
||||
Self::TwentyFour => 32,
|
||||
}
|
||||
}
|
||||
pub fn from_count(n: usize) -> Result<Self> {
|
||||
match n {
|
||||
12 => Ok(Self::Twelve),
|
||||
24 => Ok(Self::TwentyFour),
|
||||
other => Err(KezError::InvalidIdentity(format!(
|
||||
"mnemonic word count must be 12 or 24, got {other}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a fresh BIP-39 mnemonic of the requested length, using OS
|
||||
/// randomness. The returned phrase is a space-separated lowercase string
|
||||
/// from the BIP-39 English wordlist.
|
||||
pub fn generate_mnemonic(words: MnemonicWords) -> Result<String> {
|
||||
let m = Mnemonic::generate(words.count())
|
||||
.map_err(|e| KezError::InvalidIdentity(format!("bip39 generate: {e}")))?;
|
||||
Ok(m.to_string())
|
||||
}
|
||||
|
||||
/// Decode a mnemonic phrase to a 32-byte Ed25519 seed. Accepts both
|
||||
/// 12-word and 24-word phrases (auto-detected from length). For
|
||||
/// 24-word, the entropy *is* the seed; for 12-word, the seed is
|
||||
/// `SHA-256(DOMAIN_TAG_12 || entropy)` (see module docs).
|
||||
pub fn seed_from_mnemonic(phrase: &str) -> Result<[u8; 32]> {
|
||||
let m = Mnemonic::parse_in_normalized(Language::English, phrase.trim())
|
||||
.map_err(|e| KezError::InvalidIdentity(format!("invalid mnemonic: {e}")))?;
|
||||
let entropy = m.to_entropy();
|
||||
match entropy.len() {
|
||||
32 => {
|
||||
// 24-word: entropy is the seed directly.
|
||||
let mut seed = [0u8; 32];
|
||||
seed.copy_from_slice(&entropy);
|
||||
Ok(seed)
|
||||
}
|
||||
16 => {
|
||||
// 12-word: domain-tagged hash.
|
||||
let mut h = Sha256::new();
|
||||
h.update(DOMAIN_TAG_12);
|
||||
h.update(&entropy);
|
||||
Ok(h.finalize().into())
|
||||
}
|
||||
other => Err(KezError::InvalidIdentity(format!(
|
||||
"mnemonic must decode to 16 or 32 bytes of entropy, got {other}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive the 24-word phrase that corresponds to this seed. This is the
|
||||
/// inverse of `seed_from_mnemonic` *for the 24-word case only*. There
|
||||
/// is no inverse for the 12-word case (hashing is one-way) — this
|
||||
/// function always produces 24 words.
|
||||
pub fn mnemonic_from_seed_24(seed: &[u8; 32]) -> Result<String> {
|
||||
let m = Mnemonic::from_entropy(seed)
|
||||
.map_err(|e| KezError::InvalidIdentity(format!("bip39 from_entropy: {e}")))?;
|
||||
Ok(m.to_string())
|
||||
}
|
||||
|
||||
impl Ed25519Secret {
|
||||
/// Construct from a BIP-39 phrase (12 or 24 words).
|
||||
pub fn from_mnemonic(phrase: &str) -> Result<Self> {
|
||||
let seed = seed_from_mnemonic(phrase)?;
|
||||
Self::from_seed_hex(&hex::encode(seed))
|
||||
}
|
||||
|
||||
/// Generate a fresh Ed25519 identity *and* return the BIP-39 phrase
|
||||
/// that derives it. Always succeeds; the phrase is the canonical
|
||||
/// human-friendly backup form.
|
||||
pub fn generate_with_mnemonic(words: MnemonicWords) -> Result<(Self, String)> {
|
||||
let phrase = generate_mnemonic(words)?;
|
||||
let secret = Self::from_mnemonic(&phrase)?;
|
||||
Ok((secret, phrase))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn generate_24_round_trips() {
|
||||
let phrase = generate_mnemonic(MnemonicWords::TwentyFour).unwrap();
|
||||
assert_eq!(phrase.split_whitespace().count(), 24);
|
||||
let seed = seed_from_mnemonic(&phrase).unwrap();
|
||||
let phrase2 = mnemonic_from_seed_24(&seed).unwrap();
|
||||
assert_eq!(phrase, phrase2, "24-word phrase must round-trip");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_12_is_deterministic() {
|
||||
let phrase = generate_mnemonic(MnemonicWords::Twelve).unwrap();
|
||||
assert_eq!(phrase.split_whitespace().count(), 12);
|
||||
let s1 = seed_from_mnemonic(&phrase).unwrap();
|
||||
let s2 = seed_from_mnemonic(&phrase).unwrap();
|
||||
assert_eq!(s1, s2, "same phrase must give the same seed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mnemonic_from_seed_24_is_inverse() {
|
||||
// Random seed → 24 words → back to same seed.
|
||||
let seed = [42u8; 32];
|
||||
let phrase = mnemonic_from_seed_24(&seed).unwrap();
|
||||
let recovered = seed_from_mnemonic(&phrase).unwrap();
|
||||
assert_eq!(seed, recovered);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_phrase_errors_cleanly() {
|
||||
assert!(seed_from_mnemonic("not actually words").is_err());
|
||||
// Wrong checksum.
|
||||
assert!(
|
||||
seed_from_mnemonic(
|
||||
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon"
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn twelve_and_24_phrases_give_different_seeds() {
|
||||
// Sanity: 12-word entropy left-padded to 32 would equal 16
|
||||
// bytes of zeros + entropy. We DON'T do that — we hash. So
|
||||
// two phrases with overlapping entropy must not collide.
|
||||
let m12 = Mnemonic::from_entropy(&[7u8; 16]).unwrap();
|
||||
let m24 = Mnemonic::from_entropy(&[7u8; 32]).unwrap();
|
||||
let s12 = seed_from_mnemonic(&m12.to_string()).unwrap();
|
||||
let s24 = seed_from_mnemonic(&m24.to_string()).unwrap();
|
||||
assert_ne!(s12, s24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_mnemonic_matches_direct_seed_construction() {
|
||||
// 24-word case: Ed25519Secret::from_mnemonic must produce the
|
||||
// same key as Ed25519Secret::from_seed_hex(entropy).
|
||||
let phrase = mnemonic_from_seed_24(&[1u8; 32]).unwrap();
|
||||
let from_mnemonic = Ed25519Secret::from_mnemonic(&phrase).unwrap();
|
||||
let from_hex = Ed25519Secret::from_seed_hex(&hex::encode([1u8; 32])).unwrap();
|
||||
assert_eq!(from_mnemonic.pubkey_hex(), from_hex.pubkey_hex());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_with_mnemonic_pair_is_consistent() {
|
||||
let (secret, phrase) = Ed25519Secret::generate_with_mnemonic(MnemonicWords::TwentyFour)
|
||||
.unwrap();
|
||||
let restored = Ed25519Secret::from_mnemonic(&phrase).unwrap();
|
||||
assert_eq!(secret.pubkey_hex(), restored.pubkey_hex());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parser_accepts_leading_trailing_whitespace() {
|
||||
let phrase = generate_mnemonic(MnemonicWords::TwentyFour).unwrap();
|
||||
let padded = format!(" {phrase} ");
|
||||
assert_eq!(
|
||||
seed_from_mnemonic(&phrase).unwrap(),
|
||||
seed_from_mnemonic(&padded).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mnemonic_words_count_round_trip() {
|
||||
assert_eq!(MnemonicWords::Twelve.count(), 12);
|
||||
assert_eq!(MnemonicWords::TwentyFour.count(), 24);
|
||||
assert_eq!(MnemonicWords::from_count(12).unwrap(), MnemonicWords::Twelve);
|
||||
assert_eq!(
|
||||
MnemonicWords::from_count(24).unwrap(),
|
||||
MnemonicWords::TwentyFour
|
||||
);
|
||||
assert!(MnemonicWords::from_count(18).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
// Catches inverse with hint for FromStr users.
|
||||
impl std::str::FromStr for MnemonicWords {
|
||||
type Err = KezError;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
Self::from_count(
|
||||
s.parse::<usize>()
|
||||
.map_err(|_| KezError::InvalidIdentity(format!("not a number: {s}")))?,
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user