Crypt-Age
view release on metacpan or search on metacpan
lib/Crypt/Age.pm view on Meta::CPAN
package Crypt::Age;
# ABSTRACT: Perl implementation of age encryption (age-encryption.org)
use Moo;
use Carp qw(croak);
use Crypt::Age::Keys;
use Crypt::Age::Primitives;
use Crypt::Age::Header;
use namespace::clean;
our $VERSION = '0.003';
sub generate_keypair {
my ($class) = @_;
return Crypt::Age::Keys->generate_keypair;
}
sub encrypt {
my ($class, %args) = @_;
my $plaintext = $args{plaintext} // croak "plaintext required";
my $recipients = $args{recipients} // croak "recipients required";
# This is perl's own test for the in-memory open below, hoisted so it can
# say what is wrong. PerlIO::scalar downgrades the string in place
# (sv_utf8_downgrade) and, when that cannot be done, warns "Strings with
# code points over 0xFF may not be mapped into in-memory file handles" and
# returns EINVAL -- so the open croaked "open on input string: Invalid
# argument", which tells a caller who passed decoded characters nothing at
# all. Running the test first replaces that with a message naming the
# cause, and the warning never happens because the open is never reached.
#
# Not utf8::is_utf8: it reports the internal representation, so a pure
# ASCII string that happens to be stored upgraded answers true while
# holding nothing above 0xFF. What decides this is the content.
#
# Not a /[^\x00-\xff]/ scan either, though both are free on an unflagged
# string (perl short-circuits: downgrade on the flag, the regex on the
# optimizer knowing that class cannot match a non-UTF-8 target -- measured
# at 2000 passes over 16 MiB in under 0.01s CPU for both). They part on a
# flagged string: over 16 MiB, downgrade costs 32ms against the regex's
# 55ms, and it *is* the scan perl is about to do, so it leaves the string
# downgraded and perl's repeat of it is then a flag test. The regex pays
# for that scan twice. Worth it either way, but this way costs least.
#
# Mutating our own copy is safe and changes nothing a caller can see:
# downgrading converts the representation, never the value, %args and the
# lexical are both copies, and on success perl's open performs this very
# conversion anyway. A failure leaves the string untouched.
#
# One bit comes back, which is all that may be reported anyway: the offset
# a scan would yield is derived from the caller's plaintext, and plaintext
# does not go into error messages.
utf8::downgrade($plaintext, 1)
or croak 'plaintext must be a byte string: it holds a code point '
.'above 0xFF, encode it before passing it in';
open my $ifh, '<:raw', \$plaintext or croak "open on input string: $!";
my $output = '';
open my $ofh, '>:raw', \$output or croak "open on output string: $!";
$class->_encrypt_fh($ifh, $ofh, $recipients);
return $output;
}
sub decrypt {
my ($class, %args) = @_;
my $ciphertext = $args{ciphertext} // croak "ciphertext required";
my $identities = $args{identities} // croak "identities required";
# Same test, same reasons as in encrypt above; the advice differs because
# age ciphertext is binary, so a wide character in it means the caller
# decoded bytes that were never text rather than forgot to encode text.
utf8::downgrade($ciphertext, 1)
or croak 'ciphertext must be a byte string: it holds a code point '
.'above 0xFF, read it with :raw rather than decoding it';
open my $ifh, '<:raw', \$ciphertext or croak "open on input string: $!";
my $output = '';
open my $ofh, '>:raw', \$output or croak "open on output string: $!";
$class->_decrypt_fh($ifh, $ofh, $identities);
return $output;
}
sub _encrypt_fh {
my ($class, $ifh, $ofh, $recipients) = @_;
binmode($ifh, ':raw') or croak "cannot binmode input filehandle: $!";
binmode($ofh, ':raw') or croak "cannot binmode output filehandle: $!";
# Same skeleton as the shape checks in Crypt::Age::Header -- <param> must
# be a <Type>, then a clause carrying the requirement and its reason, then
# the fix -- because this guard and Header::create's fire on the identical
# caller mistake and only differ in which layer catches it first. This one
# always does: create is called below with the list this already accepted,
# so its message is unreachable from here and a caller who searched for
# one wording had to find the other.
#
# The reason is this layer's, not create's: create wraps the file key once
# per entry, while what a caller of encrypt sees is that every entry ends
# up able to decrypt. Nothing is interpolated, for the reason written out
# at create -- the mistake that puts a bare string here is the swap of
# recipient and identity, so the value is not reliably a public one.
croak 'recipients must be an ArrayRef: this method encrypts to every '
.'entry, pass [$recipient] rather than $recipient'
if ref($recipients) ne 'ARRAY';
# The emptiness case, moved onto the same skeleton in the same change that
# gave Header::create a guard of its own. Leaving it on its old wording
# would have rebuilt, for the empty list, exactly the divergence the shape
# checks above were just brought out of. The reason is this layer's again:
# what a caller of encrypt sees is not a header without stanzas but a
# result nobody can open.
croak 'recipients must not be empty: this method encrypts to every entry, '
.'so with none the result can never be decrypted, pass at least one '
.'recipient'
unless @$recipients;
# Generate random file key
my $file_key = Crypt::Age::Primitives->generate_file_key;
# Create header with wrapped file key for each recipient
print {$ofh} Crypt::Age::Header->create($file_key, $recipients)->to_string;
# Generate payload nonce and derive payload key
my $nonce = Crypt::Age::Primitives->generate_payload_nonce;
print {$ofh} $nonce;
my $payload_key = Crypt::Age::Primitives->derive_payload_key($file_key, $nonce);
return Crypt::Age::Primitives->encrypt_payload_fh($payload_key, $ifh, $ofh);
}
lib/Crypt/Age.pm view on Meta::CPAN
and so can leave an authenticated-but-incomplete prefix behind.
=head2 encrypt_file
Crypt::Age->encrypt_file(
input => 'plaintext.txt',
output => 'encrypted.age',
recipients => \@public_keys,
);
Encrypts a file for one or more recipients.
Parameters:
=over 4
=item * C<input> - Path to input file (required)
=item * C<output> - Path to output file (required)
=item * C<recipients> - ArrayRef of Bech32-encoded public keys (required)
=back
The output file will be in age format and can be decrypted with the C<age> or
C<rage> command-line tools.
Returns C<1> on success. Dies if a required argument is missing, if
C<recipients> is not a non-empty ArrayRef -- L</encrypt> quotes the two
messages, one for the shape and one for the empty list -- if a recipient
string is not a valid Bech32 C<age1...> public key, if C<binmode> fails on
either handle, or on file I/O errors.
Reads and writes the file in 64 KiB chunks, so memory use does not grow with
the size of the file.
=head2 encrypt_filehandle
Crypt::Age->encrypt_filehandle(
input => \*STDIN,
output => \*STDOUT,
recipients => \@public_keys,
);
Encrypts for one or more recipients, based on filehandles for both input and
output.
Parameters:
=over 4
=item * C<input> - Input filehandle (required)
=item * C<output> - Output filehandle (required)
=item * C<recipients> - ArrayRef of Bech32-encoded public keys (required)
=back
Both filehandles will be forced to be C<:raw> using C<binmode>. That removes
every layer the caller had set, C<:encoding> included, so what is encrypted is
the bytes in C<input> and never characters decoded from them. This method
therefore needs no byte-string check of its own, unlike L</encrypt>: a handle
delivers octets by the time it is read from here.
The output stream will be in age format and can be decrypted with the C<age> or
C<rage> command-line tools.
Returns C<1> on success. Dies if a required argument is missing, if
C<recipients> is not a non-empty ArrayRef -- L</encrypt> quotes the two
messages, one for the shape and one for the empty list -- if a recipient string
is not a valid Bech32 C<age1...> public key, or if C<binmode> fails on either
handle. Unlike L</encrypt_file>, this method never opens or closes a file
itself -- C<input> and C<output> are handles the caller already has open -- so
it cannot die with a "file not found" or "permission denied" error; that is the
caller's concern before the handle is passed in. Streams in 64 KiB chunks, so
memory use does not grow with the amount of data written.
=head2 decrypt_file
Crypt::Age->decrypt_file(
input => 'encrypted.age',
output => 'plaintext.txt',
identities => \@secret_keys,
);
Decrypts an age-encrypted file using one or more identities.
Parameters:
=over 4
=item * C<input> - Path to encrypted input file (required)
=item * C<output> - Path to decrypted output file (required)
=item * C<identities> - ArrayRef of Bech32-encoded secret keys (required)
=back
Returns C<1> on success. Dies if a required argument is missing, if
C<identities> is not a non-empty ArrayRef -- L</decrypt> quotes the two
messages, one for the shape and one for the empty list -- if the header is
invalid, if no identity matches any stanza, if the MAC verification fails,
if payload authentication fails, if C<binmode> fails on either handle, or on
file I/O errors.
Reads the input and writes the output in 64 KiB chunks, so memory use does not
grow with the size of the file. B<This means a failure does not undo what was
already written>: every chunk that authenticated before the error is already
in C<output> on disk once this method dies. Each such chunk is individually
authentic, but the file as a whole is not -- that is exactly what the error
reports. Treat a partial C<output> as undecrypted and discard it; do not rely
on the bytes that made it out. See L<Crypt::Age::Primitives/decrypt_payload_fh>
for the same guarantee stated at the primitive layer.
=head2 decrypt_filehandle
Crypt::Age->decrypt_filehandle(
input => \*STDIN,
output => \*STDOUT,
identities => \@secret_keys,
( run in 3.856 seconds using v1.01-cache-2.11-cpan-d01c6094234 )