Drop the BIP39 passphrase from cbk

The seed is always computed with an empty passphrase; Params is just
the mnemonic and the BIP85 index.
This commit is contained in:
2026-10-31 11:41:11 -07:00
parent fe6dbba428
commit ad1d51a9f9
3 changed files with 30 additions and 77 deletions
+14 -17
View File
@@ -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.
+9 -53
View File
@@ -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()