Search code examples
perlencryptionaespbkdf2

AES Encryption between two languages not producing output


Trying to send data though AES encryption from Javascript to Perl. However, I am not getting any output when decoding on the Perl side.

EDIT:

  1. Desired behavior - To have the Perl code unencrypt messages encrypted by the listed Javascript.
  2. Specific problem or error - Perl code does not decrypt javascript messages due to unknown reasons.
  3. Shortest code necessary to reproduce the problem - Detailed as follows:

JAVASCRIPT ENCRYPT

var keySize = 256;
var ivSize = 128;
var iterations = 100;

function testcrypt(){
  var pass = 'testpass';
  var msg = 'testmsg';
  var salt = CryptoJS.lib.WordArray.random(128/8);
  var key = CryptoJS.PBKDF2(pass, salt, {
      keySize: keySize/32,
      iterations: iterations
    });
  var iv = CryptoJS.lib.WordArray.random(128/8);
  var encrypted = CryptoJS.AES.encrypt(JSON.stringify(msg), key, {
    iv: iv,
    padding: CryptoJS.pad.Pkcs7,
    mode: CryptoJS.mode.CBC
  });
  
  // salt, iv will be hex 32 in length 
  // append them to the ciphertext for use  in decryption
  var packet = salt.toString()+'-'+ iv.toString()+'-' + encrypted.toString();
  alert(packet);
}

OUTPUT IS -> 71b851532027b022c50f1a389254fca4-d8a9d7c4fa30f7d9427a5762e2c5794b-8S/JOXgDYuRloLKBiH5fYg==

PERL DECRYPT

use warnings;
use strict;

use Crypt::CBC;
use Crypt::PBKDF2;
use MIME::Base64;
print "\nGot: ".decrypt('ebdfcbd226b7dbc73e77a29d31fab723-3daf178979cb03087f5258907e73e547-CyG6R0EU9s0c8dBCAZk6Jg==', 'testpass');
print "\nGot: ".decrypt('71b851532027b022c50f1a389254fca4-d8a9d7c4fa30f7d9427a5762e2c5794b-8S/JOXgDYuRloLKBiH5fYg==', 'testpass');

sub decrypt{
        my $encrypted = shift;
        my $key = shift;

        my($salt,$iv,$packet)=split '-',$encrypted,3;
        $packet = decode_base64($packet);
        my %options=(
                key_len => length $key,
                iv_len  => length $iv,
                iterations => 100,
               # hash_class => 'HMACSHA2',
                hash_class => 'HMACSHA1',  # To match CryptoJS
        );
        $options{output_len}  =  $options{key_len} + $options{iv_len};

        my $pbkdf2 = Crypt::PBKDF2->new(%options);

        my $AES = Crypt::CBC->new(
                #-key    => $pbkdf2->PBKDF2($key,$salt),# Was Reversed
                -key    => $pbkdf2->PBKDF2($salt,$key),
                -cipher => 'Cipher::AES',
                -pbkdf  => 'pbkdf2',
                -header => 'none',
        );

       return $AES->decrypt($packet);

}



Solution

  • Some of the issues have already been addressed in the comments. In the meantime, some changes have been made to the code. The following bug list refers to the originally posted code:

    1. The CryptoJS code hex encodes salt and IV, the Perl code lacks hex decoding.
    2. The CryptoJS code uses HMAC/SHA1, the Perl code HMAC/SHA256
    3. In the Perl code, the arguments are swapped when calling PBKDF2().
    4. In the CryptoJS code, the key is derived from the password via PBKDF2. In the Perl code, the key derivation is done twice (explicitly with PBKDF2(), implicitly with -pbkdf => 'pbkdf2').
    5. In the CryptoJS code, the IV is randomly generated. In the Perl code the IV is derived together with the key via PBKDF2.

    These bugs can be fixed as follows:

    use warnings;
    use strict;
    
    use Crypt::CBC;
    use Crypt::PBKDF2;
    use MIME::Base64;
    
    print "\nGot: ".decrypt('ebdfcbd226b7dbc73e77a29d31fab723-3daf178979cb03087f5258907e73e547-CyG6R0EU9s0c8dBCAZk6Jg==', 'testpass');
    print "\nGot: ".decrypt('71b851532027b022c50f1a389254fca4-d8a9d7c4fa30f7d9427a5762e2c5794b-8S/JOXgDYuRloLKBiH5fYg==', 'testpass');
    
    sub decrypt{
        my $encrypted = shift;
        my $pwd = shift;
    
        # get IV, salt and ciphertext
        my($salt,$iv,$packet)=split '-',$encrypted,3;
        $iv = pack("H*", $iv);                      # Fix regarding 1.
        $salt = pack("H*", $salt);                  # Fix regarding 1. 
        $packet = decode_base64($packet);
        
        # derive key
        my %options=(
                output_len => 32,           
                iterations => 100,
                hash_class => 'HMACSHA1',           # Fix regarding 2.
        );
        my $pbkdf2 = Crypt::PBKDF2->new(%options);
        my $key = $pbkdf2->PBKDF2($salt, $pwd);     # Fix regarding 3.
     
        # decrypt
        my $AES = Crypt::CBC->new(
                 -pbkdf  => 'none',                 # Fix regarding 4.
                 -key    => $key,   
                 -iv     => $iv,                    # Fix regarding 5.
        );
    
        return $AES->decrypt($packet);
    } 
    

    When the script is executed, the ciphertexts are now decrypted correctly (versions used: Crypt::CBC: 3.04, Crypt::PBKDF2: 0.161520).


    The internal key derivation cannot be applied because the salt used is 16 bytes long and the internal key derivation supports only 8 byte salts (error message for other salt lengths: argument to -salt must be exactly 8 bytes long...).
    In case of an 8 bytes salt the internal key derivation can be used as follows:

    use warnings;
    use strict;
    
    use Crypt::CBC;
    use Crypt::PBKDF2;
    use MIME::Base64;
    
    print "\nGot: ".decrypt('0dad5e7e0c105e82-665304727191506bd818cd48ac9917a0-DXF+Bh/K4ESEw1tgtp6p2Q==', 'testpass');
    
    sub decrypt{
        my $encrypted = shift;
        my $pwd = shift;
    
        # get IV, salt and ciphertext
        my($salt,$iv,$packet)=split '-',$encrypted,3;
        $iv = pack("H*", $iv);                      
        $salt = pack("H*", $salt);                  
        $packet = decode_base64($packet);
    
        # derive key and decrypt
        my $AES = Crypt::CBC->new(
                 -pbkdf   => 'pbkdf2',                 
                 -key     => $pwd,   
                 -salt    => $salt,
                 -hasher  => 'HMACSHA1',
                 -iter    => 100,
                 -header  => 'none',
                 -iv      => $iv,                    
        );
    
        return $AES->decrypt($packet);
    }