All checks were successful
check / check (push) Successful in 2m0s
- 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 User-visible strings -------------------- No user-visible string changes remain. Every error message this branch composes is byte-identical to the one main composes. The err113 sentinels are shaped so that fmt.Errorf reassembles the original text around them: a sentinel carries the fixed words of the message and the caller supplies the interpolated value in the position it has always occupied. Where the value sits in the middle of the sentence the sentinel therefore holds only a fragment (for example vault.ErrVaultNotFound is "does not exist", composed by its caller as "vault <name> does not exist"); each such sentinel documents the message it participates in. Verified mechanically rather than by inspection: every fmt.Errorf and errors.New call site in both trees was parsed, the Error() text of any sentinel passed to %w substituted in, and the resulting sets of composed message templates compared. All 350 templates main produces are still produced, character for character; the set of messages lost or altered is empty. unlocker list ------------- findUnlockerIDByMetadata now returns (string, error) instead of signalling failure with an empty ID. An unreadable unlockers.d is no longer indistinguishable from "no matching entry", so UnlockersList skips the entry with a warning naming the directory, as it did before the scan was extracted into a helper, rather than emitting a row under a synthesized fallback ID that no unlocker remove or unlocker select can match and that suppresses the current-unlocker marker. The duplicate-check and shell-completion callers skip on the same condition, matching their pre-extraction behavior. Covered by tests in internal/cli/unlockers_list_test.go.
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
|
|
}
|