From ad1d51a9f9c25d15849013065367d82000f8ad1b Mon Sep 17 00:00:00 2001 From: sneak Date: Sat, 31 Oct 2026 11:41:11 -0700 Subject: [PATCH] Drop the BIP39 passphrase from cbk The seed is always computed with an empty passphrase; Params is just the mnemonic and the BIP85 index. --- README.md | 14 +++++----- pkg/cbk/cbk.go | 31 ++++++++++------------- pkg/cbk/cbk_test.go | 62 +++++++-------------------------------------- 3 files changed, 30 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index fe82d26..2b56318 100644 --- a/README.md +++ b/README.md @@ -66,9 +66,9 @@ func main() { panic(err) } - // The same mnemonic (and optional passphrase) always yields the same - // keypair, on any machine. The keys are plain crypto_box keys for use - // with golang.org/x/crypto/nacl/box. + // The same mnemonic always yields the same keypair, on any machine. + // The keys are plain crypto_box keys for use with + // golang.org/x/crypto/nacl/box. keypair, err := cbk.New(cbk.Params{Mnemonic: mnemonic}) if err != nil { panic(err) @@ -138,8 +138,8 @@ independent keypairs: 1. The mnemonic is validated and canonicalized with `bip39wordlist.NormalizeWordlist`. 2. The standard 64-byte BIP39 seed is computed with PBKDF2-HMAC-SHA512 (2048 - rounds, salt `"mnemonic" + passphrase`), exactly as a BIP39 wallet would, so - the optional passphrase works as the usual "25th word". + rounds, salt `"mnemonic"`), exactly as a BIP39 wallet would with an empty + passphrase. BIP39 passphrases are deliberately not supported. 3. The seed becomes a BIP32 master key. 4. BIP85's HEX application derives 32 bytes of entropy at `m/83696968'/128169'/32'/{index}'`; those bytes are the private key. @@ -148,8 +148,8 @@ independent keypairs: The resulting keys are plain 32-byte crypto_box keys for use with `golang.org/x/crypto/nacl/box`. The API: -- `cbk.New(cbk.Params{Mnemonic, Passphrase, Index})` and `cbk.FromMnemonic(m)` - (no passphrase, index 0) return a `CryptoBoxKeypair`. +- `cbk.New(cbk.Params{Mnemonic, Index})` and `cbk.FromMnemonic(m)` (index 0) + return a `CryptoBoxKeypair`. - `CryptoBoxKey.Hex()`, `CryptoBoxKey.Bytes()`, and the `PublicKeyHex`, `PrivateKeyHex`, `PublicKeyBytes`, `PrivateKeyBytes` accessors on the keypair. diff --git a/pkg/cbk/cbk.go b/pkg/cbk/cbk.go index 3e241c1..814b5f5 100644 --- a/pkg/cbk/cbk.go +++ b/pkg/cbk/cbk.go @@ -3,13 +3,14 @@ // resulting keys are plain 32-byte crypto_box keys for use with // golang.org/x/crypto/nacl/box. // -// The same mnemonic, passphrase, and index always produce the same keypair: +// The same mnemonic and index always produce the same keypair: // // 1. The mnemonic is validated and canonicalized with // bip39wordlist.NormalizeWordlist (lowercase, single spaces). // 2. The standard 64-byte BIP39 seed is computed from the canonical -// mnemonic and the optional passphrase with PBKDF2-HMAC-SHA512 (2048 -// rounds, salt "mnemonic" + passphrase), exactly as a wallet would. +// mnemonic with PBKDF2-HMAC-SHA512 (2048 rounds, salt "mnemonic"), +// exactly as a wallet would with an empty passphrase. BIP39 +// passphrases are deliberately not supported. // 3. The seed becomes a BIP32 master key. // 4. BIP85 (sneak.berlin/go/secret/pkg/bip85) derives 32 bytes of // application entropy from the master key with the HEX application, @@ -70,11 +71,6 @@ type Params struct { // whitespace between words is accepted; it is normalized before use. Mnemonic string - // Passphrase is the optional BIP39 passphrase (sometimes called the - // "25th word"). It is used verbatim: it is case sensitive and is not - // trimmed. An empty passphrase is the common case. - Passphrase string - // Index selects which of the keypairs derivable from the mnemonic to // return. It is the final component of the BIP85 derivation path, so // each index yields an independent keypair. Zero is the first. @@ -87,9 +83,9 @@ type CryptoBoxKeypair struct { PrivateKey CryptoBoxKey } -// New derives the keypair for the mnemonic, passphrase, and index in p. If -// the mnemonic is not valid it returns an error wrapping one of the -// bip39wordlist sentinel errors. +// New derives the keypair for the mnemonic and index in p. If the mnemonic +// is not valid it returns an error wrapping one of the bip39wordlist +// sentinel errors. func New(p Params) (CryptoBoxKeypair, error) { if p.Index > MaxIndex { return CryptoBoxKeypair{}, fmt.Errorf("%w: %d", ErrIndexTooLarge, p.Index) @@ -100,10 +96,11 @@ func New(p Params) (CryptoBoxKeypair, error) { return CryptoBoxKeypair{}, err } - // The network parameters only choose the version bytes used when a - // key is serialized; they do not affect derivation. + // The seed is always computed with an empty BIP39 passphrase. The + // network parameters only choose the version bytes used when a key is + // serialized; they do not affect derivation. masterKey, err := hdkeychain.NewMaster( - bip39.NewSeed(mnemonic, p.Passphrase), &chaincfg.MainNetParams, + bip39.NewSeed(mnemonic, ""), &chaincfg.MainNetParams, ) if err != nil { return CryptoBoxKeypair{}, fmt.Errorf("cbk: deriving BIP32 master key: %w", err) @@ -130,10 +127,10 @@ func New(p Params) (CryptoBoxKeypair, error) { }, nil } -// FromMnemonic derives the keypair for mnemonic with an empty passphrase -// at index zero. It is shorthand for New(Params{Mnemonic: mnemonic}). +// FromMnemonic derives the keypair for mnemonic at index zero. It is +// shorthand for New(Params{Mnemonic: mnemonic}). func FromMnemonic(mnemonic string) (CryptoBoxKeypair, error) { - return New(Params{Mnemonic: mnemonic, Passphrase: "", Index: 0}) + return New(Params{Mnemonic: mnemonic, Index: 0}) } // PublicKeyBytes returns a copy of the public key as a byte slice. diff --git a/pkg/cbk/cbk_test.go b/pkg/cbk/cbk_test.go index c4ec30a..c22d684 100644 --- a/pkg/cbk/cbk_test.go +++ b/pkg/cbk/cbk_test.go @@ -16,9 +16,9 @@ import ( ) // The BIP85 specification publishes its test vectors against one master -// key. specMnemonic is the BIP39 phrase that produces that master key with -// an empty passphrase, so the whole mnemonic-to-entropy chain this package -// relies on can be checked against the specification. +// key. specMnemonic is the BIP39 phrase that produces that master key, so +// the whole mnemonic-to-entropy chain this package relies on can be +// checked against the specification. const ( specMnemonic = "install scatter logic circle pencil average fall shoe " + "quantum disease suspect usage" @@ -72,11 +72,7 @@ func TestPrivateKeyIsBIP85HexApplicationOutput(t *testing.T) { expected, err := bip85.DeriveHex(masterKey, cbk.KeySize, index) require.NoError(t, err) - keypair, err := cbk.New(cbk.Params{ - Mnemonic: specMnemonic, - Passphrase: "", - Index: index, - }) + keypair, err := cbk.New(cbk.Params{Mnemonic: specMnemonic, Index: index}) require.NoError(t, err) assert.Equal(t, expected, keypair.PrivateKeyHex(), "index %d", index) @@ -86,10 +82,10 @@ func TestPrivateKeyIsBIP85HexApplicationOutput(t *testing.T) { func TestNewIndexSelectsIndependentKeypairs(t *testing.T) { t.Parallel() - first, err := cbk.New(cbk.Params{Mnemonic: specMnemonic, Passphrase: "", Index: 0}) + first, err := cbk.New(cbk.Params{Mnemonic: specMnemonic, Index: 0}) require.NoError(t, err) - second, err := cbk.New(cbk.Params{Mnemonic: specMnemonic, Passphrase: "", Index: 1}) + second, err := cbk.New(cbk.Params{Mnemonic: specMnemonic, Index: 1}) require.NoError(t, err) assert.NotEqual(t, first.PublicKey, second.PublicKey) @@ -103,18 +99,10 @@ func TestNewIndexSelectsIndependentKeypairs(t *testing.T) { func TestNewRejectsIndexBeyondHardenedRange(t *testing.T) { t.Parallel() - _, err := cbk.New(cbk.Params{ - Mnemonic: specMnemonic, - Passphrase: "", - Index: cbk.MaxIndex + 1, - }) + _, err := cbk.New(cbk.Params{Mnemonic: specMnemonic, Index: cbk.MaxIndex + 1}) require.ErrorIs(t, err, cbk.ErrIndexTooLarge) - _, err = cbk.New(cbk.Params{ - Mnemonic: specMnemonic, - Passphrase: "", - Index: cbk.MaxIndex, - }) + _, err = cbk.New(cbk.Params{Mnemonic: specMnemonic, Index: cbk.MaxIndex}) require.NoError(t, err) } @@ -128,44 +116,12 @@ func TestNewNormalizesMnemonic(t *testing.T) { messy := " " + strings.ToUpper(words[0]) + "\t" + strings.Join(words[1:], " ") + "\n" - normalized, err := cbk.New(cbk.Params{ - Mnemonic: messy, - Passphrase: "", - Index: 0, - }) + normalized, err := cbk.New(cbk.Params{Mnemonic: messy, Index: 0}) require.NoError(t, err) assert.Equal(t, canonical, normalized) } -func TestNewPassphraseChangesKeys(t *testing.T) { - t.Parallel() - - plain, err := cbk.FromMnemonic(specMnemonic) - require.NoError(t, err) - - extraWord := "TREZOR" - - withExtraWord, err := cbk.New(cbk.Params{ - Mnemonic: specMnemonic, - Passphrase: extraWord, - Index: 0, - }) - require.NoError(t, err) - - assert.NotEqual(t, plain.PublicKey, withExtraWord.PublicKey) - assert.NotEqual(t, plain.PrivateKey, withExtraWord.PrivateKey) - - again, err := cbk.New(cbk.Params{ - Mnemonic: specMnemonic, - Passphrase: extraWord, - Index: 0, - }) - require.NoError(t, err) - - assert.Equal(t, withExtraWord, again, "derivation is deterministic") -} - func TestNewDifferentMnemonicsGiveDifferentKeys(t *testing.T) { t.Parallel()