A mnemonic typed with extra whitespace derives a different long-term key, so the vault cannot be recovered with the same words #135

Closed
opened 2026-10-13 00:29:17 +02:00 by clawbot · 0 comments
Collaborator

Severity: critical
The vault's long-term key is derived from the mnemonic text exactly as typed, so a vault made with a mnemonic that had an extra space cannot be opened with the same words typed normally, and the recovery the README promises ("the vault still opens with its mnemonic") fails for it.

What happens: secret init and secret vault create validate the mnemonic with bip39.IsMnemonicValid, which splits on any whitespace, but then derive the key from the raw string: bip39.NewSeed feeds the bytes as given into PBKDF2. Two spaces between words, or a trailing space or tab (easy in SB_SECRET_MNEMONIC="..." or a pasted line), give a different seed from the single-spaced sentence BIP39 defines, so the vault's key matches no other BIP39 tool's and not the words the user wrote down.

Reproduction (M is a valid 12-word mnemonic, M2 the same words with two spaces after the first word):

SB_UNLOCK_PASSPHRASE=pw SB_SECRET_MNEMONIC="$M2" secret init
echo -n x | secret add db/pass
SB_SECRET_MNEMONIC="$M" secret unlocker add passphrase
# Error: ... derived public key does not match vault: mnemonic may be incorrect
SB_SECRET_MNEMONIC="$M" secret get db/pass      # same error

A second state directory initialised with M gets a different pub.age at the same derivation index 0.

Where:

mnemonic := mnemonicBuffer.String()
(processMnemonicForVault passes the raw string on),
seed := bip39.NewSeed(mnemonic, "")
(DeriveEntropy),
func NewSeed(mnemonic string, password string) []byte {
(NewSeed) against
words := strings.Fields(mnemonic)
(validation splits on strings.Fields). The same raw string reaches deriveLongTermKeyFromMnemonic and GetNextDerivationIndex.

Correct behaviour: derive from the BIP39 sentence, the words joined by single spaces, NFKD-normalised, wherever the mnemonic is used (init, vault create, vault import, unlocker add, every unlock); the words as typed with any whitespace then always give the same key.

Definition of done

  • A mnemonic typed with extra or trailing whitespace creates a vault with the same key as the single-spaced sentence, and opens it.
  • A regression test derives a key from "w1 w2 ... " and from "w1 w2 ..." and requires the same identity; it fails before the fix.

Model: fable-5-1

Severity: critical The vault's long-term key is derived from the mnemonic text exactly as typed, so a vault made with a mnemonic that had an extra space cannot be opened with the same words typed normally, and the recovery the README promises ("the vault still opens with its mnemonic") fails for it. What happens: `secret init` and `secret vault create` validate the mnemonic with `bip39.IsMnemonicValid`, which splits on any whitespace, but then derive the key from the raw string: `bip39.NewSeed` feeds the bytes as given into PBKDF2. Two spaces between words, or a trailing space or tab (easy in `SB_SECRET_MNEMONIC="..."` or a pasted line), give a different seed from the single-spaced sentence BIP39 defines, so the vault's key matches no other BIP39 tool's and not the words the user wrote down. Reproduction (`M` is a valid 12-word mnemonic, `M2` the same words with two spaces after the first word): ``` SB_UNLOCK_PASSPHRASE=pw SB_SECRET_MNEMONIC="$M2" secret init echo -n x | secret add db/pass SB_SECRET_MNEMONIC="$M" secret unlocker add passphrase # Error: ... derived public key does not match vault: mnemonic may be incorrect SB_SECRET_MNEMONIC="$M" secret get db/pass # same error ``` A second state directory initialised with `M` gets a different `pub.age` at the same derivation index 0. Where: https://git.eeqj.de/sneak/secret/src/commit/ef0ae90768c8b18a829033b3ebd4cf98fcaeb3d1/internal/vault/management.go#L169 (`processMnemonicForVault` passes the raw string on), https://git.eeqj.de/sneak/secret/src/commit/ef0ae90768c8b18a829033b3ebd4cf98fcaeb3d1/pkg/agehd/agehd.go#L77 (`DeriveEntropy`), https://git.eeqj.de/sneak/secret/src/commit/ef0ae90768c8b18a829033b3ebd4cf98fcaeb3d1/internal/bip39/bip39.go#L197 (`NewSeed`) against https://git.eeqj.de/sneak/secret/src/commit/ef0ae90768c8b18a829033b3ebd4cf98fcaeb3d1/internal/bip39/bip39.go#L274 (validation splits on `strings.Fields`). The same raw string reaches `deriveLongTermKeyFromMnemonic` and `GetNextDerivationIndex`. Correct behaviour: derive from the BIP39 sentence, the words joined by single spaces, NFKD-normalised, wherever the mnemonic is used (`init`, `vault create`, `vault import`, `unlocker add`, every unlock); the words as typed with any whitespace then always give the same key. **Definition of done** - A mnemonic typed with extra or trailing whitespace creates a vault with the same key as the single-spaced sentence, and opens it. - A regression test derives a key from `"w1 w2 ... "` and from `"w1 w2 ..."` and requires the same identity; it fails before the fix. Model: fable-5-1
clawbot added this to the 1.0.0 milestone 2026-10-13 00:29:17 +02:00
clawbot added the critical label 2026-10-13 00:29:17 +02:00
clawbot self-assigned this 2026-10-13 00:29:17 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/secret#135