The mnemonic command: child mnemonics (closes #4)
All checks were successful
check / check (push) Successful in 4s
All checks were successful
check / check (push) Successful in 4s
keyfunc mnemonic derives a 12, 18 or 24 word child mnemonic through BIP-85's own mnemonic application, with the specification's test vectors as tests. One review round, passed with no findings; the reviewer reproduced the vectors from an implementation written from the specification alone. Model: opus-5 (implementation and review); fable-5-1 (landing)
This commit was merged in pull request #6.
This commit is contained in:
63
internal/childmnemonic/childmnemonic.go
Normal file
63
internal/childmnemonic/childmnemonic.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// Package childmnemonic derives a mnemonic from another mnemonic.
|
||||
package childmnemonic
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.eeqj.de/sneak/keyfunc/internal/derive"
|
||||
"git.eeqj.de/sneak/secret/pkg/bip85"
|
||||
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||
bip39 "github.com/tyler-smith/go-bip39"
|
||||
)
|
||||
|
||||
// english is the number BIP-85 gives the English word list.
|
||||
const english = 0
|
||||
|
||||
// The lengths a child mnemonic may have. BIP-85 also allows 15 and 21
|
||||
// words; these three are the ones this tool offers.
|
||||
const (
|
||||
twelve = 12
|
||||
eighteen = 18
|
||||
twentyFour = 24
|
||||
)
|
||||
|
||||
// DefaultWords is how long a child mnemonic is when the user does not
|
||||
// say.
|
||||
const DefaultWords = twelve
|
||||
|
||||
// ErrWordCount is returned for a length this tool does not offer.
|
||||
var ErrWordCount = errors.New("a child mnemonic has 12, 18 or 24 words")
|
||||
|
||||
// Derive returns the English child mnemonic of that many words at that
|
||||
// key index, taken from the master key with BIP-85's own mnemonic
|
||||
// application, number 39. The words come straight from the BIP-85
|
||||
// entropy, cut to the length the word count needs, rather than from
|
||||
// the generator the other key types read their bytes from.
|
||||
func Derive(
|
||||
master *hdkeychain.ExtendedKey,
|
||||
words, index uint32,
|
||||
) (string, error) {
|
||||
switch words {
|
||||
case twelve, eighteen, twentyFour:
|
||||
default:
|
||||
return "", fmt.Errorf("%w, not %d", ErrWordCount, words)
|
||||
}
|
||||
|
||||
err := derive.CheckIndex(index)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
entropy, err := bip85.DeriveBIP39Entropy(master, english, words, index)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("deriving entropy: %w", err)
|
||||
}
|
||||
|
||||
child, err := bip39.NewMnemonic(entropy)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("turning the entropy into words: %w", err)
|
||||
}
|
||||
|
||||
return child, nil
|
||||
}
|
||||
117
internal/childmnemonic/childmnemonic_test.go
Normal file
117
internal/childmnemonic/childmnemonic_test.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package childmnemonic_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.eeqj.de/sneak/keyfunc/internal/childmnemonic"
|
||||
"git.eeqj.de/sneak/keyfunc/internal/derive"
|
||||
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||
"github.com/stretchr/testify/require"
|
||||
bip39 "github.com/tyler-smith/go-bip39"
|
||||
)
|
||||
|
||||
// The lengths the tool offers, and one it does not.
|
||||
const (
|
||||
twelve = 12
|
||||
eighteen = 18
|
||||
twentyFour = 24
|
||||
fifteen = 15
|
||||
)
|
||||
|
||||
// specificationKey is the master key the BIP-85 specification gives
|
||||
// every one of its test vectors for.
|
||||
const specificationKey = "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBq" +
|
||||
"sx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb"
|
||||
|
||||
// The three English child mnemonics at key index 0 that the BIP-85
|
||||
// specification gives for that master key.
|
||||
const (
|
||||
twelveWords = "girl mad pet galaxy egg matter matrix prison refuse " +
|
||||
"sense ordinary nose"
|
||||
|
||||
eighteenWords = "near account window bike charge season chef number " +
|
||||
"sketch tomorrow excuse sniff circle vital hockey outdoor " +
|
||||
"supply token"
|
||||
|
||||
twentyFourWords = "puppy ocean match cereal symbol another shed " +
|
||||
"magic wrap hammer bulb intact gadget divorce twin tonight " +
|
||||
"reason outdoor destroy simple truth cigar social volcano"
|
||||
)
|
||||
|
||||
// example returns the mnemonic every BIP-39 document uses to show its
|
||||
// test vectors: eleven abandons and about.
|
||||
func example() string {
|
||||
return strings.Repeat("abandon ", 11) + "about"
|
||||
}
|
||||
|
||||
func TestTheSpecificationTestVectors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require.Equal(t, twelveWords, fromSpecificationKey(t, twelve))
|
||||
require.Equal(t, eighteenWords, fromSpecificationKey(t, eighteen))
|
||||
require.Equal(t, twentyFourWords, fromSpecificationKey(t, twentyFour))
|
||||
}
|
||||
|
||||
func TestTheLongestChildMnemonicPassesItsOwnChecksum(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
child := fromSpecificationKey(t, twentyFour)
|
||||
|
||||
require.Len(t, strings.Fields(child), twentyFour)
|
||||
require.True(t, bip39.IsMnemonicValid(child))
|
||||
}
|
||||
|
||||
func TestEachKeyIndexGivesItsOwnChildMnemonic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
master, err := derive.Master(example())
|
||||
require.NoError(t, err)
|
||||
|
||||
first, err := childmnemonic.Derive(master, twelve, 0)
|
||||
require.NoError(t, err)
|
||||
require.True(t, bip39.IsMnemonicValid(first))
|
||||
|
||||
second, err := childmnemonic.Derive(master, twelve, 1)
|
||||
require.NoError(t, err)
|
||||
require.True(t, bip39.IsMnemonicValid(second))
|
||||
|
||||
require.NotEqual(t, first, second)
|
||||
}
|
||||
|
||||
func TestALengthTheToolDoesNotOfferIsRefused(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
master, err := hdkeychain.NewKeyFromString(specificationKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = childmnemonic.Derive(master, fifteen, 0)
|
||||
require.ErrorIs(t, err, childmnemonic.ErrWordCount)
|
||||
}
|
||||
|
||||
func TestAnIndexWithNoHardenedChildIsRefused(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
master, err := hdkeychain.NewKeyFromString(specificationKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = childmnemonic.Derive(master, twelve, derive.MaxIndex+1)
|
||||
require.ErrorIs(t, err, derive.ErrIndexTooLarge)
|
||||
|
||||
_, err = childmnemonic.Derive(master, twelve, derive.MaxIndex)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// fromSpecificationKey derives the child mnemonic of that length at key
|
||||
// index 0 from the master key the specification gives.
|
||||
func fromSpecificationKey(t *testing.T, words uint32) string {
|
||||
t.Helper()
|
||||
|
||||
master, err := hdkeychain.NewKeyFromString(specificationKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
child, err := childmnemonic.Derive(master, words, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
return child
|
||||
}
|
||||
Reference in New Issue
Block a user