package cli import ( "errors" "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" ) // Sentinel errors for encrypt/decrypt operations var ( errNotAgeSecretKey = errors.New( "does not contain a valid age secret key") errSecretDoesNotExist = errors.New("does not exist") ) // newCryptoCmd builds an encrypt/decrypt command with input/output flags func newCryptoCmd( use, short, long string, run func(cli *Instance, secretName, inputFile, outputFile string) error, ) *cobra.Command { cmd := &cobra.Command{ Use: use, Short: short, Long: long, 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 run(cli, 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 newEncryptCmd() *cobra.Command { return newCryptoCmd( "encrypt ", "Encrypt data using an age secret key stored in a secret", "Encrypt data using an age secret key. If the secret doesn't "+ "exist, a new age key is generated and stored.", (*Instance).Encrypt, ) } func newDecryptCmd() *cobra.Command { return newCryptoCmd( "decrypt ", "Decrypt data using an age secret key stored in a secret", "Decrypt data using an age secret key stored in the specified secret.", (*Instance).Decrypt, ) } // resolveEncryptionKey returns a secure buffer holding the age secret key // for the named secret, generating and storing a new key if the secret // does not exist. The caller must destroy the returned buffer. func (cli *Instance) resolveEncryptionKey( vlt *vault.Vault, secretName string, ) (*memguard.LockedBuffer, error) { // Check if secret exists secretObj := secret.NewSecret(vlt, secretName) exists, err := secretObj.Exists() if err != nil { return nil, fmt.Errorf("failed to check if secret exists: %w", err) } if !exists { // Secret doesn't exist, generate new age key and store it identity, err := age.GenerateX25519Identity() if err != nil { return nil, fmt.Errorf("failed to generate age key: %w", err) } // Store the generated key directly in a secure buffer secureBuffer := memguard.NewBufferFromBytes([]byte(identity.String())) err = vlt.AddSecret(secretName, secureBuffer, false) if err != nil { secureBuffer.Destroy() return nil, fmt.Errorf("failed to store age key: %w", err) } return secureBuffer, nil } // Secret exists, get the age secret key from it secretBuffer, err := cli.getSecretValue(vlt, secretObj) if err != nil { return nil, fmt.Errorf("failed to get secret value: %w", err) } // Validate that it's a valid age secret key if !isValidAgeSecretKey(secretBuffer.String()) { secretBuffer.Destroy() return nil, fmt.Errorf("secret '%s' %w", secretName, errNotAgeSecretKey) } return secretBuffer, nil } // 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 } // Get or create the age secret key for this secret keyBuffer, err := cli.resolveEncryptionKey(vlt, secretName) if err != nil { return err } defer keyBuffer.Destroy() // Parse the secret key identity, err := age.ParseX25519Identity(keyBuffer.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) } _, err = io.Copy(encryptor, input) if err != nil { return fmt.Errorf("failed to encrypt data: %w", err) } err = encryptor.Close() if 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' %w", secretName, errSecretDoesNotExist) } // Get the age secret key from the secret secretBuffer, err := cli.getSecretValue(vlt, secretObj) 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' %w", secretName, errNotAgeSecretKey) } // 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) } _, err = io.Copy(output, decryptor) if 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) }