Jump to content
Dave Nottage

Decryption of values encrypted by crypto-js, using OpenSSL

Recommended Posts

The goal is to be able to encrypt some data using crypto-js (in Javascript) and decrypt the value in a back end built with Delphi, using OpenSSL (since crypto-js claims to be OpenSSL compliant).

 

This is the Javascript code:

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

var IV = '583066480e215358084bc6640df95fdd';
var passphrase = 's0m3s3cr3t!';

// According to:
//   https://stackoverflow.com/a/75473014/3164070
// IV is ignored, so can be anything!

function encrypt(text: string): string {
  var key = passphrase;
  var iv = CryptoJS.enc.Hex.parse(IV);
  console.log('iv: ' + iv.toString());
  var encrypted = CryptoJS.AES.encrypt(text, key, { iv: iv });
  return encrypted.toString();
}

This is the Delphi code. Note that OpenSSL.Api_11 comes from Grijjy (as well as the SSL binaries):

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

uses
  System.NetEncoding,
  OpenSSL.Api_11;

const
  cPassphrase = 's0m3s3cr3t!';

procedure CryptoError;
var
  LError: TBytes;
  LMessage: string;
begin
  SetLength(LError, 120);
  ERR_load_crypto_strings;
  ERR_error_string_n(ERR_get_error, @LError[0], Length(LError));
  LMessage := TEncoding.UTF8.GetString(LError);
  // Display the message
end;

function DecryptAES256(const AValue: string): string;
var
  LContext: PEVP_CIPHER_CTX;
  LOutputLen, LFinalLen: Integer;
  LBytes, LSalt, LIV, LEncryptedText, LKey, LOutput: TBytes;
begin
  Result := '';
  LBytes := TNetEncoding.Base64.DecodeStringToBytes(AValue);
  // crypto-js returns an OpenSSL result, i.e. "salted__" + salt + encrypted text
  LSalt := Copy(LBytes, 8, 8);
  LEncryptedText := Copy(LBytes, 16, MaxInt);
  SetLength(LKey, EVP_MAX_KEY_LENGTH + EVP_MAX_IV_LENGTH);
  // Calc key and IV from passphrase and salt
  if PKCS5_PBKDF2_HMAC(cPassphrase, Length(cPassphrase), @LSalt[0], Length(LSalt), 1000, EVP_sha256, Length(LKey), @LKey[0]) = 1 then
  begin
    LIV := Copy(LKey, EVP_MAX_KEY_LENGTH, EVP_MAX_IV_LENGTH);
    LKey := Copy(LKey, 0, EVP_MAX_KEY_LENGTH);
    // Create a new cipher context
    LContext := EVP_CIPHER_CTX_new();
    if LContext <>  nil then
    try
      // Initialize the decryption operation with 256 bit AES
      if EVP_DecryptInit_ex(LContext, EVP_aes_256_cbc, nil, @LKey[0], @LIV[0]) = 1 then
      begin
        SetLength(LOutput, Length(LEncryptedText) + EVP_MAX_BLOCK_LENGTH);
        // Provide the message to be decrypted, and obtain the plaintext output
        if EVP_DecryptUpdate(LContext, @LOutput[0], @LOutputLen, @LEncryptedText[0], Length(LEncryptedText)) = 1 then
        begin
          if EVP_DecryptFinal_ex(LContext, @LOutput[LOutputLen], @LFinalLen) = 1 then
          begin
            Inc(LOutputLen, LFinalLen);
            Result := TEncoding.UTF8.GetString(LOutput);
          end
          else
            CryptoError;
        end
        else
          CryptoError;
      end
      else
        CryptoError;
    finally
      EVP_CIPHER_CTX_free(LContext);
    end;
  end;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  // Decrypt value output by crypto-js
  DecryptAES256('U2FsdGVkX1812TdTm8MD2w4u2AaxUB2PdurCNOmu4bmutkR1Ul7Z1+bGXDsdlNK5'); // This is just an example value. Generate a new one from the Javascript code
end;

The code "fails" at the EVP_DecryptFinal_ex stage. Have I missed anything obvious?

 

I know there's 3rd party code that will probably handle this, however it's just this function that is required (for now), so I'd prefer to avoid introducing a bunch of dependencies

Share this post


Link to post
4 hours ago, Dave Nottage said:

OpenSSL.Api_11 comes from Grijjy (as well as the SSL binaries):

Looks like it doesn't support older Delphi versions (up to Seattle it doesn't) , so sorry for this not so sure comments.

 

AES don't rat ass to the any values being fed, but it is all about the length of the data, here comes the problems with padding.

for more information about this you can read https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Padding

 

4 hours ago, Dave Nottage said:

DecryptAES256('U2FsdGVkX1812TdTm8MD2w4u2AaxUB2PdurCNOmu4bmutkR1Ul7Z1+bGXDsdlNK5'); // This is just an example value. Generate a new one from the Javascript code

This base64 have the length of 64 chars meaning it will decode to 48 byte, so in theory the IV would be 16 out of these and the rest will be the encrypted text.

I really recommend to refrain from using the _MAX_ everywhere, and either fix the lengths as you know the algorithm/scheme is used, or use other OpenSSL function to get the right lengths, it will spare you troubles down the road.

 

Now if EVP_DecryptFinal_ex failed then this should mean one thing and one only, EVP_DecryptUpdate received data and still waiting for more to be compliant with the parameters from EVP_DecryptInit_ex and the used padding scheme.

 

With some searching, found this https://stackoverflow.com/questions/53394102/openssl-decryption-evp-decryptfinal-ex-fails

 

Hope it helps.

