// Package mnemonic finds the mnemonic the keys are derived from. package mnemonic import ( "bytes" "context" "errors" "fmt" "os" "os/exec" "strings" bip39 "github.com/tyler-smith/go-bip39" "golang.org/x/term" ) const ( // CommandVariable holds a shell command whose output is the // mnemonic. CommandVariable = "KEYFUNC_MNEMONIC_COMMAND" // Variable holds the mnemonic itself. Variable = "KEYFUNC_MNEMONIC" ) // ErrMissing is returned when there is nowhere left to look and // standard input is not a terminal, so there is nobody to ask. var ErrMissing = errors.New( "no mnemonic given and standard input is not a terminal", ) // ErrChecksum is returned for a mnemonic that fails the BIP-39 // checksum. var ErrChecksum = errors.New("the mnemonic fails its BIP-39 checksum") // Read returns the mnemonic. The command given on the command line is // used first; then the command in KEYFUNC_MNEMONIC_COMMAND; then the // mnemonic in KEYFUNC_MNEMONIC; then a prompt on the terminal with // echo turned off. The first source that has one wins. func Read(ctx context.Context, command string) (string, error) { if command == "" { command = os.Getenv(CommandVariable) } if command != "" { words, err := run(ctx, command) if err != nil { return "", err } return checked(words) } if words := os.Getenv(Variable); words != "" { return checked(words) } return ask() } // run executes the command with sh and returns its standard output. // If the command fails, its standard error becomes part of the error. func run(ctx context.Context, command string) (string, error) { //nolint:gosec // running the user's own command is the point shell := exec.CommandContext(ctx, "sh", "-c", command) var complaint bytes.Buffer shell.Stderr = &complaint out, err := shell.Output() if err != nil { said := strings.TrimSpace(complaint.String()) if said == "" { return "", fmt.Errorf("the mnemonic command failed: %w", err) } return "", fmt.Errorf( "the mnemonic command failed: %w: %s", err, said, ) } return string(out), nil } // ask prompts on the terminal with echo turned off. func ask() (string, error) { fd := int(os.Stdin.Fd()) if !term.IsTerminal(fd) { return "", ErrMissing } fmt.Fprint(os.Stderr, "mnemonic: ") typed, err := term.ReadPassword(fd) fmt.Fprintln(os.Stderr) if err != nil { return "", fmt.Errorf("reading the mnemonic: %w", err) } return checked(string(typed)) } // checked drops the surrounding whitespace and refuses a mnemonic that // does not pass the BIP-39 checksum. func checked(words string) (string, error) { words = strings.TrimSpace(words) if !bip39.IsMnemonicValid(words) { return "", ErrChecksum } return words, nil }