Crypt-Age
view release on metacpan or search on metacpan
lib/Crypt/Age/Primitives.pm view on Meta::CPAN
return hmac('SHA256', $mac_key, $header_bytes);
}
sub encrypt_payload {
my ($class, $payload_key, $plaintext) = @_;
# Perl's own test for the in-memory open below, hoisted so the failure
# names its cause instead of arriving as EINVAL from the open. Why
# utf8::downgrade rather than utf8::is_utf8 or a /[^\x00-\xff]/ scan, and
# why mutating this copy is safe, is written out over the same check in
# Crypt::Age::encrypt.
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 "Cannot open input string: $!";
my $output = '';
open my $ofh, '>:raw', \$output or croak "Cannot open output string: $!";
$class->encrypt_payload_fh($payload_key, $ifh, $ofh);
close $ofh or croak "Cannot close output string: $!";
close $ifh or croak "Cannot close input string: $!";
return $output;
}
sub encrypt_payload_fh {
my ($class, $payload_key, $ifh, $ofh) = @_;
my $counter = 0;
my $is_final = 0;
while (! $is_final) {
my $chunk = $class->paranoid_read($ifh, CHUNK_SIZE);
$is_final = eof($ifh);
my $nonce = $class->_make_nonce($counter, $is_final);
my $ae = Crypt::AuthEnc::ChaCha20Poly1305->new($payload_key, $nonce);
my $ciphertext = $ae->encrypt_add($chunk);
my $tag = $ae->encrypt_done;
print {$ofh} $ciphertext, $tag;
$counter++;
}
return;
}
sub decrypt_payload {
my ($class, $payload_key, $ciphertext) = @_;
# Same test, same reasons as in encrypt_payload above; the advice differs
# because an age payload is binary, so a wide character in it means the
# caller decoded bytes that were never 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 "Cannot open input string: $!";
my $output = '';
open my $ofh, '>:raw', \$output or croak "Cannot open output string: $!";
$class->decrypt_payload_fh($payload_key, $ifh, $ofh);
close $ofh or croak "Cannot close output string: $!";
close $ifh or croak "Cannot close input string: $!";
return $output;
}
sub decrypt_payload_fh {
my ($class, $payload_key, $ifh, $ofh) = @_;
my $max_encrypted_chunk = CHUNK_SIZE + TAG_SIZE;
my $counter = 0;
while (1) {
# Each encrypted chunk is plaintext + 16 byte tag
my $ct = $class->paranoid_read($ifh, $max_encrypted_chunk);
# paranoid_read only comes back short at end of file, so a short read
# is how this loop learns the file ends here. A *full* read says
# nothing either way: more chunks may follow, or this may be the final
# chunk sitting flush against the end of the file.
my $short_read = length($ct) < $max_encrypted_chunk;
if ($short_read) {
# Spec: "Streaming decryption MUST signal an error if the end of
# file is reached without successfully decrypting a final chunk."
croak 'Payload authentication failed at chunk '.$counter
. ': end of file reached without a final chunk'
if length($ct) == 0;
croak 'Payload authentication failed at chunk '.$counter
. ': chunk is shorter than its authentication tag'
if length($ct) < TAG_SIZE;
# Spec: "The final chunk MAY be shorter than 64 KiB but MUST NOT
# be empty unless the whole payload is empty."
croak 'Payload authentication failed at chunk '.$counter
. ': final chunk is empty and is not the only chunk'
if length($ct) == TAG_SIZE && $counter > 0;
}
my $tag = substr($ct, -TAG_SIZE, TAG_SIZE, '');
# The final-chunk flag lives in the nonce, so the only way to learn
# which nonce the writer used is to authenticate under it -- the
# file's length cannot answer it. A short chunk can only be the final
# one. A full chunk is tried as non-final first and, failing that, as
# a full-length final chunk. No ciphertext can authenticate under both
# nonces, so the second attempt cannot accept a chunk the writer did
# not mark that way: it is a second verification, not a second chance.
my $is_final = $short_read ? 1 : 0;
my $plaintext = $class->_open_chunk($payload_key, $counter, $is_final, $ct, $tag);
( run in 0.963 second using v1.01-cache-2.11-cpan-d01c6094234 )