Crypt-Age
view release on metacpan or search on metacpan
t/02-encrypt-decrypt.t view on Meta::CPAN
Crypt::Age->encrypt(plaintext => $plaintext, recipients => [uc($public)]);
};
is($@, '', 'an all-uppercase AGE1... recipient encrypts without dying');
my $decrypted = Crypt::Age->decrypt(ciphertext => $encrypted, identities => [$secret]);
is($decrypted, $plaintext,
'ciphertext for an uppercase recipient decrypts with the matching identity');
# (2) a mixed-case recipient is rejected, but specifically by the
# bech32 mixed-case guard, not by the "Unsupported recipient format"
# branch. That distinction is the regression: it shows the string
# passed the /^age1/i prefix test and was only then rejected inside
# decode_public_key -- a test that only checked "dies somehow" would
# not catch the prefix test itself going missing.
my $mixed = 'Age1' . substr($public, 4);
ok($mixed =~ /^age1/i && $mixed ne lc($mixed) && $mixed ne uc($mixed),
'fixture: mixed-case recipient matches the prefix case-insensitively and is genuinely mixed case')
or die 'fixture assumption broken -- generated public key data has no lowercase letter to mix';
eval { Crypt::Age->encrypt(plaintext => $plaintext, recipients => [$mixed]) };
like($@, qr/Invalid bech32: mixed case/,
'a mixed-case recipient dies with the bech32 mixed-case guard');
unlike($@, qr/Unsupported recipient format/,
'and not with the encrypt-side "Unsupported recipient format" rejection');
# (3) an all-lowercase identity still decrypts -- the /i on the
# identity side predates #19 and must stay untouched by it.
my $encrypted2 = Crypt::Age->encrypt(plaintext => $plaintext, recipients => [$public]);
my $decrypted2 = Crypt::Age->decrypt(ciphertext => $encrypted2, identities => [lc($secret)]);
is($decrypted2, $plaintext, 'an all-lowercase identity still decrypts');
}
# Ticket #22 regression, the version-line half: parse_from_fh's version
# check used to interpolate the first line straight into the croak
# ("Invalid age version: $version_line"). That is unbounded when the input
# has no newline at all: parse_from_fh reads under `local $/ = "\n"`, so
# with no newline to stop at, <$fh> reads to EOF and the *entire* input
# becomes "the first line". Feed decrypt a plaintext string with no
# trailing newline that cannot possibly be a real age header, and confirm
# none of it survives into the error.
{
my ($public, $secret) = Crypt::Age->generate_keypair;
my $not_an_age_file = 'this is definitely not an age header and has no newline at all';
ok(index($not_an_age_file, "\n") == -1, 'fixture: input contains no newline');
my $err = do {
local $@;
eval { Crypt::Age->decrypt(ciphertext => $not_an_age_file, identities => [$secret]) };
$@;
};
ok($err, 'decrypting a no-newline non-age input dies');
like($err,
qr/^Invalid age version: expected the literal age-encryption\.org\/v1 version line/,
'the message names the expected literal version line');
ok(index($err, $not_an_age_file) == -1,
'none of the input content appears in the error');
}
# Ticket #26, carrying the #24 regression: the string API takes bytes. Perl
# refuses to map a string holding a code point above 0xFF into an in-memory
# handle, so handing encrypt/decrypt a character (decoded) string used to fail
# at the *input* open. #24 made that open croak instead of die, which put the
# blame on the caller -- those location assertions are kept below, they are
# the point of the block. #26 replaces what the caller is told: EINVAL from an
# in-memory open ("open on input string: Invalid argument") names no cause and
# suggests no fix, so encrypt/decrypt now run perl's own downgrade test first
# and say what is wrong. The open is no longer reached on this path, so perl's
# "code points over 0xFF" warning is gone too -- asserted, not suppressed.
#
# The output-side opens (on a lexical the method owns, written through :raw)
# have no caller-reachable failure and are deliberately not covered here.
{
my ($public, $secret) = Crypt::Age->generate_keypair;
my $wide = "\x{100} not bytes";
my ($enc_err, $enc_line, @enc_warn);
{
local $SIG{__WARN__} = sub { push @enc_warn, $_[0] };
local $@;
$enc_line = __LINE__ + 1;
eval { Crypt::Age->encrypt(plaintext => $wide, recipients => [$public]) };
$enc_err = $@;
}
ok($enc_err, 'encrypt with a wide-character plaintext dies');
like($enc_err,
qr/^plaintext must be a byte string: it holds a code point above 0xFF, encode it before passing it in\b/,
'the message names the cause and the fix, not EINVAL');
unlike($enc_err, qr/Invalid argument/,
'encrypt no longer passes perl\'s EINVAL through');
is_deeply(\@enc_warn, [],
'the check runs before the open, so perl emits no >0xFF warning');
unlike($enc_err, qr{Crypt/Age\.pm},
'encrypt croaks: Crypt/Age.pm is not blamed as the origin');
my $enc_where = quotemeta(__FILE__).' line '.$enc_line;
like($enc_err, qr/$enc_where/,
'encrypt reports the caller position in this test file');
ok(index($enc_err, 'not bytes') == -1,
'no part of the plaintext appears in the error');
my ($dec_err, $dec_line, @dec_warn);
{
local $SIG{__WARN__} = sub { push @dec_warn, $_[0] };
local $@;
$dec_line = __LINE__ + 1;
eval { Crypt::Age->decrypt(ciphertext => $wide, identities => [$secret]) };
$dec_err = $@;
}
ok($dec_err, 'decrypt with a wide-character ciphertext dies');
like($dec_err,
qr/^ciphertext must be a byte string: it holds a code point above 0xFF, read it with :raw rather than decoding it\b/,
'the message names the cause and the fix, not EINVAL');
unlike($dec_err, qr/Invalid argument/,
'decrypt no longer passes perl\'s EINVAL through');
is_deeply(\@dec_warn, [],
'the check runs before the open, so perl emits no >0xFF warning');
unlike($dec_err, qr{Crypt/Age\.pm},
'decrypt croaks: Crypt/Age.pm is not blamed as the origin');
my $dec_where = quotemeta(__FILE__).' line '.$dec_line;
like($dec_err, qr/$dec_where/,
( run in 2.412 seconds using v1.01-cache-2.11-cpan-d01c6094234 )