From Code to Community: Sponsoring The Perl and Raku Conference 2025 Learn more

use strict;
use lib qw{ . lib};
use English qw{ -no_match_vars };
use UnitTestSetup qw(:all);
BEGIN {
my $has_crypt_cbc = eval {
require Crypt::CBC;
};
if ( !$has_crypt_cbc ) {
plan skip_all => 'Crypt::CBC unavilable';
}
else {
plan tests => 14;
}
{
no strict 'refs'; ## no critic
*{'HTTP::Request::new'} = sub { bless {}, 'HTTP::Request'; };
*{'HTTP::Request::request'} = sub { HTTP::Response->new; };
*{'HTTP::Response::new'} = sub { bless {}, 'HTTP::Response'; };
*{'HTTP::Response::is_success'} = sub { 1; };
*{'LWP::UserAgent::new'} = sub { bless {}, 'LWP::UserAgent'; };
*{'LWP::UserAgent::request'} = sub { HTTP::Response->new; };
## no critic
}
mark_as_loaded(HTTP::Request);
mark_as_loaded(HTTP::Response);
mark_as_loaded(LWP::UserAgent);
use_ok('Amazon::Credentials');
}
########################################################################
sub my_encrypt {
########################################################################
my ( $str, $passkey ) = @_;
return if !$str;
my $sum = 0;
foreach ( split //xsm, $passkey ) {
$sum += ord $_;
}
my @encrypted_str;
foreach ( split //xsm, $str ) {
push @encrypted_str, $sum + ord $_;
}
return \@encrypted_str;
}
########################################################################
sub my_decrypt {
########################################################################
my ( $str, $passkey ) = @_;
return if !$str;
my @encrypted_str = @{$str};
my $sum = 0;
foreach ( split //xsm, $passkey ) {
$sum += ord $_;
}
$str = q{};
foreach my $c (@encrypted_str) {
$c -= $sum;
$str .= chr $c;
}
return $str;
}
########################################################################
sub check_credentials {
########################################################################
my ( $credentials, $unencrypted_creds, $test ) = @_;
$test = $test // q{};
my $retval = 0;
if ( $credentials->get_cache ) {
foreach my $e (qw{ access_key_id secret_access_key }) {
my $encrypted_value = $credentials->can( 'get__' . $e )->($credentials);
my $unencrypted_value
= $credentials->can( 'get_aws_' . $e )->($credentials);
$retval
+= !ok( $encrypted_value && $encrypted_value ne $unencrypted_value,
$test . ' - ' . $e . ' encrypted ok' );
$retval += !ok(
$unencrypted_value eq $unencrypted_creds->{$e},
$test . ' - ' . $e . ' decrypted ok'
);
}
}
else {
foreach my $e (qw{access_key_id secret_access_key }) {
$retval
+= !ok( !defined $credentials->can( 'get__' . $e )->($credentials),
$test . ' - ' . $e . ' not cached' );
}
}
return !$retval;
}
########################################################################
sub check_cipher {
########################################################################
my ( $cipher_name, $test ) = @_;
my $credentials = Amazon::Credentials->new(
profile => 'foo',
cipher => $cipher_name,
no_passkey_warning => 1,
);
ok( $credentials->get_encryption, 'encryption enabled' );
$cipher_name = $cipher_name || $credentials->get_cipher;
is( $credentials->get_cipher, $cipher_name, $test || $cipher_name )
or diag( $credentials->get_cipher );
my $passkey = $credentials->get_passkey;
my $cipher = Crypt::CBC->new(
'-pass' => $passkey,
'-key' => $passkey,
'-nodeprecate' => 1,
'-cipher' => $cipher_name,
);
my $access_key_id = decode_base64( $credentials->get__access_key_id );
my $unencrypted_access_key_id = $credentials->get_aws_access_key_id;
my $encrypted_access_key_id = $cipher->encrypt($unencrypted_access_key_id);
isnt( $encrypted_access_key_id, $access_key_id,
'encrypted strings different (salt)' )
or diag( Dumper [ $passkey, $encrypted_access_key_id, $access_key_id ] );
# decrypt your encrypted string with my cipher
is(
$cipher->decrypt($access_key_id),
$credentials->get_aws_access_key_id,
'encrypted with ' . $cipher_name
)
or diag( Dumper [ $passkey, $encrypted_access_key_id, $access_key_id ] );
return;
}
# +------------------ +
# | TESTS START HERE |
# +------------------ +
init_test;
Amazon::Credentials->import('create_passkey');
my %unencrypted_creds = (
access_key_id => 'foo-aws-access-key-id',
secret_access_key => 'foo-aws-secret-access-key',
);
# !! this test must be run first !!
########################################################################
subtest 'obfuscation without Crypt::CBC' => sub {
########################################################################
{
# use Devel::Hide qw{ -lexically -quiet Crypt::CBC };
eval "use Test::Without::Module qw{ Crypt::CBC Crypt::Cipher::AES };"; ## no critic
my $credentials = Amazon::Credentials->new(
profile => 'foo',
encryption => 1,
no_passkey_warning => 1,
);
ok( !$credentials->get_encryption,
'encryption disabled (no Crypt::CBC)' );
ok(
decode_base64( $credentials->get__access_key_id ) eq
$unencrypted_creds{access_key_id},
'base64 encoded obfuscation'
);
ok(
decode_base64( $credentials->get__secret_access_key ) eq
$unencrypted_creds{secret_access_key},
'base64 encoded obfuscation'
);
check_credentials( $credentials, \%unencrypted_creds, 'obfuscation' )
or diag( Dumper [$credentials] );
}
eval q{ no Test::Without::Module qw{ Crypt::CBC Crypt::Cipher::AES }; }; ## no critic
};
########################################################################
subtest 'decrypt' => sub {
########################################################################
my $credentials = Amazon::Credentials->new(
profile => 'foo',
no_passkey_warning => 1,
);
ok( defined $credentials->get_passkey, 'passkey created' );
ok( $credentials->get_encryption, 'default is encryption enabled' );
check_credentials( $credentials, \%unencrypted_creds, 'decrypt' )
or diag( Dumper [$credentials] );
};
########################################################################
subtest 'rotate credentials' => sub {
########################################################################
my $credentials = Amazon::Credentials->new(
profile => 'foo',
encryption => 1,
no_passkey_warning => 1,
);
my $passkey = $credentials->get_passkey;
my $new_passkey = $credentials->rotate_credentials;
ok( $new_passkey ne $passkey, 'passkey changed' )
or diag( Dumper [ $passkey, $new_passkey ] );
check_credentials( $credentials, \%unencrypted_creds, 'rotate' )
or diag(
Dumper [
$credentials->get_passkey(), 'new:', $new_passkey, $credentials
]
);
};
########################################################################
subtest 'rotate credentials with custom passkey' => sub {
########################################################################
our $PASSKEY = create_passkey();
sub get_passkey {
my ($regenerate) = @_;
return $regenerate ? create_passkey() : $PASSKEY;
}
my $credentials = Amazon::Credentials->new(
passkey => \&get_passkey,
profile => 'foo',
cache => 1,
no_passkey_warning => 1,
);
isa_ok( $credentials, 'Amazon::Credentials' );
my $old_passkey = $PASSKEY;
$PASSKEY = $credentials->rotate_credentials( get_passkey(1) );
ok( $old_passkey && $PASSKEY, 'passkeys are not null' );
ok( $old_passkey ne $PASSKEY, 'passkey has changed' );
check_credentials( $credentials, \%unencrypted_creds, 'rotate (cache on)' )
or diag( Dumper [$credentials] );
$credentials = Amazon::Credentials->new(
cache => 0,
passkey => \&get_passkey,
profile => 'foo',
no_passkey_warning => 1,
);
$old_passkey = $PASSKEY = get_passkey(1);
$PASSKEY = $credentials->rotate_credentials( get_passkey(1) );
ok( $old_passkey && $PASSKEY, 'passkeys are not null (cacheing off)' );
ok( $old_passkey ne $PASSKEY, 'passkey has changed (cacheing off' );
check_credentials( $credentials, \%unencrypted_creds, 'rotate (cache off)' )
or diag( Dumper [$credentials] );
sub get_passkey_v2 {
return 'abra cadabra ala kazam!';
}
$credentials->set_cache(1);
$credentials->set_passkey( \&get_passkey_v2 );
$credentials->reset_credentials(1);
check_credentials( $credentials, \%unencrypted_creds, 'set new passkey' )
or diag( Dumper [$credentials] );
$credentials->set_insecure(1);
$credentials->set_cache(1);
$credentials->set_passkey( \&get_passkey );
$credentials->reset_credentials(1);
check_credentials( $credentials, \%unencrypted_creds,
'set new passkey (cached)' )
or diag( Dumper [$credentials] );
};
########################################################################
subtest 'custom encryption/decryption' => sub {
########################################################################
my $credentials = Amazon::Credentials->new(
profile => 'foo',
encrypt => \&my_encrypt,
decrypt => \&my_decrypt,
passkey => sub { return 'my passkey' },
no_passkey_warning => 1,
);
check_credentials( $credentials, \%unencrypted_creds, 'custom encryption' )
or diag( Dumper [$credentials] );
};
########################################################################
subtest 'custom encryption/decryption setting' => sub {
########################################################################
# set only decrypt or encrypt
foreach my $sub (qw{ encrypt decrypt }) {
my $credentials = eval {
return Amazon::Credentials->new(
profile => 'foo',
$sub => sub { },
passkey => sub { return 'my passkey' },
no_passkey_warning => 1,
);
};
ok( $EVAL_ERROR && $EVAL_ERROR =~ /must\sbe\sa\scode\sref/xsm,
"set just $sub" )
or diag($EVAL_ERROR);
}
};
########################################################################
subtest 'cache credentials' => sub {
########################################################################
my $credentials = eval {
return Amazon::Credentials->new(
profile => 'foo',
cache => 1,
no_passkey_warning => 1,
);
};
check_credentials( $credentials, \%unencrypted_creds, 'cache on' )
or diag( Dumper [$credentials] );
ok( defined $credentials->get__secret_access_key,
'secret access key retained' );
ok( defined $credentials->get__access_key_id, 'access key id retained' );
};
########################################################################
subtest 'do not cache credentials' => sub {
########################################################################
my $credentials = eval {
return Amazon::Credentials->new(
profile => 'foo',
cache => 0,
no_passkey_warning => 1,
);
};
check_credentials( $credentials, \%unencrypted_creds, 'cache off' )
or diag( Dumper [$credentials] );
ok( !defined $credentials->get__secret_access_key,
'secret access key removed' );
ok( !defined $credentials->get__access_key_id, 'access key id removed' );
};
########################################################################
subtest 'get passkey from sub' => sub {
########################################################################
my $passkey = create_passkey();
my $credentials = eval {
return Amazon::Credentials->new(
profile => 'foo',
cache => 1,
encryption => 1,
passkey => sub {
return $passkey;
},
no_passkey_warning => 1,
);
};
ok( $credentials->get_encryption, 'encryption enabled' )
or diag( Dumper [$credentials] );
check_credentials( $credentials, \%unencrypted_creds, )
or diag( Dumper [ $passkey, $credentials->get_passkey, $credentials ] );
};
########################################################################
subtest 'rotate credentials w/new passkey' => sub {
########################################################################
my $passkey = 'abra cadabra ala kazam!';
my $credentials = eval {
return Amazon::Credentials->new(
profile => 'foo',
cache => 1,
encryption => 1,
passkey => $passkey,
no_passkey_warning => 1,
);
};
# encrypted values
my ( $secret_access_key, $access_key_id ) = (
$credentials->get__secret_access_key,
$credentials->get__access_key_id
);
my $new_passkey = $credentials->rotate_credentials( create_passkey() );
ok( $new_passkey ne $passkey, 'passkey rotated' )
or diag(
Dumper [ $new_passkey, $secret_access_key, $access_key_id, $credentials ]
);
check_credentials( $credentials, \%unencrypted_creds )
or diag(
Dumper [ $new_passkey, $secret_access_key, $access_key_id, $credentials ]
);
ok( $secret_access_key ne $credentials->get__secret_access_key,
'encrypted secret different' )
or diag(
Dumper [ $new_passkey, $secret_access_key, $access_key_id, $credentials ]
);
ok( $access_key_id ne $credentials->get__access_key_id,
'encrypted access_key_id different' )
or diag(
Dumper [ $new_passkey, $secret_access_key, $access_key_id, $credentials ]
);
$new_passkey = $credentials->rotate_credentials;
ok( $new_passkey ne $passkey, 'passkey rotated' )
or diag(
Dumper [ $new_passkey, $secret_access_key, $access_key_id, $credentials ]
);
};
########################################################################
subtest 'token encryption' => sub {
########################################################################
my $credentials = Amazon::Credentials->new(
aws_access_key_id => 'foo',
aws_secret_access_key => 'bar',
token => 'biz',
encryption => 1,
cache => 1,
no_passkey_warning => 1,
);
ok( $credentials->get_encryption, 'encryption enabled' )
or diag( Dumper [$credentials] );
ok( $credentials->get__session_token ne 'biz', 'token encrypted' )
or diag( Dumper [$credentials] );
ok( $credentials->get_token eq 'biz', 'token decrypted' )
or diag( Dumper [$credentials] );
ok( decode_base64( $credentials->get__session_token ) ne 'biz',
'encrypted, not just obfuscated' )
or diag( Dumper [$credentials] );
};
########################################################################
subtest 'use Crypt::CBC' => sub {
########################################################################
eval {
require Crypt::CBC;
};
if ($EVAL_ERROR) {
plan skip_all => $EVAL_ERROR;
}
check_cipher( q{}, 'default cipher' );
};
########################################################################
subtest 'use custom cipher' => sub {
########################################################################
my $cipher_name = $ENV{AMAZON_CREDENTIAL_TEST_CIPHER} || 'Crypt::Blowfish';
eval "require $cipher_name;"; ## no critic
if ($EVAL_ERROR) {
plan skip_all => $EVAL_ERROR;
}
check_cipher( $cipher_name, 'custom cipher ' . $cipher_name );
};
1;