2

I have an application that encrypts and decrypts a field in C# using Rfc2898DeriveBytes. I have been trying to work out a cross platform solution using CryptoJS PBKDF2 to write a decrypt method in JavaScript. However, I have not been able to figure out the following 2 issues:

  • I can't get the keys to match
  • I can't figure out what the IV should be in JavaScript.

Key generation

Generating the key in C# (unfortunately I don't have much control over this as it supports a lot of legacy systems)

private readonly RijndaelManaged _alg = new RijndaelManaged();

public EncryptionManager()
{
    var secret = 'D2s1d_5$_t0t3||y_4c3$0m3!1!1!!';
    var salt = 'o6805542kcM7c5';

    var saltBytes = Encoding.ASCII.GetBytes(salt);
    using (var keyDeriver = new Rfc2898DeriveBytes(secret, saltBytes))
    {
        _alg.Key = keyDeriver.GetBytes(_alg.KeySize / 8); // _alg.KeySize = 256
    }
}

The JS code I have for the key generation is:

const secret = CryptoJS.enc.Utf8.parse('D2s1d_5$_t0t3||y_4c3$0m3!1!1!!');

// Encoding the Salt in from UTF8 to byte array
const salt = CryptoJS.enc.Utf8.parse('o6805542kcM7c5');

// Creating the key in PBKDF2 format to be used during the decryption
const key = CryptoJS.PBKDF2(secret.toString(CryptoJS.enc.Utf8), salt, {
    keySize: 128 / 32, 
    iterations: 1000,
});

This should ideally work according to my research but the keys generated in both the codes are never the same. I have burnt a lot of midnight oil scratching my head over what I am doing wrong but I don't see why.

Decryption

The decryption method in C# is as follows:

