check / check (push) Failing after 2s
internal/cli drops its copies of vault.ErrSecretNotFound, ErrVaultNotFound, ErrVersionNotFound and ErrSecretExists and of the secret package's keychain and Secure Enclave errors, and its second error for an unknown unlocker type, an invalid mnemonic, a length below 1, an unsupported secret type and an oversized secret. vault.ErrNilValueBuffer becomes secret.ErrNilValueBuffer. Every error of secret.ReadPassphrase wraps ErrPassphraseNotRead. ResolveGPGKeyFingerprint returns ErrGPGKeyNotFound for a key the keyring lacks. storeInKeychain returns errNilDataBuffer. bip85's ErrPasswordTooShort and ErrEncodedTooShort go with their unreachable checks, as does the macOS check in macOS-only code. Tests that matched these errors' text use errors.Is. Model: opus-5-5
222 lines
6.0 KiB
Go
222 lines
6.0 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")
|
|
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 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
|
|
}
|