Files
keyfunc/internal/mnemonic/mnemonic.go
T
clawbot 45d302a6f0
check / check (push) Successful in 4m53s
Copy go-bip39 into internal/bip39 (closes #42)
go-bip39's repository no longer exists, so keyfunc now carries the
part of v1.1.0 it uses, with the upstream LICENSE beside it, and
imports it from internal/bip39. Upstream's tests and test vectors for
the kept code come along unchanged; where they called a removed
function, crypto/rand stands in for NewEntropy and EntropyFromMnemonic
for MnemonicToByteArray.

Changes beyond the trimming are what the linter asked for: the
package-level variables moved into the functions that use them, three
error strings were lower-cased, and the unknown-word error now wraps
ErrInvalidMnemonic.

go.mod no longer requires go-bip39. go mod tidy keeps its two go.sum
lines, because a test in sneak/secret's bip85 package imports it, and
drops five lines that only go-bip39's own requirements needed.

Model: opus-5-5
2026-10-06 00:32:11 +00:00

119 lines
2.8 KiB
Go

// Package mnemonic finds the mnemonic the keys are derived from.
package mnemonic
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"golang.org/x/term"
"sneak.berlin/go/keyfunc/internal/bip39"
)
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 joins the words with single spaces, whatever whitespace
// separated them, since the seed is computed over the string itself,
// and refuses a mnemonic that does not pass the BIP-39 checksum.
func checked(words string) (string, error) {
words = strings.Join(strings.Fields(words), " ")
if !bip39.IsMnemonicValid(words) {
return "", ErrChecksum
}
return words, nil
}