Files
secret/internal/cli/generate.go
T
clawbot 63928ea745
check / check (push) Waiting to run
Read secret environment variables once per command, then unset them (closes #60)
init and vault create put the mnemonic into the process environment for
vault.CreateVault to read back, so every program they ran, gpg included,
inherited it, and SB_SECRET_MNEMONIC and SB_UNLOCK_PASSPHRASE were read
at 13 places and never unset. Each command that may need them now reads
both once, in its RunE, into locked buffers on the CLI Instance, and
unsets them at once. The buffers are passed down: vault.CreateVault
takes the mnemonic, a Vault carries Mnemonic and UnlockPassphrase, and
the PGP, keychain and Secure Enclave unlocker constructors take both.
Nothing below the command reads the environment. README warns against
both variables.

Model: opus-5-5
2026-10-04 12:28:18 +00:00

225 lines
6.2 KiB
Go

package cli
import (
"crypto/rand"
"errors"
"fmt"
"math/big"
"os"
"git.eeqj.de/sneak/secret/internal/vault"
"github.com/awnumar/memguard"
"github.com/spf13/cobra"
"github.com/tyler-smith/go-bip39"
)
const (
defaultSecretLength = 16
mnemonicEntropyBits = 128
)
// Sentinel errors for secret generation
var (
errLengthTooSmall = errors.New("length must be at least 1")
errLengthNotPositive = errors.New("length must be positive")
errMnemonicTypeNotSupported = errors.New(
"mnemonic type not supported for secret generation, " +
"use 'secret generate mnemonic' instead")
errUnsupportedSecretType = errors.New("unsupported type")
)
func newGenerateCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "generate",
Short: "Generate random data",
Long: `Generate various types of random data including mnemonics and secrets.`,
}
cmd.AddCommand(newGenerateMnemonicCmd())
cmd.AddCommand(newGenerateSecretCmd())
return cmd
}
func newGenerateMnemonicCmd() *cobra.Command {
return &cobra.Command{
Use: "mnemonic",
Short: "Generate a random BIP39 mnemonic phrase",
Long: `Generate a cryptographically secure random BIP39 ` +
`mnemonic phrase that can be used with 'secret init' ` +
`or 'secret import'.`,
RunE: func(cmd *cobra.Command, _ []string) error {
cli, err := NewCLIInstance()
if err != nil {
return fmt.Errorf("failed to initialize CLI: %w", err)
}
return cli.GenerateMnemonic(cmd)
},
}
}
func newGenerateSecretCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "secret <name>",
Short: "Generate a random secret and store it in the vault",
Long: `Generate a cryptographically secure random secret and ` +
`store it in the current vault under the given name.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
length, _ := cmd.Flags().GetInt("length")
secretType, _ := cmd.Flags().GetString("type")
force, _ := cmd.Flags().GetBool("force")
cli, err := NewCLIInstance()
if err != nil {
return fmt.Errorf("failed to initialize CLI: %w", err)
}
destroySecrets := cli.readSecretEnv()
defer destroySecrets()
return cli.GenerateSecret(cmd, args[0], length, secretType, force)
},
}
cmd.Flags().IntP("length", "l", defaultSecretLength,
"Length of the generated secret (default 16)")
cmd.Flags().StringP("type", "t", "base58",
"Type of secret to generate (base58, alnum)")
cmd.Flags().BoolP("force", "f", false, "Overwrite existing secret")
return cmd
}
// GenerateMnemonic generates a random BIP39 mnemonic phrase
func (cli *Instance) GenerateMnemonic(cmd *cobra.Command) error {
// Generate 128 bits of entropy for a 12-word mnemonic
entropy, err := bip39.NewEntropy(mnemonicEntropyBits)
if err != nil {
return fmt.Errorf("failed to generate entropy: %w", err)
}
// Create mnemonic from entropy
mnemonic, err := bip39.NewMnemonic(entropy)
if err != nil {
return fmt.Errorf("failed to generate mnemonic: %w", err)
}
// Output mnemonic to stdout
cmd.Println(mnemonic)
// Output helpful information to stderr
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "⚠️ IMPORTANT: Save this mnemonic phrase securely!")
fmt.Fprintln(os.Stderr, " • Write it down on paper and store it safely")
fmt.Fprintln(os.Stderr, " • Do not store it digitally or share it with anyone")
fmt.Fprintln(os.Stderr, " • You will need this phrase to recover your secrets")
fmt.Fprintln(os.Stderr,
" • If you lose this phrase, your secrets cannot be recovered")
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "Use this mnemonic with:")
fmt.Fprintln(os.Stderr, " secret init (to initialize a new secret manager)")
fmt.Fprintln(os.Stderr, " secret import (to import into an existing vault)")
return nil
}
// GenerateSecret generates a random secret and stores it in the vault
func (cli *Instance) GenerateSecret(
cmd *cobra.Command,
secretName string,
length int,
secretType string,
force bool,
) error {
if length < 1 {
return errLengthTooSmall
}
var (
secretValue string
err error
)
switch secretType {
case "base58":
secretValue, err = generateRandomBase58(length)
case "alnum":
secretValue, err = generateRandomAlnum(length)
case "mnemonic":
return errMnemonicTypeNotSupported
default:
return fmt.Errorf("%w: %s (supported: base58, alnum)",
errUnsupportedSecretType, secretType)
}
if err != nil {
return fmt.Errorf("failed to generate random secret: %w", err)
}
release, err := vault.LockStateDir(cli.fs, cli.stateDir)
if err != nil {
return err
}
defer release()
// Store the secret in the vault
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
if err != nil {
return err
}
vlt.Mnemonic, vlt.UnlockPassphrase = cli.Mnemonic, cli.UnlockPassphrase
// Protect the generated secret immediately
secretBuffer := memguard.NewBufferFromBytes([]byte(secretValue))
defer secretBuffer.Destroy()
err = vlt.AddSecret(secretName, secretBuffer, force)
if err != nil {
return err
}
cmd.Printf("Generated and stored %d-character %s secret: %s\n",
length, secretType, secretName)
return nil
}
// generateRandomBase58 generates a random base58 string of the specified length
func generateRandomBase58(length int) (string, error) {
const base58Chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
return generateRandomString(length, base58Chars)
}
// generateRandomAlnum generates a random alphanumeric string of the specified length
func generateRandomAlnum(length int) (string, error) {
const alnumChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
return generateRandomString(length, alnumChars)
}
// generateRandomString generates a random string of the specified length
// using the given character set
func generateRandomString(length int, charset string) (string, error) {
if length <= 0 {
return "", errLengthNotPositive
}
result := make([]byte, length)
charsetLen := big.NewInt(int64(len(charset)))
for i := range length {
randomIndex, err := rand.Int(rand.Reader, charsetLen)
if err != nil {
return "", fmt.Errorf("failed to generate random number: %w", err)
}
result[i] = charset[randomIndex.Int64()]
}
return string(result), nil
}