244 lines
6.6 KiB
Go
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
|
|
}
|