public string Decrypt(string ciphertext)
{
    var cipherTextBytes = Convert.FromBase64String(ciphertext);
    var ivSize = BitConverter.ToInt32(cipherTextBytes, 0);
    var iv = new byte[ivSize];
    var offset = sizeof(int);
    Array.Copy(cipherTextBytes, offset, iv, 0, ivSize);
    offset += ivSize;

    using (var msDecrypt = new MemoryStream(cipherTextBytes, offset, cipherTextBytes.Length - offset))
    {
        lock (_syncLock)
        {
            using (var decryptor = _alg.CreateDecryptor(_alg.Key, iv))
            using (var decryptStream = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
            using (var reader = new StreamReader(decryptStream))
            {
                return reader.ReadToEnd();
            }
        }
    }
}

My solution for the decryption in JS is:

const decrypt = (encryptedData: string): string => {

    // Enclosing the test to be decrypted in a CipherParams object as supported by the CryptoJS libarary
    const cipherParams = CryptoJS.lib.CipherParams.create({
        ciphertext: CryptoJS.enc.Base64.parse(encryptedData),
    });

    // What should be the IV be here?
    const iv = CryptoJS.enc.Hex.parse(encryptedData);

    // Decrypting the string contained in cipherParams using the PBKDF2 key
    const decrypted = CryptoJS.AES.decrypt(cipherParams, key, {
        mode: CryptoJS.mode.CBC,
        // iv,
        padding: CryptoJS.pad.Pkcs7,
    });
    decryptedText = decrypted.toString(CryptoJS.enc.Utf8);
    return decryptedText;
}

I am assuming if I could get the correct key and IV, I could solve this. I have looked around a lot for compatible solutions but I'm at a loss as to what I am doing wrong.

Any assistance would be greatly appreciated.

nashcheez
  • 5,067
  • 1
  • 27
  • 53
  • I would try to simplify the example, especially the used password and salt (e.g. only small letters), just to make sure you are not running into an encoding problem. And don't use different encodings, like ASCII on one platform, UTF-8 on another like you do for the salt. BTW: the IV size for AES CBC is fixed 128bit, hence you don't have to store/transmit that. – Robert Nov 27 '20 at 17:38
  • The C# code returns for your passphrase and salt the 32 bytes key (hex encoded) `e3912cec5e9d1ec5a756cf95991c08f8ce174ee2ddad61c13ff7ece89c0e83e1`. The CryptoJS code returns the 16 bytes key (hex encoded) `e3912cec5e9d1ec5a756cf95991c08f8`. If the key size in the CryptoJS code is adjusted with `keySize: 256 / 32`, then the keys are identical. If you get other keys, you should post them. – Topaco Nov 27 '20 at 17:52
  • It would also be helpful if you could post the C# encryption code and/or sample data (plaintext, key, ciphertext). – Topaco Nov 27 '20 at 17:56
  • @Robert Thank you for your suggestions, will keep in mind. Unfortunately, I don't have much control over the C# code but I will try to modify the JS And yes, you are correct the IV size in C# has been calculated to 16bytes, won't do the calculation in JS. – nashcheez Nov 27 '20 at 19:11
  • @Topaco That's amazing. I changed the `keySize: 256/32` and voila they matched when hex encoded. Thank you for assisting me on that. The passphrase and salt here are not exactly what I am using but are slightly modified for security purposes. I am still having trouble figuring out the IV on the JS and the offset logic in the C# decrypt isn't very clear to me either. – nashcheez Nov 27 '20 at 19:16
  • For decryption you must first separate the encrypted data (IV size, IV, ciphertext) after Base64 decoding, please see my answer. – Topaco Nov 27 '20 at 20:50

1 Answers1

1

As already mentioned in my comment, the C# code returns the following 32 bytes key with the posted salt and passphrase (hex encoded):

e3912cec5e9d1ec5a756cf95991c08f8ce174ee2ddad61c13ff7ece89c0e83e1

The CryptoJS code generates only a 16 bytes key. If the key size is adjusted with keySize: 256 / 32, the CryptoJS code returns the same 32 bytes key.


The following Base64 encoded ciphertext can be decrypted with the C# code using the posted passphrase and salt:

EAAAACMtkB64He4p/MSI+yF2A2rJWhxpssG6b48Z01JrfbErvQ1r6Gi0esgCmdrBaFxOPFF1+AUsyrUUl5FQ4Nk0dSU=

If the ciphertext is hex encoded, the result is:

10000000 232d901eb81dee29fcc488fb2176036a c95a1c69b2c1ba6f8f19d3526b7db12bbd0d6be868b47ac80299dac1685c4e3c5175f8052ccab514979150e0d9347525

The first 4 bytes contain the information about the IV length. Since the length of the IV is known (16 bytes for AES), it would not really be necessary to store this information, as already mentioned in Robert's comment. The following 16 bytes correspond to the IV, the remaining bytes to the actual ciphertext. These data have to be separated. After that the decryption can be done:

// Key derivation
var secret = CryptoJS.enc.Utf8.parse('D2s1d_5$_t0t3||y_4c3$0m3!1!1!!');
var salt = CryptoJS.enc.Utf8.parse('o6805542kcM7c5');
var key = CryptoJS.PBKDF2(secret, salt, {
    keySize: 256 / 32, 
    iterations: 1000,
});

// Separation of iv size, iv and ciphertext
var encryptedDataB64 = "EAAAACMtkB64He4p/MSI+yF2A2rJWhxpssG6b48Z01JrfbErvQ1r6Gi0esgCmdrBaFxOPFF1+AUsyrUUl5FQ4Nk0dSU=";
var encryptedData = CryptoJS.enc.Base64.parse(encryptedDataB64);
var ivSize = CryptoJS.lib.WordArray.create(encryptedData.words.slice(0, 1));
var iv = CryptoJS.lib.WordArray.create(encryptedData.words.slice(1, 1 + 4));
var ciphertext = CryptoJS.lib.WordArray.create(encryptedData.words.slice(1 + 4));

// Decryption
var cipherParams = CryptoJS.lib.CipherParams.create({
    ciphertext: ciphertext
});
var decrypted = CryptoJS.AES.decrypt(cipherParams, key, {
    mode: CryptoJS.mode.CBC,
    iv: iv,
    padding: CryptoJS.pad.Pkcs7,
});
var decryptedText = decrypted.toString(CryptoJS.enc.Utf8);

// Output
console.log(decryptedText);
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>
Topaco
  • 40,594
  • 4
  • 35
  • 62
  • You sir just made my day. Very well explained answer, the separation of iv and ciphertext was what I was doing wrong. This is really helpful! – nashcheez Nov 27 '20 at 21:26