Remove cbk and mnemonic commands from sutil
sutil holds only the utilities wanted on every machine; it is not a command-line front end for the packages in pkg/. cbk and bip39wordlist remain libraries with no CLI surface. The README states the boundary.
This commit is contained in:
@@ -1,82 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"sneak.berlin/go/x/pkg/cbk"
|
||||
)
|
||||
|
||||
// maxMnemonicInputBytes bounds how much of stdin `cbk derive` will read; a
|
||||
// 24-word mnemonic is well under 300 bytes.
|
||||
const maxMnemonicInputBytes = 64 << 10
|
||||
|
||||
// newCBKCommand returns `sutil cbk`, the parent of the crypto_box
|
||||
// keypair commands.
|
||||
func newCBKCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "cbk",
|
||||
Short: "crypto_box keypairs derived from BIP39 mnemonics",
|
||||
}
|
||||
|
||||
cmd.AddCommand(newCBKDeriveCommand())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// newCBKDeriveCommand returns `sutil cbk derive`.
|
||||
func newCBKDeriveCommand() *cobra.Command {
|
||||
var publicOnly bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "derive",
|
||||
Short: "Derive a crypto_box keypair from a mnemonic read on stdin",
|
||||
Long: `Reads a BIP39 mnemonic from stdin, in any letter case and with any
|
||||
whitespace between words, and prints the crypto_box keypair derived from it
|
||||
as hex. The same mnemonic always yields the same keypair.
|
||||
|
||||
The mnemonic is read from stdin rather than taken as an argument so that it
|
||||
does not end up in shell history or process listings:
|
||||
|
||||
sutil generate mnemonic > mnemonic.txt
|
||||
sutil cbk derive < mnemonic.txt`,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
input := io.LimitReader(cmd.InOrStdin(), maxMnemonicInputBytes)
|
||||
|
||||
mnemonic, err := io.ReadAll(input)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading mnemonic from stdin: %w", err)
|
||||
}
|
||||
|
||||
keypair, err := cbk.New(cbk.Params{Mnemonic: string(mnemonic)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return writeKeypair(cmd.OutOrStdout(), keypair, publicOnly)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&publicOnly, "public", false, "print only the public key")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// writeKeypair prints the keypair as labeled hex lines, or just the bare
|
||||
// public key when publicOnly is set.
|
||||
func writeKeypair(w io.Writer, keypair cbk.CryptoBoxKeypair, publicOnly bool) error {
|
||||
if publicOnly {
|
||||
_, err := fmt.Fprintln(w, keypair.PublicKeyHex())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := fmt.Fprintf(
|
||||
w, "public: %s\nprivate: %s\n", keypair.PublicKeyHex(), keypair.PrivateKeyHex(),
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
+2
-4
@@ -37,9 +37,8 @@ func New(p Params) *cobra.Command {
|
||||
root := &cobra.Command{
|
||||
Use: "sutil",
|
||||
Short: "sneak's shell utility",
|
||||
Long: `sutil is a small collection of shell utilities built on the packages in
|
||||
sneak.berlin/go/x: fast pseudorandom byte streams, memorable passwords,
|
||||
BIP39 mnemonics, and crypto_box keypairs derived from them.
|
||||
Long: `sutil is a small collection of shell utilities worth having on every
|
||||
machine: a fast pseudorandom byte stream and a memorable password generator.
|
||||
|
||||
Before any command runs, sutil loads the optional configuration file named
|
||||
by --config, or else ~/.sneak.yaml (or .json, .toml, ...) if it exists, and
|
||||
@@ -58,7 +57,6 @@ makes environment variables available alongside it.`,
|
||||
newVersionCommand(p),
|
||||
newInfoCommand(a),
|
||||
newGenerateCommand(),
|
||||
newCBKCommand(),
|
||||
)
|
||||
|
||||
return root
|
||||
|
||||
+24
-79
@@ -13,29 +13,14 @@ import (
|
||||
|
||||
"sneak.berlin/go/x/internal/cli"
|
||||
"sneak.berlin/go/x/internal/pwgen"
|
||||
"sneak.berlin/go/x/pkg/bip39wordlist"
|
||||
"sneak.berlin/go/x/pkg/cbk"
|
||||
)
|
||||
|
||||
const (
|
||||
testVersion = "v1.2.3-test"
|
||||
|
||||
// vectorRepeats is how many times "abandon" appears in the standard
|
||||
// BIP39 test vector for all-zero 128-bit entropy, which ends with
|
||||
// "about".
|
||||
vectorRepeats = bip39wordlist.MinWordCount - 1
|
||||
)
|
||||
|
||||
// vectorMnemonic returns the standard BIP39 test vector for all-zero
|
||||
// 128-bit entropy.
|
||||
func vectorMnemonic() string {
|
||||
return strings.Repeat("abandon ", vectorRepeats) + "about"
|
||||
}
|
||||
const testVersion = "v1.2.3-test"
|
||||
|
||||
func TestVersionCommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
output, err := execute(t, "", "version")
|
||||
output, err := execute(t, "version")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, testVersion+"\n", output)
|
||||
}
|
||||
@@ -43,7 +28,7 @@ func TestVersionCommand(t *testing.T) {
|
||||
func TestVersionFlag(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
output, err := execute(t, "", "--version")
|
||||
output, err := execute(t, "--version")
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, output, testVersion)
|
||||
}
|
||||
@@ -51,7 +36,7 @@ func TestVersionFlag(t *testing.T) {
|
||||
func TestInfoCommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
output, err := execute(t, "", "info")
|
||||
output, err := execute(t, "info")
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, output, "version: "+testVersion+"\n")
|
||||
assert.Contains(t, output, "go: go")
|
||||
@@ -68,7 +53,7 @@ func TestConfigFileDiscoveredInConfigDir(t *testing.T) {
|
||||
|
||||
params := cli.Params{Version: testVersion, ConfigDir: configDir}
|
||||
|
||||
output, err := executeWithParams(t, params, "", "info")
|
||||
output, err := executeWithParams(t, params, "info")
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, output, "Using config file: "+path+"\n")
|
||||
assert.Contains(t, output, "config: "+path+"\n")
|
||||
@@ -80,7 +65,7 @@ func TestConfigFlagLoadsNamedFile(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "elsewhere.yaml")
|
||||
require.NoError(t, os.WriteFile(path, []byte("example: 1\n"), 0o600))
|
||||
|
||||
output, err := execute(t, "", "--config", path, "info")
|
||||
output, err := execute(t, "--config", path, "info")
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, output, "Using config file: "+path+"\n")
|
||||
assert.Contains(t, output, "config: "+path+"\n")
|
||||
@@ -91,7 +76,7 @@ func TestConfigFlagRejectsMissingFile(t *testing.T) {
|
||||
|
||||
path := filepath.Join(t.TempDir(), "missing.yaml")
|
||||
|
||||
_, err := execute(t, "", "--config", path, "version")
|
||||
_, err := execute(t, "--config", path, "version")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -104,22 +89,22 @@ func TestConfigRejectsMalformedFile(t *testing.T) {
|
||||
|
||||
params := cli.Params{Version: testVersion, ConfigDir: configDir}
|
||||
|
||||
_, err := executeWithParams(t, params, "", "version")
|
||||
_, err := executeWithParams(t, params, "version")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestGenerateRandomSeededIsDeterministic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
first, err := execute(t, "", "generate", "random", "--count", "4096", "seed")
|
||||
first, err := execute(t, "generate", "random", "--count", "4096", "seed")
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, first, 4096)
|
||||
|
||||
second, err := execute(t, "", "generate", "random", "--count", "4096", "seed")
|
||||
second, err := execute(t, "generate", "random", "--count", "4096", "seed")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, first, second)
|
||||
|
||||
other, err := execute(t, "", "gen", "random", "-c", "4096", "other seed")
|
||||
other, err := execute(t, "gen", "random", "-c", "4096", "other seed")
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, first, other)
|
||||
}
|
||||
@@ -127,11 +112,11 @@ func TestGenerateRandomSeededIsDeterministic(t *testing.T) {
|
||||
func TestGenerateRandomUnseeded(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
first, err := execute(t, "", "generate", "random", "--count", "1024")
|
||||
first, err := execute(t, "generate", "random", "--count", "1024")
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, first, 1024)
|
||||
|
||||
second, err := execute(t, "", "generate", "random", "--count", "1024")
|
||||
second, err := execute(t, "generate", "random", "--count", "1024")
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, first, second)
|
||||
}
|
||||
@@ -139,28 +124,10 @@ func TestGenerateRandomUnseeded(t *testing.T) {
|
||||
func TestGenerateRandomRejectsExtraArguments(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := execute(t, "", "generate", "random", "one", "two")
|
||||
_, err := execute(t, "generate", "random", "one", "two")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestGenerateMnemonic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
output, err := execute(t, "", "generate", "mnemonic")
|
||||
require.NoError(t, err)
|
||||
|
||||
mnemonic := strings.TrimSuffix(output, "\n")
|
||||
require.NoError(t, bip39wordlist.ValidateWordlist(mnemonic))
|
||||
assert.Len(t, strings.Fields(mnemonic), bip39wordlist.MaxWordCount)
|
||||
|
||||
output, err = execute(t, "", "generate", "mnemonic", "--words", "12")
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, strings.Fields(output), bip39wordlist.MinWordCount)
|
||||
|
||||
_, err = execute(t, "", "generate", "mnemonic", "--words", "13")
|
||||
require.ErrorIs(t, err, bip39wordlist.ErrInvalidWordCount)
|
||||
}
|
||||
|
||||
func TestGeneratePassword(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -171,7 +138,7 @@ func TestGeneratePassword(t *testing.T) {
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
output, err := execute(t, "", "generate", "password")
|
||||
output, err := execute(t, "generate", "password")
|
||||
require.NoError(t, err)
|
||||
|
||||
password := strings.TrimSuffix(output, "\n")
|
||||
@@ -181,48 +148,27 @@ func TestGeneratePassword(t *testing.T) {
|
||||
assert.True(t, strings.ContainsAny(password, pwgen.Delimiters), password)
|
||||
}
|
||||
|
||||
func TestCBKDerive(t *testing.T) {
|
||||
func TestUnknownCommandFails(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
expected, err := cbk.FromMnemonic(vectorMnemonic())
|
||||
require.NoError(t, err)
|
||||
|
||||
stdin := " " + strings.ToUpper(vectorMnemonic()) + "\n"
|
||||
|
||||
output, err := execute(t, stdin, "cbk", "derive")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t,
|
||||
"public: "+expected.PublicKeyHex()+"\n"+
|
||||
"private: "+expected.PrivateKeyHex()+"\n",
|
||||
output,
|
||||
)
|
||||
|
||||
output, err = execute(t, vectorMnemonic(), "cbk", "derive", "--public")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expected.PublicKeyHex()+"\n", output)
|
||||
_, err := execute(t, "cbk")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestCBKDeriveRejectsInvalidMnemonic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := execute(t, "not a mnemonic\n", "cbk", "derive")
|
||||
require.ErrorIs(t, err, bip39wordlist.ErrInvalidWordCount)
|
||||
}
|
||||
|
||||
// execute runs sutil with the given stdin and arguments, looking for config
|
||||
// in an empty temporary directory, and returns everything it wrote to
|
||||
// stdout and stderr.
|
||||
func execute(t *testing.T, stdin string, args ...string) (string, error) {
|
||||
// execute runs sutil with the given arguments, looking for config in an
|
||||
// empty temporary directory, and returns everything it wrote to stdout and
|
||||
// stderr.
|
||||
func execute(t *testing.T, args ...string) (string, error) {
|
||||
t.Helper()
|
||||
|
||||
params := cli.Params{Version: testVersion, ConfigDir: t.TempDir()}
|
||||
|
||||
return executeWithParams(t, params, stdin, args...)
|
||||
return executeWithParams(t, params, args...)
|
||||
}
|
||||
|
||||
// executeWithParams is execute with explicit Params.
|
||||
func executeWithParams(
|
||||
t *testing.T, params cli.Params, stdin string, args ...string,
|
||||
t *testing.T, params cli.Params, args ...string,
|
||||
) (string, error) {
|
||||
t.Helper()
|
||||
|
||||
@@ -232,7 +178,6 @@ func executeWithParams(
|
||||
|
||||
root.SetOut(&output)
|
||||
root.SetErr(&output)
|
||||
root.SetIn(strings.NewReader(stdin))
|
||||
root.SetArgs(args)
|
||||
|
||||
err := root.Execute()
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
"sneak.berlin/go/x/internal/pwgen"
|
||||
"sneak.berlin/go/x/internal/randstream"
|
||||
"sneak.berlin/go/x/pkg/bip39wordlist"
|
||||
)
|
||||
|
||||
// streamChunkSize is the write size used by `generate random`. Large
|
||||
@@ -21,13 +20,12 @@ func newGenerateCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "generate",
|
||||
Aliases: []string{"gen"},
|
||||
Short: "Generate random bytes, passwords, and mnemonics",
|
||||
Short: "Generate random bytes and passwords",
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
newGenerateRandomCommand(),
|
||||
newGeneratePasswordCommand(),
|
||||
newGenerateMnemonicCommand(),
|
||||
)
|
||||
|
||||
return cmd
|
||||
@@ -107,36 +105,6 @@ no usable word list.`,
|
||||
}
|
||||
}
|
||||
|
||||
// newGenerateMnemonicCommand returns `sutil generate mnemonic`.
|
||||
func newGenerateMnemonicCommand() *cobra.Command {
|
||||
var wordCount int
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "mnemonic",
|
||||
Short: "Print a new random BIP39 mnemonic phrase",
|
||||
Long: `Prints a new BIP39 mnemonic phrase generated from crypto/rand. The
|
||||
default of 24 words encodes 256 bits of entropy; 12 words encodes 128.`,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
mnemonic, err := bip39wordlist.GenerateMnemonic(wordCount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintln(cmd.OutOrStdout(), mnemonic)
|
||||
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().IntVarP(
|
||||
&wordCount, "words", "w", bip39wordlist.MaxWordCount,
|
||||
"number of words: 12, 15, 18, 21, or 24",
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// streamBytes copies limit bytes (or everything, when limit is zero) from
|
||||
// src to dst in streamChunkSize chunks.
|
||||
func streamBytes(dst io.Writer, src io.Reader, limit int64) error {
|
||||
|
||||
Reference in New Issue
Block a user