diff options
| author | Gab Virebent <gabriel1@virebent.art> | 2026-08-22 20:36:56 +0200 |
|---|---|---|
| committer | Gab Virebent <gabriel1@virebent.art> | 2026-08-22 20:36:56 +0200 |
| commit | c1decadb590c4d79d92bcc4df7772119a5c91244 (patch) | |
| tree | 468f9fa37535dbb90cc5d4061eb20de83912131e /internal/cryptokit/age.go | |
| download | aegis-c1decadb590c4d79d92bcc4df7772119a5c91244.tar.gz aegis-c1decadb590c4d79d92bcc4df7772119a5c91244.tar.xz aegis-c1decadb590c4d79d92bcc4df7772119a5c91244.zip | |
Initial Aegis Usenet client release
Diffstat (limited to 'internal/cryptokit/age.go')
| -rw-r--r-- | internal/cryptokit/age.go | 91 |
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 +} |
