check / check (push) Waiting to run
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
228 lines
6.1 KiB
Go
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
|
|
}
|