Compare commits
1 Commits
4b8373ed1f
...
05d67708ec
| Author | SHA1 | Date | |
|---|---|---|---|
| 05d67708ec |
@@ -1,63 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.eeqj.de/sneak/keyfunc/internal/cli/mnemonic"
|
|
||||||
"git.eeqj.de/sneak/keyfunc/internal/cli/options"
|
"git.eeqj.de/sneak/keyfunc/internal/cli/options"
|
||||||
"git.eeqj.de/sneak/keyfunc/internal/cli/ssh"
|
"git.eeqj.de/sneak/keyfunc/internal/cli/ssh"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -31,7 +30,7 @@ func Root() *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
options.Add(root)
|
options.Add(root)
|
||||||
root.AddCommand(ssh.Command(), mnemonic.Command())
|
root.AddCommand(ssh.Command())
|
||||||
|
|
||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.eeqj.de/sneak/keyfunc/internal/childmnemonic"
|
|
||||||
"git.eeqj.de/sneak/keyfunc/internal/cli"
|
"git.eeqj.de/sneak/keyfunc/internal/cli"
|
||||||
"git.eeqj.de/sneak/keyfunc/internal/derive"
|
"git.eeqj.de/sneak/keyfunc/internal/derive"
|
||||||
"git.eeqj.de/sneak/keyfunc/internal/mnemonic"
|
"git.eeqj.de/sneak/keyfunc/internal/mnemonic"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
bip39 "github.com/tyler-smith/go-bip39"
|
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,12 +20,6 @@ const (
|
|||||||
"0I4FKs+eVUulTPHfk9VtXw1tMF"
|
"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:
|
// example returns the mnemonic the README gives its test vectors for:
|
||||||
// eleven abandons and about.
|
// eleven abandons and about.
|
||||||
func example() string {
|
func example() string {
|
||||||
@@ -89,28 +81,6 @@ func TestTheMnemonicCommandIsUsed(t *testing.T) {
|
|||||||
require.Equal(t, vectorZero+" keyfunc/ssh/0", line)
|
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
|
// run executes the tool with the given arguments and returns what it
|
||||||
// wrote to standard output.
|
// wrote to standard output.
|
||||||
func run(t *testing.T, args ...string) string {
|
func run(t *testing.T, args ...string) string {
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
// Package mnemonic is the command that prints a child mnemonic.
|
|
||||||
package mnemonic
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.eeqj.de/sneak/keyfunc/internal/childmnemonic"
|
|
||||||
"git.eeqj.de/sneak/keyfunc/internal/cli/options"
|
|
||||||
"git.eeqj.de/sneak/keyfunc/internal/derive"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Command returns the mnemonic command.
|
|
||||||
func Command() *cobra.Command {
|
|
||||||
cmd := &cobra.Command{
|
|
||||||
Use: "mnemonic",
|
|
||||||
Short: "print a child mnemonic derived from the main one",
|
|
||||||
Args: cobra.NoArgs,
|
|
||||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
||||||
child, err := derived(cmd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = fmt.Fprintln(cmd.OutOrStdout(), child)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("writing the mnemonic: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Flags().Uint32(
|
|
||||||
"words", childmnemonic.DefaultWords,
|
|
||||||
"how long the child mnemonic is: 12, 18 or 24 words",
|
|
||||||
)
|
|
||||||
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// derived returns the child mnemonic this run asks for.
|
|
||||||
func derived(cmd *cobra.Command) (string, error) {
|
|
||||||
index, err := options.Index(cmd)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
words, err := cmd.Flags().GetUint32("words")
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("reading the word count: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
parent, err := options.Mnemonic(cmd)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
master, err := derive.Master(parent)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return childmnemonic.Derive(master, words, index)
|
|
||||||
}
|
|
||||||
@@ -35,47 +35,24 @@ func Path(application, index uint32) string {
|
|||||||
return fmt.Sprintf("m/%d'/%d'/%d'", purpose, application, index)
|
return fmt.Sprintf("m/%d'/%d'/%d'", purpose, application, index)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckIndex refuses a key index above MaxIndex, so that every key
|
|
||||||
// type turns such an index down before deriving anything.
|
|
||||||
func CheckIndex(index uint32) error {
|
|
||||||
if index > MaxIndex {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"%w: %d is above %d",
|
|
||||||
ErrIndexTooLarge, index, MaxIndex,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Master returns the BIP-32 master key a mnemonic stands for: the
|
|
||||||
// mnemonic becomes a seed with an empty passphrase, and the seed
|
|
||||||
// becomes the key.
|
|
||||||
func Master(words string) (*hdkeychain.ExtendedKey, error) {
|
|
||||||
seed := bip39.NewSeed(words, "")
|
|
||||||
|
|
||||||
master, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("making the master key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return master, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bytes returns the bytes for an application number and a key index.
|
// Bytes returns the bytes for an application number and a key index.
|
||||||
// The mnemonic becomes a seed with an empty passphrase, the seed
|
// The mnemonic becomes a seed with an empty passphrase, the seed
|
||||||
// becomes a master key, the master key gives BIP-85 entropy at the
|
// 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.
|
// path, and the entropy seeds the generator the bytes are read from.
|
||||||
// An index above MaxIndex is refused before any of that happens.
|
// An index above MaxIndex is refused before any of that happens.
|
||||||
func Bytes(words string, application, index uint32) ([]byte, error) {
|
func Bytes(words string, application, index uint32) ([]byte, error) {
|
||||||
err := CheckIndex(index)
|
if index > MaxIndex {
|
||||||
if err != nil {
|
return nil, fmt.Errorf(
|
||||||
return nil, err
|
"%w: %d is above %d",
|
||||||
|
ErrIndexTooLarge, index, MaxIndex,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
master, err := Master(words)
|
seed := bip39.NewSeed(words, "")
|
||||||
|
|
||||||
|
master, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("making the master key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
entropy, err := bip85.DeriveBIP85Entropy(master, Path(application, index))
|
entropy, err := bip85.DeriveBIP85Entropy(master, Path(application, index))
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ func TestTheAgentServesTheOneKeyAndNothingElse(t *testing.T) {
|
|||||||
|
|
||||||
served, err := key.Serve(t.Context(), "a comment")
|
served, err := key.Serve(t.Context(), "a comment")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(served.Stop)
|
|
||||||
|
|
||||||
directory, err := os.Stat(filepath.Dir(served.Socket()))
|
directory, err := os.Stat(filepath.Dir(served.Socket()))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
Reference in New Issue
Block a user