secret/internal/cli/crypto.go
2025-05-29 11:02:22 -07:00

244 lines
6.6 KiB
Go

package cli
import (
"fmt"
"io"
"os"
"filippo.io/age"
"git.eeqj.de/sneak/secret/internal/secret"
"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 := NewCLIInstance()
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 := NewCLIInstance()
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 *CLIInstance) Encrypt(secretName, inputFile, outputFile string) error {
// Get current vault
vault, err := secret.GetCurrentVault(cli.fs, cli.stateDir)
if err != nil {
return err
}
var ageSecretKey string
// Check if secret exists
secretObj := secret.NewSecret(vault, secretName)
exists, err := secretObj.Exists()
if err != nil {
return fmt.Errorf("failed to check if secret exists: %w", err)
}
if exists {
// Secret exists, get the age secret key from it
var secretValue []byte
if os.Getenv(secret.EnvMnemonic) != "" {
secretValue, err = secretObj.GetValue(nil)
} else {
unlockKey, unlockErr := vault.GetCurrentUnlockKey()
if unlockErr != nil {
return fmt.Errorf("failed to get current unlock key: %w", unlockErr)
}
secretValue, err = secretObj.GetValue(unlockKey)
}
if err != nil {
return fmt.Errorf("failed to get secret value: %w", err)
}
ageSecretKey = string(secretValue)
// 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)
}
} else {
// Secret doesn't exist, generate a new age secret key
identity, err := age.GenerateX25519Identity()
if err != nil {
return fmt.Errorf("failed to generate age secret key: %w", err)
}
ageSecretKey = identity.String()
// Store the new secret
if err := vault.AddSecret(secretName, []byte(ageSecretKey), false); err != nil {
return fmt.Errorf("failed to store age secret key: %w", err)
}
fmt.Fprintf(os.Stderr, "Generated new age secret key and stored in secret '%s'\n", secretName)
}
// Parse the age secret key to get the identity
identity, err := age.ParseX25519Identity(ageSecretKey)
if err != nil {
return fmt.Errorf("failed to parse age secret key: %w", err)
}
// Get the recipient (public key) for encryption
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 file.Close()
input = file
}
// Set up output writer
var output io.Writer = os.Stdout
if outputFile != "" {
file, err := cli.fs.Create(outputFile)
if err != nil {
return fmt.Errorf("failed to create output file: %w", err)
}
defer 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 *CLIInstance) Decrypt(secretName, inputFile, outputFile string) error {
// Get current vault
vault, err := secret.GetCurrentVault(cli.fs, cli.stateDir)
if err != nil {
return err
}
// Check if secret exists
secretObj := secret.NewSecret(vault, 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 secretValue []byte
if os.Getenv(secret.EnvMnemonic) != "" {
secretValue, err = secretObj.GetValue(nil)
} else {
unlockKey, unlockErr := vault.GetCurrentUnlockKey()
if unlockErr != nil {
return fmt.Errorf("failed to get current unlock key: %w", unlockErr)
}
secretValue, err = secretObj.GetValue(unlockKey)
}
if err != nil {
return fmt.Errorf("failed to get secret value: %w", err)
}
ageSecretKey := string(secretValue)
// 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 age secret key to get the identity
identity, err := age.ParseX25519Identity(ageSecretKey)
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 file.Close()
input = file
}
// Set up output writer
var output io.Writer = os.Stdout
if outputFile != "" {
file, err := cli.fs.Create(outputFile)
if err != nil {
return fmt.Errorf("failed to create output file: %w", err)
}
defer 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
}