diff options
| author | Levi Durfee <levi.durfee@gmail.com> | 2026-01-06 19:08:34 -0500 |
|---|---|---|
| committer | Levi Durfee <levi.durfee@gmail.com> | 2026-01-06 19:08:44 -0500 |
| commit | 35a6325ba12d0462bf01eb740fb6abde2d43c17a (patch) | |
| tree | e102dac429a78557ab4078d66bbd175d88b2d277 | |
| parent | 452911586df6115a7c8deadee87fe5d97a7fe36f (diff) | |
Add ability to encrypt files
| -rw-r--r-- | cmd/goaes/commands/decrypt.go | 12 | ||||
| -rw-r--r-- | cmd/goaes/commands/encrypt.go | 38 | ||||
| -rw-r--r-- | cmd/goaes/main.go | 26 | ||||
| -rw-r--r-- | internal/encrypt.go | 38 | ||||
| -rw-r--r-- | internal/goaes.go | 95 |
5 files changed, 209 insertions, 0 deletions
diff --git a/cmd/goaes/commands/decrypt.go b/cmd/goaes/commands/decrypt.go new file mode 100644 index 0000000..f89da22 --- /dev/null +++ b/cmd/goaes/commands/decrypt.go @@ -0,0 +1,12 @@ +package commands + +import ( + "context" + + "github.com/urfave/cli/v3" +) + +func Decrypt(ctx context.Context, cmd *cli.Command) error { + + return nil +} diff --git a/cmd/goaes/commands/encrypt.go b/cmd/goaes/commands/encrypt.go new file mode 100644 index 0000000..0c5d578 --- /dev/null +++ b/cmd/goaes/commands/encrypt.go @@ -0,0 +1,38 @@ +package commands + +import ( + "bytes" + "context" + "encoding/gob" + "os" + + "github.com/nerdsec/goaes/internal" + "github.com/urfave/cli/v3" +) + +func Encrypt(ctx context.Context, cmd *cli.Command) error { + source := cmd.String("source") + destination := cmd.String("destination") + + plaintext, err := os.ReadFile(source) + if err != nil { + return err + } + + payload, err := internal.Encrypt(plaintext) + if err != nil { + return err + } + + var dataBuffer bytes.Buffer + enc := gob.NewEncoder(&dataBuffer) + + err = enc.Encode(payload) + if err != nil { + return err + } + + os.WriteFile(destination, dataBuffer.Bytes(), 0666) + + return nil +} diff --git a/cmd/goaes/main.go b/cmd/goaes/main.go index 30f1d1d..f0b4368 100644 --- a/cmd/goaes/main.go +++ b/cmd/goaes/main.go @@ -23,6 +23,32 @@ func main() { Usage: "Generate a base64 encoded key", Action: commands.Generate, }, + { + Name: "encrypt", + Aliases: []string{"e"}, + Usage: "Encrypt a file", + Action: commands.Encrypt, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "source", + Aliases: []string{"s"}, + Usage: "source file to encrypt", + Required: true, + }, + &cli.StringFlag{ + Name: "destination", + Aliases: []string{"d"}, + Usage: "where to write the encrypted file", + Required: true, + }, + }, + }, + { + Name: "decrypt", + Aliases: []string{"d"}, + Usage: "Decrypt a file", + Action: commands.Decrypt, + }, }, } diff --git a/internal/encrypt.go b/internal/encrypt.go new file mode 100644 index 0000000..a404de7 --- /dev/null +++ b/internal/encrypt.go @@ -0,0 +1,38 @@ +package internal + +import ( + "log" + + "github.com/joho/godotenv" +) + +func Encrypt(data []byte) (EncryptedDataPayload, error) { + if err := godotenv.Load(); err != nil { + log.Fatal("Error loading .env file") + } + + kek, err := NewKEKFromEnvB64("SECRET_KEY") + if err != nil { + return EncryptedDataPayload{}, err + } + + dek, err := NewDEK() + if err != nil { + return EncryptedDataPayload{}, err + } + + edek, err := WrapDEK(dek, kek) + if err != nil { + return EncryptedDataPayload{}, err + } + + ct, err := EncryptData(data, dek) + if err != nil { + return EncryptedDataPayload{}, err + } + + return EncryptedDataPayload{ + DEK: edek, + Payload: ct, + }, nil +} diff --git a/internal/goaes.go b/internal/goaes.go index 668ef17..ce054e8 100644 --- a/internal/goaes.go +++ b/internal/goaes.go @@ -1,11 +1,34 @@ package internal import ( + "crypto/aes" + "crypto/cipher" "crypto/rand" + "encoding/base64" + "errors" "fmt" "io" + "os" ) +func NewKEKFromEnvB64(envVar string) (KEK, error) { + b64 := os.Getenv(envVar) + if b64 == "" { + return nil, fmt.Errorf("%s is not set", envVar) + } + + raw, err := base64.StdEncoding.DecodeString(b64) + if err != nil { + return nil, fmt.Errorf("decode %s base64: %w", envVar, err) + } + + if !validAESKeyLen(len(raw)) { + return nil, errBadKeyLn + } + + return KEK(raw), nil +} + func NewDEK() (DEK, error) { key := make([]byte, 32) // AES-256 if _, err := io.ReadFull(rand.Reader, key); err != nil { @@ -13,3 +36,75 @@ func NewDEK() (DEK, error) { } return DEK(key), nil } + +func WrapDEK(dek DEK, kek KEK) (WrappedDEK, error) { + edek, err := encryptAEAD([]byte(dek), []byte(kek), aadWrapDEK) + return WrappedDEK(edek), err +} + +func UnwrapDEK(edek WrappedDEK, kek KEK) (DEK, error) { + dek, err := decryptAEAD([]byte(edek), []byte(kek), aadWrapDEK) + return DEK(dek), err +} + +func EncryptData(plaintext []byte, dek DEK) (Ciphertext, error) { + ct, err := encryptAEAD(plaintext, []byte(dek), aadDataMsg) + return Ciphertext(ct), err +} + +func DecryptData(ct Ciphertext, dek DEK) ([]byte, error) { + return decryptAEAD([]byte(ct), []byte(dek), aadDataMsg) +} + +// encryptAEAD returns: nonce || ciphertext +func encryptAEAD(plaintext, key, aad []byte) ([]byte, error) { + if !validAESKeyLen(len(key)) { + return nil, errBadKeyLn + } + + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + + return gcm.Seal(nonce, nonce, plaintext, aad), nil +} + +func decryptAEAD(ciphertext, key, aad []byte) ([]byte, error) { + if !validAESKeyLen(len(key)) { + return nil, errBadKeyLn + } + + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + ns := gcm.NonceSize() + if len(ciphertext) < ns { + return nil, errors.New("ciphertext too short") + } + + nonce := ciphertext[:ns] + body := ciphertext[ns:] + return gcm.Open(nil, nonce, body, aad) +} + +func validAESKeyLen(n int) bool { + return n == 16 || n == 24 || n == 32 +} |
