diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 4bbd762..75b3df8 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -6,6 +6,7 @@ import ( "testing" "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" "golang.org/x/crypto/ssh" @@ -62,6 +63,14 @@ func TestThePrivateKeyMatchesThePublicOne(t *testing.T) { require.Equal(t, vectorOne, back) } +func TestAnIndexWithNoHardenedChildIsRefused(t *testing.T) { + t.Setenv(mnemonic.Variable, example()) + + out, err := execute(t, "ssh", "pub", "-n", "2147483648") + require.ErrorIs(t, err, derive.ErrIndexTooLarge) + require.Empty(t, out) +} + func TestTheMnemonicCommandIsUsed(t *testing.T) { t.Setenv(mnemonic.Variable, "") @@ -77,6 +86,17 @@ func TestTheMnemonicCommandIsUsed(t *testing.T) { func run(t *testing.T, args ...string) string { t.Helper() + out, err := execute(t, args...) + require.NoError(t, err) + + return out +} + +// execute runs the tool and returns both what it wrote and how it +// ended. +func execute(t *testing.T, args ...string) (string, error) { + t.Helper() + var out bytes.Buffer root := cli.Root() @@ -84,7 +104,7 @@ func run(t *testing.T, args ...string) string { root.SetErr(&out) root.SetArgs(args) - require.NoError(t, root.ExecuteContext(t.Context())) + err := root.ExecuteContext(t.Context()) - return out.String() + return out.String(), err } diff --git a/internal/derive/derive.go b/internal/derive/derive.go index 49265e4..fe9176c 100644 --- a/internal/derive/derive.go +++ b/internal/derive/derive.go @@ -2,6 +2,7 @@ package derive import ( + "errors" "fmt" "git.eeqj.de/sneak/secret/pkg/bip85" @@ -16,8 +17,18 @@ const ( // Size is how many bytes every key type is given. Size = 32 + + // MaxIndex is the largest key index there is. Every element of + // the path is hardened, and a hardened BIP-32 child index stops + // here. + MaxIndex = 1<<31 - 1 ) +// ErrIndexTooLarge is returned for a key index above MaxIndex. Such an +// index has no hardened child to derive, so there is no key to give +// back rather than a key nobody else would reproduce. +var ErrIndexTooLarge = errors.New("the key index is too large") + // Path returns the derivation path for an application number and a key // index. func Path(application, index uint32) string { @@ -28,7 +39,15 @@ func Path(application, index uint32) string { // The mnemonic becomes a seed with an empty passphrase, the seed // becomes a master key, the master key gives BIP-85 entropy at the // path, and the entropy seeds the generator the bytes are read from. +// An index above MaxIndex is refused before any of that happens. func Bytes(words string, application, index uint32) ([]byte, error) { + if index > MaxIndex { + return nil, fmt.Errorf( + "%w: %d is above %d", + ErrIndexTooLarge, index, MaxIndex, + ) + } + seed := bip39.NewSeed(words, "") master, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams) diff --git a/internal/derive/derive_test.go b/internal/derive/derive_test.go index d5461ac..75f8b70 100644 --- a/internal/derive/derive_test.go +++ b/internal/derive/derive_test.go @@ -38,6 +38,16 @@ func TestEveryIndexGivesItsOwnBytes(t *testing.T) { require.False(t, bytes.Equal(first, second)) } +func TestAnIndexWithNoHardenedChildIsRefused(t *testing.T) { + t.Parallel() + + _, err := derive.Bytes(example(), application, derive.MaxIndex+1) + require.ErrorIs(t, err, derive.ErrIndexTooLarge) + + _, err = derive.Bytes(example(), application, derive.MaxIndex) + require.NoError(t, err) +} + func TestTheSameInputAlwaysGivesTheSameBytes(t *testing.T) { t.Parallel() diff --git a/script/install-precommit b/script/install-precommit index bef6406..723bae1 100755 --- a/script/install-precommit +++ b/script/install-precommit @@ -8,8 +8,8 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" main() { cd "$ROOT" hook=".git/hooks/pre-commit" - printf '#!/bin/sh\nset -e\nscript/precommit\n' > .git/hooks/pre-commit - chmod +x .git/hooks/pre-commit + printf '#!/bin/sh\nset -e\nscript/precommit\n' > "$hook" + chmod +x "$hook" echo "pre-commit hook installed: runs script/precommit" }