Change NewCLIInstance() and NewCLIInstanceWithFs() to return (*Instance, error) instead of panicking on DetermineStateDir failure. Callers in RunE contexts propagate the error. Callers in command construction (for shell completion) use log.Fatalf. Test callers use t.Fatalf. Addresses review feedback on PR #18.
271 lines
7.4 KiB
Go
271 lines
7.4 KiB
Go
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
|
|
"filippo.io/age"
|
|
"git.eeqj.de/sneak/secret/internal/secret"
|
|
"git.eeqj.de/sneak/secret/internal/vault"
|
|
"github.com/awnumar/memguard"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func newEncryptCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "encrypt <secret-name>",
|
|
Short: "Encrypt data using an age secret key stored in a secret",
|
|
Long: `Encrypt data using an age secret key. If the secret doesn't exist, a new age key is generated and stored.`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
inputFile, _ := cmd.Flags().GetString("input")
|
|
outputFile, _ := cmd.Flags().GetString("output")
|
|
|
|
cli, err := NewCLIInstance()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to initialize CLI: %w", err)
|
|
}
|
|
cli.cmd = cmd
|
|
|
|
return cli.Encrypt(args[0], inputFile, outputFile)
|
|
},
|
|
}
|
|
|
|
cmd.Flags().StringP("input", "i", "", "Input file (default: stdin)")
|
|
cmd.Flags().StringP("output", "o", "", "Output file (default: stdout)")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func newDecryptCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "decrypt <secret-name>",
|
|
Short: "Decrypt data using an age secret key stored in a secret",
|
|
Long: `Decrypt data using an age secret key stored in the specified secret.`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
inputFile, _ := cmd.Flags().GetString("input")
|
|
outputFile, _ := cmd.Flags().GetString("output")
|
|
|
|
cli, err := NewCLIInstance()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to initialize CLI: %w", err)
|
|
}
|
|
cli.cmd = cmd
|
|
|
|
return cli.Decrypt(args[0], inputFile, outputFile)
|
|
},
|
|
}
|
|
|
|
cmd.Flags().StringP("input", "i", "", "Input file (default: stdin)")
|
|
cmd.Flags().StringP("output", "o", "", "Output file (default: stdout)")
|
|
|
|
return cmd
|
|
}
|
|
|
|
// Encrypt encrypts data using an age secret key stored in a secret
|
|
func (cli *Instance) Encrypt(secretName, inputFile, outputFile string) error {
|
|
// Get current vault
|
|
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var ageSecretKey string
|
|
|
|
// Check if secret exists
|
|
secretObj := secret.NewSecret(vlt, secretName)
|
|
exists, err := secretObj.Exists()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to check if secret exists: %w", err)
|
|
}
|
|
|
|
if !exists { //nolint:nestif // Clear conditional logic for secret generation vs retrieval
|
|
// Secret doesn't exist, generate new age key and store it
|
|
identity, err := age.GenerateX25519Identity()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate age key: %w", err)
|
|
}
|
|
|
|
// Store the generated key directly in a secure buffer
|
|
identityStr := identity.String()
|
|
secureBuffer := memguard.NewBufferFromBytes([]byte(identityStr))
|
|
defer secureBuffer.Destroy()
|
|
|
|
// Set ageSecretKey for later use (we need it for encryption)
|
|
ageSecretKey = identityStr
|
|
|
|
err = vlt.AddSecret(secretName, secureBuffer, false)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to store age key: %w", err)
|
|
}
|
|
} else {
|
|
// Secret exists, get the age secret key from it
|
|
secretBuffer, err := cli.getSecretValue(vlt, secretObj)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get secret value: %w", err)
|
|
}
|
|
defer secretBuffer.Destroy()
|
|
|
|
ageSecretKey = secretBuffer.String()
|
|
|
|
// Validate that it's a valid age secret key
|
|
if !isValidAgeSecretKey(ageSecretKey) {
|
|
return fmt.Errorf("secret '%s' does not contain a valid age secret key", secretName)
|
|
}
|
|
}
|
|
|
|
// Parse the secret key using secure buffer
|
|
finalSecureBuffer := memguard.NewBufferFromBytes([]byte(ageSecretKey))
|
|
defer finalSecureBuffer.Destroy()
|
|
|
|
identity, err := age.ParseX25519Identity(finalSecureBuffer.String())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse age secret key: %w", err)
|
|
}
|
|
|
|
// Get recipient from identity
|
|
recipient := identity.Recipient()
|
|
|
|
// Set up input reader
|
|
var input io.Reader = os.Stdin
|
|
if inputFile != "" {
|
|
file, err := cli.fs.Open(inputFile)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open input file: %w", err)
|
|
}
|
|
defer func() { _ = file.Close() }()
|
|
input = file
|
|
}
|
|
|
|
// Set up output writer
|
|
output := cli.cmd.OutOrStdout()
|
|
if outputFile != "" {
|
|
file, err := cli.fs.Create(outputFile)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create output file: %w", err)
|
|
}
|
|
defer func() { _ = file.Close() }()
|
|
output = file
|
|
}
|
|
|
|
// Encrypt the data
|
|
encryptor, err := age.Encrypt(output, recipient)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create age encryptor: %w", err)
|
|
}
|
|
|
|
if _, err := io.Copy(encryptor, input); err != nil {
|
|
return fmt.Errorf("failed to encrypt data: %w", err)
|
|
}
|
|
|
|
if err := encryptor.Close(); err != nil {
|
|
return fmt.Errorf("failed to finalize encryption: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Decrypt decrypts data using an age secret key stored in a secret
|
|
func (cli *Instance) Decrypt(secretName, inputFile, outputFile string) error {
|
|
// Get current vault
|
|
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check if secret exists
|
|
secretObj := secret.NewSecret(vlt, secretName)
|
|
exists, err := secretObj.Exists()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to check if secret exists: %w", err)
|
|
}
|
|
|
|
if !exists {
|
|
return fmt.Errorf("secret '%s' does not exist", secretName)
|
|
}
|
|
|
|
// Get the age secret key from the secret
|
|
var secretBuffer *memguard.LockedBuffer
|
|
if os.Getenv(secret.EnvMnemonic) != "" {
|
|
secretBuffer, err = secretObj.GetValue(nil)
|
|
} else {
|
|
unlocker, unlockErr := vlt.GetCurrentUnlocker()
|
|
if unlockErr != nil {
|
|
return fmt.Errorf("failed to get current unlocker: %w", unlockErr)
|
|
}
|
|
secretBuffer, err = secretObj.GetValue(unlocker)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get secret value: %w", err)
|
|
}
|
|
defer secretBuffer.Destroy()
|
|
|
|
// Validate that it's a valid age secret key
|
|
if !isValidAgeSecretKey(secretBuffer.String()) {
|
|
return fmt.Errorf("secret '%s' does not contain a valid age secret key", secretName)
|
|
}
|
|
|
|
// Parse the age secret key to get the identity
|
|
identity, err := age.ParseX25519Identity(secretBuffer.String())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse age secret key: %w", err)
|
|
}
|
|
|
|
// Set up input reader
|
|
var input io.Reader = os.Stdin
|
|
if inputFile != "" {
|
|
file, err := cli.fs.Open(inputFile)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open input file: %w", err)
|
|
}
|
|
defer func() { _ = file.Close() }()
|
|
input = file
|
|
}
|
|
|
|
// Set up output writer
|
|
output := cli.cmd.OutOrStdout()
|
|
if outputFile != "" {
|
|
file, err := cli.fs.Create(outputFile)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create output file: %w", err)
|
|
}
|
|
defer func() { _ = file.Close() }()
|
|
output = file
|
|
}
|
|
|
|
// Decrypt the data
|
|
decryptor, err := age.Decrypt(input, identity)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create age decryptor: %w", err)
|
|
}
|
|
|
|
if _, err := io.Copy(output, decryptor); err != nil {
|
|
return fmt.Errorf("failed to decrypt data: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// isValidAgeSecretKey checks if a string is a valid age secret key by attempting to parse it
|
|
func isValidAgeSecretKey(key string) bool {
|
|
_, err := age.ParseX25519Identity(key)
|
|
|
|
return err == nil
|
|
}
|
|
|
|
// getSecretValue retrieves the value of a secret using the appropriate unlocker
|
|
func (cli *Instance) getSecretValue(vlt *vault.Vault, secretObj *secret.Secret) (*memguard.LockedBuffer, error) {
|
|
if os.Getenv(secret.EnvMnemonic) != "" {
|
|
return secretObj.GetValue(nil)
|
|
}
|
|
|
|
unlocker, err := vlt.GetCurrentUnlocker()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get current unlocker: %w", err)
|
|
}
|
|
|
|
return secretObj.GetValue(unlocker)
|
|
}
|