NAME
Crypt::Age::Header - age file header parsing and generation
VERSION
version 0.002
SYNOPSIS
use Crypt::Age::Header;
# Create header for encryption
my $header = Crypt::Age::Header->create($file_key, \@recipient_public_keys);
my $header_text = $header->to_string;
# Parse header during decryption
my $offset = 0;
my $header = Crypt::Age::Header->parse(\$ciphertext, \$offset);
# Unwrap file key
my $file_key = $header->unwrap_file_key(\@identity_secret_keys);
DESCRIPTION
This module handles parsing and generation of age file headers.
An age file header is a text section at the beginning of an age file that contains:
Version line (
age-encryption.org/v1)One or more recipient stanzas (each wrapping the file key)
MAC footer (authenticates the header)
The header format is:
age-encryption.org/v1
-> X25519 <base64-ephemeral-public-key>
<base64-wrapped-file-key>
--- <base64-mac>
This is an internal module used by Crypt::Age.
stanzas
ArrayRef of Crypt::Age::Stanza objects representing recipient stanzas.
Each stanza wraps the file key for one recipient.
mac
The header MAC as raw bytes (32 bytes).
Used to authenticate the header and verify that the correct file key was unwrapped.
create
my $header = Crypt::Age::Header->create($file_key, \@recipients);
Creates a new header for encrypting to multiple recipients.
Parameters:
$file_key- The 16-byte file key to wrap\@recipients- ArrayRef of Bech32-encoded public keys (age1...)
Returns a Crypt::Age::Header object with stanzas for each recipient and a computed MAC.
to_string
my $header_text = $header->to_string;
Serializes the header to text format.
Returns a string containing the version line, all stanzas, and the MAC footer, suitable for writing to the beginning of an age file.
parse_from_fh
my $header = Crypt::Age::Header->parse_from_fh($fh);
Parses an age header directly from a filehandle.
Parameters:
$fh- An open, readable filehandle positioned at the first byte of the header
Puts the handle into :raw mode and reads it line by line (with "\n" as the input record separator) for the duration of the call, so the caller does not need to prepare the handle's discipline beforehand. It reads the version line, every recipient stanza, and the --- MAC footer line, stopping as soon as that footer line has been consumed. On return the handle is therefore positioned at the first byte of the payload -- this is what lets "parse" call tell on it afterwards to report the new offset.
While reading, it accumulates the literal header bytes it consumed -- the version line, every stanza line exactly as read, and the --- of the footer, with no trailing space, MAC value, or newline -- and stores them on the returned object. "verify_mac" authenticates against these captured bytes, not against a re-serialization of the parsed stanzas, so a header this method accepted is exactly the header the MAC is checked against. (Header construction on the write path, "create", has no bytes to capture and re-serializes the stanzas instead.)
Returns a Crypt::Age::Header object holding the parsed stanzas, the raw MAC bytes, and the captured header bytes. It does not verify the MAC itself -- that is "verify_mac"'s job, and it only runs after a file key has been unwrapped from one of the stanzas.
Dies if:
the first line is not the literal
age-encryption.org/v1version linea stanza body line is longer than 64 characters
a stanza body never reaches a line shorter than 64 characters before the handle runs out -- the required short (possibly empty) final line is missing
the handle runs out, or a line fails to match either a stanza start line (
-> type arg1 arg2 ...) or the---MAC footer (three dashes, a space, and a 43-character base64 MAC), before a valid MAC line has been founda stanza start line carries an argument that is empty (two spaces in a row, or a trailing space) or that contains a byte outside printable ASCII,
0x21-0x7e-- the format'sargument = 1*VCHAR, whereVCHARis RFC 5234's core rule. The first argument, the stanza type, is subject to the same set: the grammar defines no separate rule for it. This check applies to every stanza line in the header regardless of type, and rejecting is deliberate -- a byte outside the set invalidates the whole header rather than merely making that one stanza ignorablea stanza body, a stanza argument, or the MAC token fails the strict decoding in "decode_base64_no_padding" in Crypt::Age::Stanza --
=padding, a character outside the base64 alphabet, an impossible length, or a non-canonical encodingan
X25519stanza fails the checks in "BUILD" in Crypt::Age::Stanza::X25519: other than exactly one argument after the type, an argument that does not decode to a 32-byte value, or a body that is not exactly 32 bytes
A stanza of an unrecognized type is kept as a plain Crypt::Age::Stanza and is not validated beyond the structure every stanza shares -- the format requires unknown stanzas to be ignored, not rejected, since this is how recipient types are expected to be added in the future (grease). "The structure every stanza shares" does include the argument character set above: an unknown-type stanza whose arguments are all printable ASCII is ignored, one carrying a byte outside that set is a header failure, because the byte breaks the header's grammar rather than that one stanza's semantics.
This is the implementation "parse" wraps for its \$data/\$offset interface; see "parse" for that entry point.
parse
my $header = Crypt::Age::Header->parse(\$data, \$offset);
Parses an age header from encrypted data. This is a \$data/\$offset wrapper: it opens a filehandle on \$data and delegates the actual parsing to "parse_from_fh".
Parameters:
\$data- ScalarRef to the complete age file data\$offset- ScalarRef to offset, updated to point past the header
Returns a Crypt::Age::Header object. The $offset is updated to point to the start of the payload.
Dies if the header format is invalid. That includes a malformed X25519 stanza: one that does not carry exactly one argument after the type, whose argument is not the canonical unpadded base64 encoding of a 32-byte value, or whose body is not exactly 32 bytes. Those are header failures and are raised here, before any identity is looked at, rather than being deferred to "unwrap_file_key" and mistaken there for a stanza that simply does not match the identity. See "BUILD" in Crypt::Age::Stanza::X25519.
Stanzas of unrecognized types are kept as plain Crypt::Age::Stanza objects and are not validated beyond the structure every stanza shares; the format requires them to be ignored, not rejected.
verify_mac
my $ok = $header->verify_mac($file_key);
Verifies that the header MAC is correct for the given file key.
Returns 1 if the MAC is valid, 0 otherwise. Used to confirm that the correct file key was unwrapped from a stanza.
The comparison goes through slow_eq from Crypt::Misc, so a wrong MAC is not rejected at the first differing byte. A MAC of the wrong length -- or no MAC at all -- returns 0; it is never fatal.
unwrap_file_key
my $file_key = $header->unwrap_file_key(\@identities);
Attempts to unwrap the file key using one or more identities.
Parameters:
\@identities- ArrayRef of Bech32-encoded secret keys (AGE-SECRET-KEY-1...)
Tries each identity against each stanza until one successfully unwraps the file key and verifies the MAC. Returns the 16-byte file key. Stanzas of other types are skipped, so a file that mixes recipient types still decrypts.
Dies if no matching identity is found or if MAC verification fails. It does not die for a structurally invalid X25519 stanza -- "parse" has already rejected the header by then -- but it does propagate the abort that a low-order-point ephemeral share triggers, since that is a header failure too and not a wrong identity.
SEE ALSO
Crypt::Age - Main age encryption module
Crypt::Age::Stanza - Base stanza class
Crypt::Age::Stanza::X25519 - X25519 recipient stanza
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.