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.