Security Advisories (1)
CVE-2026-85783 (2026-10-20)

Crypt::Age versions before 0.004 for Perl allow CPU exhaustion via an unbounded number of recipient stanzas, each costing an X25519 scalar multiplication in unwrap_file_key. The header parser accepts any number of recipient stanzas, and unwrap_file_key tries every identity against every X25519 stanza. Each attempt runs a scalar multiplication, an HKDF and a ChaCha20-Poly1305 open. The header MAC cannot be checked first, because its key comes from the file key, which only a successful unwrap produces. Each stanza is 98 bytes of attacker-chosen header and buys one scalar multiplication per identity held, and the file is rejected only after every one of them has run. A caller reaches this through decrypt, decrypt_file or decrypt_filehandle whenever it decrypts a file it did not write.

NAME

Crypt::Age::Stanza::X25519 - X25519 recipient stanza for age encryption

VERSION

version 0.002

SYNOPSIS

use Crypt::Age::Stanza::X25519;

# Create stanza by wrapping file key for a recipient
my $stanza = Crypt::Age::Stanza::X25519->wrap($file_key, $recipient_public_key);

# Unwrap file key using identity
my $file_key = $stanza->unwrap($identity_secret_key);

DESCRIPTION

This module implements X25519 recipient stanzas for age encryption.

X25519 stanzas use Curve25519 Diffie-Hellman key exchange to derive a shared secret, which is then used to wrap the file key with ChaCha20-Poly1305.

The stanza format is:

-> X25519 <base64-ephemeral-public-key>
<base64-wrapped-file-key>

The ephemeral public key is generated randomly for each encryption operation. The recipient uses their identity (secret key) to compute the same shared secret and unwrap the file key.

This is the primary recipient type for age encryption.

ephemeral_public

The ephemeral X25519 public key used for this stanza (raw bytes).

Generated randomly during wrapping.

BUILD

Validates the stanza at construction time. Not called directly; it runs from new, and therefore from "wrap" and from "parse" in Crypt::Age::Header, which builds this class for every stanza whose type is X25519.

The age format requires an identity implementation to reject -- as a header failure, not as an identity that merely does not match -- an X25519 stanza that

  • does not carry exactly one argument after the type

  • whose argument is not the canonical unpadded base64 encoding of a 32-byte value

  • whose body is not exactly 32 bytes, which mitigates partitioning oracle attacks by refusing to attempt decryption at all

Dies on any of these. The messages give the reason and never the offending value, which is key material. Stanzas of other types are not affected: they are built as plain Crypt::Age::Stanza objects and are ignored rather than rejected, as the format requires.

wrap

my $stanza = Crypt::Age::Stanza::X25519->wrap($file_key, $recipient_public_key);

Wraps a file key for a recipient.

Parameters:

  • $file_key - The 16-byte file key to wrap

  • $recipient_public_key - Bech32-encoded public key (age1...)

Generates an ephemeral X25519 keypair, performs key exchange with the recipient's public key, derives a wrapping key, and wraps the file key.

Returns a Crypt::Age::Stanza::X25519 object.

unwrap

my $file_key = $stanza->unwrap($identity_secret_key);

Attempts to unwrap the file key using an identity.

Parameters:

  • $identity_secret_key - Bech32-encoded secret key (AGE-SECRET-KEY-1...)

Performs key exchange with the ephemeral public key from the stanza, derives the wrapping key, and attempts to unwrap the file key.

Returns the 16-byte file key on success, or undef when the AEAD authentication fails, which means this identity is not the one the stanza was written for. undef is the only "no match" answer: a structurally invalid stanza cannot be constructed in the first place (see "BUILD"), and a low-order-point ephemeral share dies rather than returning undef.

SEE ALSO

SUPPORT

Issues

Please report bugs and feature requests on GitHub at https://github.com/Getty/p5-crypt-age/issues.

CONTRIBUTING

Contributions are welcome! Please fork the repository and submit a pull request.

AUTHOR

Torsten Raudssus <torsten@raudssus.de>

COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Torsten Raudssus.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.