Share this post


Link to post
4 minutes ago, Kas Ob. said:

EVP_DecryptUpdate received data and still waiting for more to be compliant with the parameters from EVP_DecryptInit_ex and the used padding scheme.

Small clarification : the size will be assumed not enough by EVP_DecryptFinal_ex if the a padding scheme is expected and will fail the same way as it didn't receive enough data (the data wasn't a multiplication of block_size)

Share this post


Link to post

OK, found the problem :classic_smile:

 

It seems OpenSSL has padding enabled by default, https://www.oreilly.com/library/view/secure-programming-cookbook/0596003943/ch05s19.html

 

So this 

EVP_CIPHER_CTX_set_padding(LContext, 0);

After EVP_DecryptInit_ex should solve the failure of EVP_DecryptFinal_ex, the rest is up to you to find and fix, specially that HMAC !!!!!, where it comes from ? , well ,i guess you already researched how that java script implemented.

Share this post


Link to post
9 minutes ago, Kas Ob. said:

So this 


EVP_CIPHER_CTX_set_padding(LContext, 0);

After EVP_DecryptInit_ex should solve the failure of EVP_DecryptFinal_ex,

That it does, thanks! 

10 minutes ago, Kas Ob. said:

the rest is up to you to find and fix, specially that HMAC !!!!!, where it comes from ?

From other code I found. Yes, I guess that part is wrong since the output is wrong.

Share this post


Link to post
11 minutes ago, Dave Nottage said:

That it does, thanks! 

...

From other code I found. Yes, I guess that part is wrong since the output is wrong.

Still there is a problem somewhere i think, see, the encryption code in java will make no sense if there is no padding as it will be prone for data lose or corruption, so i have assume either that it is using a padding scheme or the the encrypted data is well known length and constant and equal to a n*block_size (n*16), so if your plain text used with the encoding java code is less than 32 bytes then it is definitely using a padding scheme and you should use the same in decryption.

Share this post


Link to post
Testing testing 1 2 3
Done...exiting

Your encrypted text, if case you didn't solved this yet, here is the fixed code

const
  cPassphrase = 's0m3s3cr3t!';

procedure CryptoError;
var
  LError: TBytes;
  LMessage: string;
begin
  SetLength(LError, 120);
  ERR_load_crypto_strings;
  ERR_error_string_n(ERR_get_error, @LError[0], Length(LError));
  LMessage := TEncoding.UTF8.GetString(LError);
  // Display the message
  Writeln(LMessage);
end;

function DecryptAES256(const AValue: string): string;
var
  LContext: PEVP_CIPHER_CTX;
  LOutputLen, LFinalLen: Integer;
  LBytes, LSalt, LIV, LEncryptedText, LKey, LOutput: TBytes;
  LCipher: Pointer;
begin
  Result := '';
  LBytes := TNetEncoding.Base64.DecodeStringToBytes(AValue);

  LCipher := EVP_aes_256_cbc;

  // prepare Key and IV length
  SetLength(LKey, EVP_CIPHER_key_length(LCipher));
  SetLength(LIV, EVP_CIPHER_iv_length(LCipher));

  // crypto-js returns an OpenSSL result, i.e. "salted__" + salt + encrypted text
  LEncryptedText := Copy(LBytes, 16, MaxInt);

  // Calc key and IV from passphrase and salt , crypto-js by default uses KDF with (MD5/8 bytes Salt/1 iteration)
  // https://github.com/brix/crypto-js/blob/develop/src/evpkdf.js
  LSalt := Copy(LBytes, 8, 8);
  if EVP_BytesToKey(LCipher, EVP_md5, @LSalt[0], @AnsiString(cPassphrase)[1], Length(cPassphrase),
    1, @LKey[0], @LIV[0]) > 0 then
  begin
    LContext := EVP_CIPHER_CTX_new();
    // Set padding
    EVP_CIPHER_CTX_set_padding(LContext, EVP_PADDING_PKCS7);
    if LContext <> nil then
    try
      // Initialize the decryption operation with 256 bit AES
      if EVP_DecryptInit_ex(LContext, LCipher, nil, @LKey[0], @LIV[0]) = 1 then
      begin
        SetLength(LOutput, Length(LEncryptedText) + EVP_CIPHER_block_size(LCipher));
        // Provide the message to be decrypted, and obtain the plaintext output
        if EVP_DecryptUpdate(LContext, @LOutput[0], @LOutputLen, @LEncryptedText[0],
          Length(LEncryptedText)) = 1 then
        begin
          if EVP_DecryptFinal_ex(LContext, @LOutput[LOutputLen], @LFinalLen) = 1 then
          begin
            Inc(LOutputLen, LFinalLen);
            SetLength(LOutput, LOutputLen);
            Result := TEncoding.UTF8.GetString(LOutput);   // might raise an unanticipated exception, should be inside try-except
            //Result := StringOf(LOutput);    // IMO, this is safer than risking raising an exception
          end
          else
            CryptoError;
        end
        else
          CryptoError;
      end
      else
        CryptoError;
    finally
      EVP_CIPHER_CTX_free(LContext);
    end;
  end
  else
    CryptoError;
end;

begin
  // Decrypt value output by crypto-js
  // This is just an example value. Generate a new one from the Javascript code
  Writeln(DecryptAES256('U2FsdGVkX1812TdTm8MD2w4u2AaxUB2PdurCNOmu4bmutkR1Ul7Z1+bGXDsdlNK5'));
  Writeln('Done...exiting');
  Readln;
end.

I recommend double check string Unicode on java part and how the above code will behave.

  • Thanks 1

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×