Compare commits
2 Commits
73c80ab173
...
a44164a6f5
| Author | SHA1 | Date | |
|---|---|---|---|
| a44164a6f5 | |||
| d69bed722a |
@@ -11,13 +11,18 @@ import (
|
||||
)
|
||||
|
||||
// The recipients the example mnemonic produces at the first two
|
||||
// indexes. They are what makes the derivation reproducible: if these
|
||||
// change, every file anyone encrypted becomes unreadable.
|
||||
// indexes, and the secret key behind the first of them. They are what
|
||||
// makes the derivation reproducible: if the recipients change, every
|
||||
// file anyone encrypted becomes unreadable, and if the secret key
|
||||
// changes, the key is no longer the one other tools derive from the
|
||||
// same mnemonic.
|
||||
const (
|
||||
recipientZero = "age1xwdy9y6ckyfsgjc8k02e9uhsf3fmjy0ufysew" +
|
||||
"lj68kmx5n67e3nsg2mftq"
|
||||
recipientOne = "age1pmm92sxaf5mazjwvjph7dx2zq9r5p8l3rarfg" +
|
||||
"qm7hmakqhvgyy4q5p3w7j"
|
||||
identityZero = "AGE-SECRET-KEY-19QKK2P38598XLXMQFFU3P7J9PLDD" +
|
||||
"7527T70JDHGDJ7AMNF3XT44S00JFU5"
|
||||
)
|
||||
|
||||
// example returns the mnemonic every BIP-39 document uses to show its
|
||||
@@ -40,12 +45,10 @@ func TestTheSameMnemonicAlwaysGivesTheSameKey(t *testing.T) {
|
||||
require.Equal(t, recipientOne, forIndex(t, 1).Recipient())
|
||||
}
|
||||
|
||||
func TestTheIdentityIsWrittenTheWayAgeWritesOne(t *testing.T) {
|
||||
func TestTheSameMnemonicAlwaysGivesTheSameSecretKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require.True(t,
|
||||
strings.HasPrefix(forIndex(t, 0).Identity(), "AGE-SECRET-KEY-1"),
|
||||
)
|
||||
require.Equal(t, identityZero, forIndex(t, 0).Identity())
|
||||
}
|
||||
|
||||
func TestWhatWasEncryptedComesBack(t *testing.T) {
|
||||
|
||||
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
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"git.eeqj.de/sneak/keyfunc/internal/agekey"
|
||||
"git.eeqj.de/sneak/keyfunc/internal/cli/options"
|
||||
@@ -13,10 +14,6 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ownerOnly is the mode a file the tool creates gets, since a
|
||||
// decrypted file is as secret as what went into it.
|
||||
const ownerOnly = 0o600
|
||||
|
||||
// Command returns the age command and everything under it.
|
||||
func Command() *cobra.Command {
|
||||
group := &cobra.Command{
|
||||
@@ -132,7 +129,7 @@ func runDecrypt(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
// through opens the input and the output the arguments ask for, hands
|
||||
// them to the work, and closes an output file afterwards either way.
|
||||
// them to the work, and finishes the output afterwards either way.
|
||||
func through(
|
||||
cmd *cobra.Command, args []string,
|
||||
work func(io.Writer, io.Reader) error,
|
||||
@@ -144,19 +141,14 @@ func through(
|
||||
|
||||
defer closeSrc()
|
||||
|
||||
dst, closeDst, err := output(cmd)
|
||||
dst, done, err := output(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = work(dst, src)
|
||||
if err != nil {
|
||||
_ = closeDst()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return closeDst()
|
||||
return done(err)
|
||||
}
|
||||
|
||||
// input returns what to read from: the named file, or the command's
|
||||
@@ -175,28 +167,61 @@ func input(cmd *cobra.Command, args []string) (io.Reader, func(), error) {
|
||||
return file, func() { _ = file.Close() }, nil
|
||||
}
|
||||
|
||||
// output returns what to write to: the file --output names, or the
|
||||
// command's own output. The second result closes a file that was
|
||||
// opened, so a failure to finish writing is not lost.
|
||||
func output(cmd *cobra.Command) (io.Writer, func() error, error) {
|
||||
// output returns what to write to: a new file beside the one --output
|
||||
// names, or the command's own output when it names none. The second
|
||||
// result finishes the write, and is given whatever the work returned:
|
||||
// the new file takes the named file's place only when the work
|
||||
// succeeded, so a file that is already there survives a run that
|
||||
// failed.
|
||||
func output(cmd *cobra.Command) (io.Writer, func(error) error, error) {
|
||||
name, err := cmd.Flags().GetString("output")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("reading the output file: %w", err)
|
||||
}
|
||||
|
||||
if name == "" {
|
||||
return cmd.OutOrStdout(), func() error { return nil }, nil
|
||||
return cmd.OutOrStdout(), func(failed error) error {
|
||||
return failed
|
||||
}, nil
|
||||
}
|
||||
|
||||
//nolint:gosec // writing the file the user named is the point
|
||||
file, err := os.OpenFile(
|
||||
name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, ownerOnly,
|
||||
)
|
||||
// The file is made in the same directory so that putting it in
|
||||
// place is a rename and never a copy, and it is readable only by
|
||||
// its owner, which is the mode it keeps once renamed.
|
||||
file, err := os.CreateTemp(filepath.Dir(name), filepath.Base(name)+".")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("creating %s: %w", name, err)
|
||||
return nil, nil, fmt.Errorf("creating a file beside %s: %w", name, err)
|
||||
}
|
||||
|
||||
return file, file.Close, nil
|
||||
return file, func(failed error) error {
|
||||
return finish(file, name, failed)
|
||||
}, nil
|
||||
}
|
||||
|
||||
// finish closes the new file and puts it in the named file's place, or
|
||||
// throws it away when the work failed. It returns the error the caller
|
||||
// should report.
|
||||
func finish(file *os.File, name string, failed error) error {
|
||||
closeErr := file.Close()
|
||||
|
||||
if failed != nil || closeErr != nil {
|
||||
_ = os.Remove(file.Name())
|
||||
|
||||
if failed != nil {
|
||||
return failed
|
||||
}
|
||||
|
||||
return fmt.Errorf("finishing %s: %w", name, closeErr)
|
||||
}
|
||||
|
||||
err := os.Rename(file.Name(), name)
|
||||
if err != nil {
|
||||
_ = os.Remove(file.Name())
|
||||
|
||||
return fmt.Errorf("putting %s in place: %w", name, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addOutput gives a command its output file flag.
|
||||
|
||||
@@ -72,6 +72,24 @@ func TestAFileForAnotherKeyIsRefused(t *testing.T) {
|
||||
require.ErrorIs(t, err, agekey.ErrNotRecipient)
|
||||
}
|
||||
|
||||
func TestARefusedDecryptionLeavesTheOutputFileAlone(t *testing.T) {
|
||||
t.Setenv(mnemonic.Variable, example())
|
||||
|
||||
plain := written(t, "notes.txt", "the secret\n")
|
||||
sealed := filepath.Join(t.TempDir(), "notes.age")
|
||||
existing := written(t, "notes.out", "what was already there\n")
|
||||
|
||||
run(t, "age", "encrypt", "-n", "7", "-o", sealed, plain)
|
||||
|
||||
_, err := execute(t, "age", "decrypt", "-o", existing, sealed)
|
||||
require.ErrorIs(t, err, agekey.ErrNotRecipient)
|
||||
|
||||
//nolint:gosec // the test made this path itself
|
||||
kept, err := os.ReadFile(existing)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "what was already there\n", string(kept))
|
||||
}
|
||||
|
||||
// written puts the contents in a file of that name in a directory of
|
||||
// this test's own and returns the path to it.
|
||||
func written(t *testing.T, name, contents string) string {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
|
||||
"git.eeqj.de/sneak/keyfunc/internal/cli/age"
|
||||
"git.eeqj.de/sneak/keyfunc/internal/cli/mnemonic"
|
||||
"git.eeqj.de/sneak/keyfunc/internal/cli/options"
|
||||
"git.eeqj.de/sneak/keyfunc/internal/cli/ssh"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -30,7 +31,7 @@ func Root() *cobra.Command {
|
||||
}
|
||||
|
||||
options.Add(root)
|
||||
root.AddCommand(ssh.Command(), age.Command())
|
||||
root.AddCommand(ssh.Command(), age.Command(), mnemonic.Command())
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
65
internal/cli/mnemonic/mnemonic.go
Normal file
65
internal/cli/mnemonic/mnemonic.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// 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,24 +35,47 @@ func Path(application, index uint32) string {
|
||||
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.
|
||||
// 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,
|
||||
)
|
||||
err := CheckIndex(index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
seed := bip39.NewSeed(words, "")
|
||||
|
||||
master, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams)
|
||||
master, err := Master(words)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("making the master key: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entropy, err := bip85.DeriveBIP85Entropy(master, Path(application, index))
|
||||
|
||||
Reference in New Issue
Block a user