summaryrefslogtreecommitdiffstats
path: root/internal/cryptokit/age.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/cryptokit/age.go')
-rw-r--r--internal/cryptokit/age.go91
1 files changed, 91 insertions, 0 deletions
diff --git a/internal/cryptokit/age.go b/internal/cryptokit/age.go
new file mode 100644
index 0000000..525b1fa
--- /dev/null
+++ b/internal/cryptokit/age.go
@@ -0,0 +1,91 @@
+package cryptokit
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "strings"
+
+ "filippo.io/age"
+ "filippo.io/age/agessh"
+ agearmor "filippo.io/age/armor"
+)
+
+const maxCryptoMessageBytes = 64 << 20
+
+func parseAgeRecipient(value string) (age.Recipient, error) {
+ value = strings.TrimSpace(value)
+ if recipient, err := age.ParseX25519Recipient(value); err == nil {
+ return recipient, nil
+ }
+ if recipient, err := agessh.ParseRecipient(value); err == nil {
+ return recipient, nil
+ }
+ return nil, errors.New("unsupported age recipient: expected native X25519 or SSH Ed25519/RSA")
+}
+
+func parseAgeIdentity(value string) (age.Identity, error) {
+ value = strings.TrimSpace(value)
+ if identity, err := age.ParseX25519Identity(value); err == nil {
+ return identity, nil
+ }
+ if identity, err := agessh.ParseIdentity([]byte(value)); err == nil {
+ return identity, nil
+ }
+ return nil, errors.New("unsupported age identity: expected native X25519 or SSH Ed25519/RSA private key")
+}
+
+// EncryptAge encrypts to a user-provided native age X25519 recipient or SSH
+// Ed25519/RSA recipient and returns ASCII-armored age text.
+func EncryptAge(message []byte, recipientKey string) ([]byte, error) {
+ recipient, err := parseAgeRecipient(recipientKey)
+ if err != nil {
+ return nil, fmt.Errorf("parse age recipient: %w", err)
+ }
+ var output bytes.Buffer
+ armored := agearmor.NewWriter(&output)
+ writer, err := age.Encrypt(armored, recipient)
+ if err != nil {
+ _ = armored.Close()
+ return nil, fmt.Errorf("create age encryption: %w", err)
+ }
+ if _, err := writer.Write(message); err != nil {
+ _ = writer.Close()
+ _ = armored.Close()
+ return nil, fmt.Errorf("write age message: %w", err)
+ }
+ if err := writer.Close(); err != nil {
+ _ = armored.Close()
+ return nil, fmt.Errorf("close age message: %w", err)
+ }
+ if err := armored.Close(); err != nil {
+ return nil, fmt.Errorf("close age armor: %w", err)
+ }
+ return output.Bytes(), nil
+}
+
+// DecryptAge decrypts an armored or binary age message with a user-provided
+// native X25519 identity or an unencrypted SSH Ed25519/RSA private key.
+func DecryptAge(message []byte, identityKey string) ([]byte, error) {
+ identity, err := parseAgeIdentity(identityKey)
+ if err != nil {
+ return nil, fmt.Errorf("parse age identity: %w", err)
+ }
+ var input io.Reader = bytes.NewReader(message)
+ if bytes.HasPrefix(bytes.TrimSpace(message), []byte(agearmor.Header)) {
+ input = agearmor.NewReader(input)
+ }
+ reader, err := age.Decrypt(input, identity)
+ if err != nil {
+ return nil, fmt.Errorf("create age decryption: %w", err)
+ }
+ plaintext, err := io.ReadAll(io.LimitReader(reader, maxCryptoMessageBytes+1))
+ if err != nil {
+ return nil, fmt.Errorf("read age message: %w", err)
+ }
+ if len(plaintext) > maxCryptoMessageBytes {
+ return nil, errors.New("decrypted age message exceeds the 64 MiB limit")
+ }
+ return plaintext, nil
+}