forked from sneak/secret
Add blank lines before return statements in all files to satisfy the nlreturn linter. This improves code readability by providing visual separation before return statements. Changes made across 24 files: - internal/cli/*.go - internal/secret/*.go - internal/vault/*.go - pkg/agehd/agehd.go - pkg/bip85/bip85.go All 143 nlreturn issues have been resolved.
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
|
|
var output io.Writer = 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
|
|
var output io.Writer = 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)
|
|
}
|