All checks were successful
check / check (push) Successful in 1m8s
- Replace .golangci.yml with the canonical strict config (all linters enabled except the standard disable list; lll 88, funlen 80/50, cyclop 15, dupl 100; test files now linted) - Pin the Dockerfile lint stage to golangci/golangci-lint:v2.12.2 by tag and digest (Debian-based) - Fix all ~1550 findings surfaced by the new config: line wrapping, wsl_v5/nlreturn blank lines, noinlineerr splits, err113 sentinel errors, perfsprint/modernize rewrites, goconst constants, thelper, testifylint, noctx CommandContext, testpackage conversions, t.Parallel() where safe, and complexity/dupl helper extraction - Record the change and follow-up items in TODO.md
214 lines
5.9 KiB
Go
214 lines
5.9 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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// Store the secret in the vault
|
|
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 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
|
|
}
|