mirror of
https://github.com/ivuorinen/a.git
synced 2026-01-26 11:34:07 +00:00
173 lines
5.1 KiB
Go
173 lines
5.1 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// Encrypt returns a cobra.Command that encrypts files using age, supporting GitHub key fetching.
|
|
func Encrypt(cfg *Config, log *logrus.Logger) *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "encrypt",
|
|
Short: "Encrypt a file",
|
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
input, _ := cmd.Flags().GetString("input")
|
|
output, _ := cmd.Flags().GetString("output")
|
|
recipients, _ := cmd.Flags().GetStringSlice("recipient")
|
|
ghUserFlag, _ := cmd.Flags().GetString("github-user")
|
|
|
|
if input == "" {
|
|
return fmt.Errorf("input file is required")
|
|
}
|
|
if output == "" {
|
|
return fmt.Errorf("output file is required")
|
|
}
|
|
if _, err := os.Stat(input); err != nil {
|
|
return fmt.Errorf("input file does not exist: %w", err)
|
|
}
|
|
|
|
allRecipients, ghUser, err := collectRecipients(cfg, recipients, ghUserFlag, log)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(allRecipients) == 0 {
|
|
return fmt.Errorf("at least one recipient is required")
|
|
}
|
|
|
|
ageArgs, err := buildAgeArgs(output, input, allRecipients)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log.WithFields(logrus.Fields{
|
|
"input": input,
|
|
"output": output,
|
|
"recipients": allRecipients,
|
|
"githubUser": ghUser,
|
|
}).Info("Encrypting file")
|
|
|
|
if err := runAgeEncrypt(ageArgs, log); err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Info("Encryption successful")
|
|
return nil
|
|
},
|
|
}
|
|
cmd.Flags().StringP("input", "i", "", "Input file to encrypt")
|
|
cmd.Flags().StringP("output", "o", "", "Output file for encrypted data")
|
|
cmd.Flags().StringSliceP("recipient", "r", []string{}, "Recipient public key file or string")
|
|
cmd.Flags().String("github-user", "", "GitHub username to fetch public keys for encryption")
|
|
return cmd
|
|
}
|
|
|
|
// Helper to collect recipients including GitHub keys
|
|
func collectRecipients(
|
|
cfg *Config,
|
|
recipients []string,
|
|
ghUserFlag string,
|
|
log *logrus.Logger,
|
|
) ([]string, string, error) {
|
|
allRecipients := append([]string{}, cfg.DefaultRecipients...)
|
|
allRecipients = append(allRecipients, recipients...)
|
|
|
|
ghUser := ghUserFlag
|
|
if ghUser == "" && cfg.GitHubUser != "" {
|
|
ghUser = cfg.GitHubUser
|
|
}
|
|
|
|
if ghUser != "" {
|
|
validUser := regexp.MustCompile(`^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?$`)
|
|
if !validUser.MatchString(ghUser) {
|
|
log.Warnf("Invalid GitHub username: %s", ghUser)
|
|
} else {
|
|
url := fmt.Sprintf("https://github.com/%s.keys", ghUser)
|
|
if !strings.HasPrefix(url, "https://github.com/") || !strings.HasSuffix(url, ".keys") {
|
|
log.Warnf("Refusing to fetch keys from non-GitHub URL: %s", url)
|
|
} else {
|
|
// #nosec G107 -- url is validated to be a GitHub keys endpoint above
|
|
resp, err := http.Get(url)
|
|
if err != nil {
|
|
log.WithError(err).Warnf("Failed to fetch GitHub keys for user %s", ghUser)
|
|
} else {
|
|
var githubKeys []string
|
|
if resp.StatusCode == http.StatusOK {
|
|
body, err := io.ReadAll(resp.Body)
|
|
closeErr := resp.Body.Close()
|
|
if err == nil && closeErr == nil {
|
|
for _, line := range strings.Split(string(body), "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line != "" {
|
|
githubKeys = append(githubKeys, line)
|
|
}
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
log.WithError(err).Warn("Failed to read GitHub keys response body")
|
|
}
|
|
if closeErr != nil {
|
|
log.WithError(closeErr).Warn("Failed to close GitHub keys response body")
|
|
}
|
|
}
|
|
} else {
|
|
_ = resp.Body.Close()
|
|
log.Warnf("GitHub returned status %d for user %s", resp.StatusCode, ghUser)
|
|
}
|
|
allRecipients = append(allRecipients, githubKeys...)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return allRecipients, ghUser, nil
|
|
}
|
|
|
|
// Helper to build and validate age arguments
|
|
func buildAgeArgs(output, input string, recipients []string) ([]string, error) {
|
|
ageArgs := []string{"-o", output}
|
|
for _, r := range recipients {
|
|
ageArgs = append(ageArgs, "-r", r)
|
|
}
|
|
ageArgs = append(ageArgs, input)
|
|
|
|
// Only allow expected flags for age and restrict file extensions
|
|
expectedFlags := map[string]bool{"-o": true, "-r": true}
|
|
for i, arg := range ageArgs {
|
|
if i%2 == 0 && i < len(ageArgs)-2 { // flags before last two args
|
|
if !expectedFlags[arg] {
|
|
return nil, fmt.Errorf("unexpected flag in age arguments: %s", arg)
|
|
}
|
|
} else if arg == "" {
|
|
return nil, fmt.Errorf("invalid argument for encryption: empty string")
|
|
}
|
|
}
|
|
// Restrict output to expected file extensions
|
|
if !strings.HasSuffix(output, ".txt") && !strings.HasSuffix(output, ".out") {
|
|
return nil, fmt.Errorf("invalid output file for encryption: %s", output)
|
|
}
|
|
return ageArgs, nil
|
|
}
|
|
|
|
// Helper to run age encryption command
|
|
func runAgeEncrypt(ageArgs []string, log *logrus.Logger) error {
|
|
ageBin := "age"
|
|
if ageBin != "age" {
|
|
return fmt.Errorf("invalid binary for encryption: %s", ageBin)
|
|
}
|
|
cmdAge := exec.Command(ageBin, ageArgs...)
|
|
if err := cmdAge.Run(); err != nil {
|
|
log.WithError(err).Error("Encryption failed")
|
|
return fmt.Errorf("age encryption failed: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Config struct should be imported from the main package or shared as needed.
|