Crypt-Age
view release on metacpan or search on metacpan
lib/Crypt/Age/Keys.pm view on Meta::CPAN
=head1 SYNOPSIS
use Crypt::Age::Keys;
# Generate keypair
my ($public, $secret) = Crypt::Age::Keys->generate_keypair();
# Encode/decode public keys
my $encoded_public = Crypt::Age::Keys->encode_public_key($public_bytes);
my $public_bytes = Crypt::Age::Keys->decode_public_key('age1...');
# Encode/decode secret keys
my $encoded_secret = Crypt::Age::Keys->encode_secret_key($secret_bytes);
my $secret_bytes = Crypt::Age::Keys->decode_secret_key('AGE-SECRET-KEY-1...');
# Derive public key from secret key
my $public = Crypt::Age::Keys->public_key_from_secret($secret);
=head1 DESCRIPTION
This module provides key generation and Bech32 encoding/decoding for age encryption.
age uses X25519 (Curve25519 Diffie-Hellman) for key agreement. Keys are encoded
using Bech32, the same encoding used for Bitcoin SegWit addresses (BIP-173).
Public keys use the human-readable part C<age> and are lowercase. Secret keys
use the human-readable part C<age-secret-key-> and are uppercase.
This is an internal module used by L<Crypt::Age>.
=head2 generate_keypair
my ($public_key, $secret_key) = Crypt::Age::Keys->generate_keypair();
Generates a new X25519 keypair.
Returns a list of two Bech32-encoded strings:
=over 4
=item * C<$public_key> - Starts with C<age1>, lowercase
=item * C<$secret_key> - Starts with C<AGE-SECRET-KEY-1>, uppercase
=back
=head2 encode_public_key
my $encoded = Crypt::Age::Keys->encode_public_key($public_bytes);
Encodes a 32-byte X25519 public key as a Bech32 string with HRP C<age>.
Returns a lowercase string starting with C<age1>.
=head2 decode_public_key
my $public_bytes = Crypt::Age::Keys->decode_public_key('age1...');
Decodes a Bech32-encoded age public key to raw bytes.
Dies if the HRP is not C<age>, if the decoded data is not 32 bytes, or if the
string mixes upper- and lowercase; see L</bech32_decode>. The HRP is compared
case-insensitively, so an all-uppercase C<AGE1...> key is accepted as well.
The HRP mismatch is reported as C<"Invalid public key HRP: expected the
literal age prefix, pass an age recipient rather than an identity or some
other Bech32 string">. It names the expected HRP, which is a constant of the
format, and B<not> the one that arrived: the received HRP is everything before
the last C<1> of the string that was passed in, so it is a prefix of the
caller's own material. Here that material is a public key and no secret is at
stake, but the message reads the same as L</decode_secret_key>'s, where it is.
Callers matching on the old C<"expected 'age', got '...'"> wording see the new
message instead.
=head2 encode_secret_key
my $encoded = Crypt::Age::Keys->encode_secret_key($secret_bytes);
Encodes a 32-byte X25519 secret key as a Bech32 string with HRP C<age-secret-key->.
Returns an uppercase string starting with C<AGE-SECRET-KEY-1>.
=head2 decode_secret_key
my $secret_bytes = Crypt::Age::Keys->decode_secret_key('AGE-SECRET-KEY-1...');
Decodes a Bech32-encoded age secret key to raw bytes.
Dies if the HRP is not C<age-secret-key->, if the decoded data is not 32 bytes,
or if the string mixes upper- and lowercase; see L</bech32_decode>. The HRP is
compared case-insensitively, so an all-lowercase C<age-secret-key-1...> key is
accepted as well as the uppercase form L</encode_secret_key> emits.
The HRP mismatch is reported as C<"Invalid secret key HRP: expected the literal
age-secret-key- prefix, pass an age identity rather than a recipient or some
other Bech32 string">, and quotes no part of what arrived. The received HRP is
everything before the last C<1> of the caller's string, so a string whose HRP
is the opening characters of a real identity would have had those characters
written into an exception raised inside this module, where the caller can no
longer redact them.
Reaching that croak needs a Bech32 string whose checksum verifies over the
wrong HRP, so it is a constructed input rather than a mistyped one: a key
truncated anywhere dies first with C<"Invalid bech32: no separator">,
C<"Invalid bech32: empty data"> or C<"Invalid bech32 checksum">, and one with
trailing junk with C<"Invalid bech32 checksum">, none of which quote anything
either. The everyday way to reach it -- passing a public key here, or an
identity to L</decode_public_key> -- puts only the other type's prefix in that
position. Callers matching on the old C<"expected 'age-secret-key-', got
'...'"> wording see the new message instead.
=head2 public_key_from_secret
my $public_key = Crypt::Age::Keys->public_key_from_secret($secret_key);
Derives the public key from a secret key.
Takes a Bech32-encoded secret key and returns the corresponding Bech32-encoded
public key. This is useful for when you have a secret key and need to know
what public key it corresponds to.
=head1 IMPLEMENTATION NOTES
C<bech32_polymod>, C<bech32_hrp_expand>, C<bech32_create_checksum> and
C<bech32_verify_checksum> below implement the checksum algorithm from BIP-173.
They are called only by L</bech32_encode> and L</bech32_decode> in this same
class, as plain functions rather than through the C<< $class->method(...) >>
convention the rest of this module uses, and are not documented individually
here.
=head2 bech32_encode
my $encoded = Crypt::Age::Keys->bech32_encode($hrp, $bytes);
Encodes C<$bytes> as Bech32 (BIP-173) with the given human-readable part
C<$hrp>: converts the bytes from 8-bit to 5-bit groups, computes the checksum,
and joins C<$hrp>, the C<1> separator, the data and the checksum through the
Bech32 charset.
This is the generic codec L</encode_public_key> and L</encode_secret_key> call;
most callers want those instead, since they also know the age HRPs and enforce
the 32-byte key length that this method does not.
=head2 bech32_decode
my ($hrp, $bytes) = Crypt::Age::Keys->bech32_decode($encoded);
Decodes a Bech32 (BIP-173) string, verifying its checksum. Returns the
human-readable part exactly as it appeared in C<$encoded> (not lowercased) and
the decoded data as raw bytes.
This is the generic codec L</decode_public_key> and L</decode_secret_key> call;
most callers want those instead, since they also check the HRP and the decoded
length.
Dies if there is no C<1> separator, if the data part is empty, if it contains a
character outside the Bech32 charset, or if the checksum does not verify.
The charset failure names the position rather than the character: C<Invalid
bech32 character at offset N>, where C<N> is a 0-based offset into the string
that was passed in -- not into the data part after the separator -- so
C<substr($encoded, $N, 1)> is the character it rejected. Withholding the
character is deliberate. Anything a caller passes reaches here, and a string
that is not an age key at all -- a passphrase handed to this method by mistake
-- would otherwise have one of its own bytes quoted back in an exception that
tends to end up in a log. No byte of an actual key is at stake either way:
every character of an encoded key is inside the charset, so none of them can
reach this failure.
BIP-173 also requires an encoding to be entirely uppercase or entirely
lowercase, and this method enforces that: a string mixing the two dies with
C<Invalid bech32: mixed case> before the separator is even looked for. An
all-uppercase and an all-lowercase string are both accepted, and decode to the
same bytes -- the checksum is verified against the lowercased HRP, since
BIP-173 defines it over the lowercase form regardless of how the string is
written.
=head1 SEE ALSO
=over 4
=item * L<Crypt::Age> - Main age encryption module
=item * L<Crypt::PK::X25519> - X25519 key handling from L<CryptX>
=item * L<https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki> - Bech32 specification
=back
=head1 SUPPORT
=head2 Issues
Please report bugs and feature requests on GitHub at
L<https://github.com/Getty/p5-crypt-age/issues>.
=head1 CONTRIBUTING
Contributions are welcome! Please fork the repository and submit a pull request.
=head1 AUTHOR
Torsten Raudssus <torsten@raudssus.de>
=head1 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.
=cut
( run in 1.744 second using v1.01-cache-2.11-cpan-d01c6094234 )