Files
secret/internal/cli/generate.go
T
clawbot f081505f78
check / check (push) Waiting to run
A secret too large for the locked-memory limit fails with an error, not a panic (closes #145)
secret add, import and generate secret crashed with memguard's panic once a
value outgrew the locked-memory limit, and because the value was held twice,
half the usual 8 MiB limit was enough. secret now raises the limit to the hard
limit at startup. The value is read a page at a time into a memguard.Stream,
which keeps it encrypted, then copied once into locked memory after its size is
checked against 100 MiB and against the limit less 16 pages for the command's
keys. It comes out of the stream through Next, not Read: Read re-encrypts a
leftover piece, and memguard hangs if that runs out of locked memory.

Judgement call: with a limit of 64 KiB or less, nothing can be stored.

Model: opus-5-5
2026-10-14 20:04:22 +00:00

228 lines
6.1 KiB
Go

package cli
import (
"crypto/rand"
"errors"
"fmt"
"math/big"
"os"
"github.com/awnumar/memguard"
"github.com/spf13/cobra"
"sneak.berlin/go/secret/internal/bip39"
"sneak.berlin/go/secret/internal/vault"
)
const (
defaultSecretLength = 16
mnemonicEntropyBits = 128
)
// Sentinel errors for secret generation
var (
errLengthTooSmall = errors.New("length must be at least 1")
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
}
if length > maxSecretSize {
return errSecretTooLarge
}
err := checkLockedMemoryLimit(length)
if err != nil {
return err
}
var secretValue string
switch secretType {
case "base58":
secretValue, err = generateRandomBase58(length)
case "alnum":
secretValue, err = generateRandomAlnum(length)
case "mnemonic":
return fmt.Errorf("%w: mnemonic (use 'secret generate mnemonic' instead)",
errUnsupportedSecretType)
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 < 1 {
return "", errLengthTooSmall
}
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
}