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 )