The mnemonic command: child mnemonics (closes #4)
All checks were successful
check / check (push) Successful in 19s

keyfunc mnemonic prints a child mnemonic derived from the main one with
BIP-85's own mnemonic application, in English, at 12, 18 or 24 words.
Its entropy is the BIP-85 entropy cut to the length the word count
needs, which is what that application asks for, so it does not go
through the generator the other key types read their bytes from. The
BIP-85 specification's own test vectors for the application are the
tests.

Two pieces the derivation package already had inside one function are
now named: the master key a mnemonic stands for, and the refusal of a
key index that has no hardened child. Both key types call them.

Model: opus-5
This commit is contained in:
clawbot
2026-09-07 15:43:10 +00:00
parent 279cba6bcf
commit cc68529c5f
6 changed files with 309 additions and 10 deletions

View File

@@ -5,10 +5,12 @@ import (
"strings"
"testing"
"git.eeqj.de/sneak/keyfunc/internal/childmnemonic"
"git.eeqj.de/sneak/keyfunc/internal/cli"
"git.eeqj.de/sneak/keyfunc/internal/derive"
"git.eeqj.de/sneak/keyfunc/internal/mnemonic"
"github.com/stretchr/testify/require"
bip39 "github.com/tyler-smith/go-bip39"
"golang.org/x/crypto/ssh"
)
@@ -20,6 +22,12 @@ const (
"0I4FKs+eVUulTPHfk9VtXw1tMF"
)
// The two child mnemonic lengths the tests ask for.
const (
twelve = 12
twentyFour = 24
)
// example returns the mnemonic the README gives its test vectors for:
// eleven abandons and about.
func example() string {
@@ -81,6 +89,28 @@ func TestTheMnemonicCommandIsUsed(t *testing.T) {
require.Equal(t, vectorZero+" keyfunc/ssh/0", line)
}
func TestAChildMnemonicIsPrinted(t *testing.T) {
t.Setenv(mnemonic.Variable, example())
short := strings.Fields(run(t, "mnemonic"))
require.Len(t, short, twelve)
require.True(t, bip39.IsMnemonicValid(strings.Join(short, " ")))
long := strings.Fields(run(t, "mnemonic", "--words", "24"))
require.Len(t, long, twentyFour)
next := strings.Fields(run(t, "mnemonic", "-n", "1"))
require.NotEqual(t, short, next)
}
func TestALengthTheToolDoesNotOfferIsRefused(t *testing.T) {
t.Setenv(mnemonic.Variable, example())
out, err := execute(t, "mnemonic", "--words", "15")
require.ErrorIs(t, err, childmnemonic.ErrWordCount)
require.Empty(t, out)
}
// run executes the tool with the given arguments and returns what it
// wrote to standard output.
func run(t *testing.T, args ...string) string {