Add sutil CLI in cmd/sutil with internal/cli command tree
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"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
|
||||
// writes keep the per-syscall overhead negligible.
|
||||
const streamChunkSize = 1 << 20
|
||||
|
||||
// newGenerateCommand returns `sutil generate`, the parent of everything
|
||||
// that produces random output.
|
||||
func newGenerateCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "generate",
|
||||
Aliases: []string{"gen"},
|
||||
Short: "Generate random bytes, passwords, and mnemonics",
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
newGenerateRandomCommand(),
|
||||
newGeneratePasswordCommand(),
|
||||
newGenerateMnemonicCommand(),
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// newGenerateRandomCommand returns `sutil generate random [seedstr]`.
|
||||
func newGenerateRandomCommand() *cobra.Command {
|
||||
var byteCount int64
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "random [seedstr]",
|
||||
Short: "Write a fast stream of pseudorandom bytes to stdout",
|
||||
Long: `Writes pseudorandom bytes to stdout until stopped, or until --count
|
||||
bytes have been written.
|
||||
|
||||
With seedstr the stream is deterministic: the same seed always produces
|
||||
the same bytes. Without it the stream is keyed from the operating
|
||||
system's secure random source and is different every run.
|
||||
|
||||
This exists because /dev/urandom is slow.`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
seed := ""
|
||||
if len(args) == 1 {
|
||||
seed = args[0]
|
||||
}
|
||||
|
||||
stream, err := randstream.New(randstream.Params{Seed: seed})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return streamBytes(cmd.OutOrStdout(), stream, byteCount)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().Int64VarP(
|
||||
&byteCount, "count", "c", 0, "stop after this many bytes (0 = never)",
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// newGeneratePasswordCommand returns `sutil generate password`.
|
||||
func newGeneratePasswordCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "password",
|
||||
Aliases: []string{"pw"},
|
||||
Short: "Print a memorable password built from system dictionary words",
|
||||
Long: `Prints a password of the form
|
||||
|
||||
<word><digit><delim><word><digit><delim><word><digit>
|
||||
|
||||
for example marble7-sprout2-velvet9. The words come from the system word
|
||||
list (/usr/share/dict/words), the delimiter is chosen at random from
|
||||
|
||||
` + pwgen.Delimiters + `
|
||||
|
||||
and is the same in both positions. The command panics if the system has
|
||||
no usable word list.`,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
generator, err := pwgen.New(pwgen.Params{})
|
||||
if err != nil {
|
||||
panic("sutil: cannot load the system word list: " + err.Error())
|
||||
}
|
||||
|
||||
password, err := generator.Generate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintln(cmd.OutOrStdout(), password)
|
||||
|
||||
return err
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if limit > 0 {
|
||||
src = io.LimitReader(src, limit)
|
||||
}
|
||||
|
||||
// Wrapping dst hides any ReadFrom method it has, so io.CopyBuffer
|
||||
// cannot bypass the large buffer.
|
||||
_, err := io.CopyBuffer(writerOnly{dst}, src, make([]byte, streamChunkSize))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// writerOnly exposes only the Write method of the wrapped writer.
|
||||
type writerOnly struct {
|
||||
io.Writer
|
||||
}
|
||||
Reference in New Issue
Block a user