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/spf13/cobra" ) func newEncryptCmd() *cobra.Command { cmd := &cobra.Command{ Use: "encrypt ", 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 ", 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 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 { // Secret exists, get the age secret key from it var secretValue []byte if os.Getenv(secret.EnvMnemonic) != "" { secretValue, err = secretObj.GetValue(nil) } else { unlocker, unlockErr := vlt.GetCurrentUnlocker() if unlockErr != nil { return fmt.Errorf("failed to get current unlocker: %w", unlockErr) } secretValue, err = secretObj.GetValue(unlocker) } 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 new age key and store it identity, err := age.GenerateX25519Identity() if err != nil { return fmt.Errorf("failed to generate age key: %w", err) } ageSecretKey = identity.String() // Store the generated key as a secret err = vlt.AddSecret(secretName, []byte(ageSecretKey), false) if err != nil { return fmt.Errorf("failed to store age key: %w", err) } } // Parse the secret key identity, err := age.ParseX25519Identity(ageSecretKey) 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 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 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 secretValue []byte if os.Getenv(secret.EnvMnemonic) != "" { secretValue, err = secretObj.GetValue(nil) } else { unlocker, unlockErr := vlt.GetCurrentUnlocker() if unlockErr != nil { return fmt.Errorf("failed to get current unlocker: %w", unlockErr) } secretValue, err = secretObj.GetValue(unlocker) } 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 }