Dave Nottage 557 Posted September 14, 2023 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
Kas Ob. 121 Posted September 14, 2023 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
Kas Ob. 121 Posted September 14, 2023 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
Kas Ob. 121 Posted September 14, 2023 OK, found the problem 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
Dave Nottage 557 Posted September 14, 2023 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
Kas Ob. 121 Posted September 14, 2023 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
Kas Ob. 121 Posted September 15, 2023 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. 1 Share this post Link to post
Dave Nottage 557 Posted September 15, 2023 2 hours ago, Kas Ob. said: here is the fixed code Great, thanks!! Share this post Link to post