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 }