- Fix staticcheck QF1011: Remove explicit type declaration for io.Writer variables - Fix tagliatelle: Change all JSON tags from snake_case to camelCase - created_at → createdAt - keychain_item_name → keychainItemName - age_public_key → agePublicKey - age_priv_key_passphrase → agePrivKeyPassphrase - encrypted_longterm_key → encryptedLongtermKey - derivation_index → derivationIndex - public_key_hash → publicKeyHash - mnemonic_family_hash → mnemonicFamilyHash - gpg_key_id → gpgKeyId - Fix lll: Break long function signature line to stay under 120 character limit All linter issues have been resolved. The codebase now passes all linter checks.
256 lines
6.8 KiB
Go
256 lines
6.8 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/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()
|
|
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 := NewCLIInstance()
|
|
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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
} else {
|
|
// Secret exists, get the age secret key from it
|
|
secretValue, err := cli.getSecretValue(vlt, secretObj)
|
|
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 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 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 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 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) ([]byte, 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)
|
|
}
|