Crypt-Age

 view release on metacpan or  search on metacpan

lib/Crypt/Age/Header.pm  view on Meta::CPAN


=item * C<$file_key> - The 16-byte file key to wrap

=item * C<\@recipients> - ArrayRef of Bech32-encoded public keys (C<age1...>)

=back

C<\@recipients> must really be an ArrayRef; a single recipient still goes in a
list of one. Every other shape used to reach a raw dereference and be reported
by perl as C<"Can't use string (...) as an ARRAY ref while "strict refs" in
use">, C<"Can't use an undefined value as an ARRAY reference"> or C<"Not an
ARRAY reference">, each of them blaming a line in this module for a mistake
made one frame up. They are now refused, before the file key is wrapped, with
C<"recipients must be an ArrayRef: this method wraps the file key once per
entry, pass [$recipient] rather than $recipient">. As elsewhere in this module
the clause after the colon carries the requirement and its reason rather than a
description of what arrived, since one message answers all of those shapes.

It quotes no part of the argument either. The first of perl's messages above
quoted the caller's string, truncated at 32 characters; the string in this
parameter is normally a public key, but the mistake that puts a bare string
here is the same one that swaps recipient and identity, which is why the
per-entry croak below reports that swap. See L</unwrap_file_key>, where the
value is never public.

An B<empty> ArrayRef is refused as well, with C<"recipients must not be empty:
this method wraps the file key once per entry, so a header with no stanzas can
never be unwrapped, pass at least one recipient">. It used to be accepted: it
passed the shape check, wrapped the file key for nobody, and returned a header
consisting of a version line and a MAC over it. The age header grammar is
C<header = v1-line 1*stanza end> -- one or more stanzas -- so that was not a
valid header at all, and the file it started could never be decrypted by
anyone, the caller included, because the file key is generated per file and
kept nowhere else. C<rage> 0.12.1 refuses such a file as C<"Unknown age
format">; C<age> 1.2.1 parses it and reports C<"no identity matched any of the
recipients">. Both arrive after the plaintext is unrecoverable, which is why
this is refused here instead.

Returns a L<Crypt::Age::Header> object with stanzas for each recipient and a
computed MAC.

The C<age1> prefix is matched case-insensitively, so an all-uppercase
C<AGE1...> recipient is accepted as well -- BIP-173 defines it as the same
encoding of the same key, and L<Crypt::Age::Keys/decode_public_key> decodes it.
A recipient mixing the two cases is not: it dies with C<Invalid bech32: mixed
case> from L<Crypt::Age::Keys/bech32_decode>. Any other recipient string dies
with C<"Unsupported recipient format at index N: expected an age1 recipient">,
where C<N> is the recipient's position in C<\@recipients>. A string that looks
like an identity adds C<", got an AGE-SECRET-KEY-1 identity"> -- the swap of
recipient and identity is the likely mistake, and both are plain strings.

An C<undef> entry dies with that same message and C<", got undef"> in place of
the identity hint; it is reported before any string operation touches it, so it
no longer produces two C<"Use of uninitialized value"> warnings ahead of the
error that explains it.

The offending string itself is never part of the message. It may be a secret
key, and the exception ends up in the caller's logs; the index locates the
entry without quoting it.

The case of the recipient string does not reach the file. It is decoded to raw
bytes here, and the stanza carries the ephemeral public key, not the recipient.

=head2 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.

=head2 parse_from_fh

    my $header = Crypt::Age::Header->parse_from_fh($fh);

Parses an age header directly from a filehandle.

Parameters:

=over 4

=item * C<$fh> - An open, readable filehandle positioned at the first byte of
the header

=back

Puts the handle into C<:raw> mode and reads it line by line (with C<"\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 C<---> 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 L</parse>
call C<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 C<---> of the footer,
with no trailing space, MAC value, or newline -- and stores them on the
returned object. L</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, L</create>, has no bytes to capture and
re-serializes the stanzas instead.)

Returns a L<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 L</verify_mac>'s job, and it only runs after a file key has been unwrapped
from one of the stanzas.

Dies if:

=over 4

=item * the first line is not the literal C<age-encryption.org/v1> version
line -- including when there is no first line at all, because the handle is
already at end of input. That case reads as an absent version line and gets
the same message, without warning about the C<undef> that C<readline> returned

=item * a stanza body line is longer than 64 characters

=item * a stanza body never reaches a line shorter than 64 characters before



( run in 1.070 second using v1.01-cache-2.11-cpan-d01c6094234 )