Search code examples
javascriptperlencryptionaes

AES encryption in Perl with custom Key and IV


I have the following AES encryption code in JavaScript, but I can't seem to get comparable result in Perl.

'use strict';
const CryptoJS = require('crypto-js');

const message = 's3cret';
const aesPassword = 'MyPassword';
const salt = CryptoJS.lib.WordArray.random(16);
const iv = CryptoJS.lib.WordArray.random(16);
const key = CryptoJS.PBKDF2(aesPassword, salt, {keySize: 128/32, iterations: 1000});
const ciphertext = CryptoJS.AES.encrypt(message, key, {iv: iv}).ciphertext.toString(CryptoJS.enc.Base64);
const encryptedMessage = Buffer.from(iv + '::' + salt + '::' + ciphertext).toString('base64');
console.log("encryptedMessage:", encryptedMessage);
encryptedMessage: NDNjOWNjYmE2MzBmZTFjZDYxZmMyYmRiOTAxMjFjNmY6OjVjNzg4YTQxNTg1MWU5MDlkOWM3OTUxNzE3NzE0MjA0OjpxVDFWdG9kN2loUzJ3dnR3bWxuUG93PT0=

(92 bytes before base64 encoding.)

Perl:

use strict;
use warnings;
use Crypt::CBC;
use Crypt::PBKDF2;
use MIME::Base64;

my $message = 's3cret';
my $aesPassword = 'MyPassword';
my $salt = Crypt::CBC->random_bytes(16);
my $iv = Crypt::CBC->random_bytes(16);
my $pbkdf2 = Crypt::PBKDF2->new(output_len=>128/32, iterations => 1000);
my $key = $pbkdf2->PBKDF2($aesPassword, $salt);
my $cipher = Crypt::CBC->new(-cipher=>'Cipher::AES', -pbkdf=>'pbkdf2', -key=>$key, -iv=>$iv, -header=>'none');
my $ciphertext = encode_base64( $cipher->encrypt($message), "" );
my $encryptedMessage = encode_base64( join('::', $iv, $salt, $ciphertext), "" );
print "encryptedMessage: $encryptedMessage";
encryptedMessage: tO/jlJ3NkuJG0ZA58EDR0Do6wUf4xHb/MwWm3iyT+ejYWDo6SllMSHB0S0ZuMDVYWDRzS3ZCWlRiZz09Cg==

(61 bytes before base64 encoding.)


Solution

    • Use encode_base64( ..., "" ) to avoid the addition of line breaks and a trailing LF. (Already fixed in the OP.)
    • You have the order of the arguments of the call to ->PBKDF2 backwards.
    • You ask Crypt::CBC to use PBKDF2 despite already having already applied it. -pbkdf => 'none' should be used.
    • You generate a key of the wrong length. 128/32 should be 128/8.
    • You need to tell Crypt::CBC when using a key length that isn't the largest possible. -keysize => length( $key ) should be used.
    • You are storing the hex of the IV and salt in the JS version, but the raw IV and salt in the Perl version.

    [Thanks to @Topaco for identifying most of these.]

    Fixed version:

    use strict;
    use warnings;
    use feature qw( say );
    
    use Crypt::CBC    qw( );
    use Crypt::PBKDF2 qw( );
    use MIME::Base64  qw( encode_base64 );
    
    use constant KEY_SIZE => 128 / 8;
    
    my $plaintext = 's3cret';
    my $password  = 'MyPassword';
    
    #my $iv   = Crypt::CBC->random_bytes( 16 );
    #my $salt = Crypt::CBC->random_bytes( 16 );
    my $iv   = pack( "H*", "43c9ccba630fe1cd61fc2bdb90121c6f" );  # For testing.
    my $salt = pack( "H*", "5c788a415851e909d9c7951717714204" );  # For testing.
    
    my $pbkdf2 = Crypt::PBKDF2->new(
       output_len => KEY_SIZE,
       iterations => 1000,
    );
    
    my $key = $pbkdf2->PBKDF2( $salt, $password );
    
    my $cipher = Crypt::CBC->new(
       -cipher  => 'Cipher::AES',
       -header  => 'none',
       -pbkdf   => 'none',
       -keysize => KEY_SIZE,
       -key     => $key,
       -iv      => $iv,
    );
    
    my $ciphertext = $cipher->encrypt( $plaintext );
    
    my $ciphertext_base64 = encode_base64( $ciphertext, "" );
    my $iv_hex   = unpack( "H*", $iv );
    my $salt_hex = unpack( "H*", $salt );
    
    my $msg = join( '::', $iv_hex, $salt_hex, $ciphertext_base64 );
    my $msg_base64 = encode_base64( $msg, "" );
    
    say $msg;
    say length( $msg );
    say $msg_base64;
    
    43c9ccba630fe1cd61fc2bdb90121c6f::5c788a415851e909d9c7951717714204::qT1Vtod7ihS2wvtwmlnPow==
    92
    NDNjOWNjYmE2MzBmZTFjZDYxZmMyYmRiOTAxMjFjNmY6OjVjNzg4YTQxNTg1MWU5MDlkOWM3OTUxNzE3NzE0MjA0OjpxVDFWdG9kN2loUzJ3dnR3bWxuUG93PT0=