Crypto JS: TripleDES not encrypting properly - javascript

When using crypto js tripleDES to encrypt, I noticed that all strings to encrypt which are of small length e.g "01", "111", "20" encrypt properly but with long strings like "5000021234567890000", the output isn't correct or is not the value I am expecting.
For example for this "5000021234567890000", I am expecting this: "HctDaKc/U9avOwZMSS5mEi62kfojDHA4" but I get this instead: HctDaKc/U9bNnFy6eZHqHj3SemorJxQM.
This is the code I found and using to encrypt
let key = CryptoJS.MD5(enckey);
key.words[4] = key.words[0];
key.words[5] = key.words[1];
let iv = CryptoJS.lib.WordArray.create(64/8);
CryptoJS.TripleDES.encrypt("5000021234567890000", key, {iv: iv});
Your help would be very much appreciated.

Looking at the encrypted data the first blocks match and the following two blocks do not.
HctDaKc/U9avOwZMSS5mEi62kfojDHA4
1DCB4368A73F53D6 AF3B064C492E6612 2EB691FA230C7038
HctDaKc/U9bNnFy6eZHqHj3SemorJxQM
1DCB4368A73F53D6 CD9C5CBA7991EA1E 3DD27A6A2B27140C
The IV is presumably all 0x00 bytes.
THis indicates one is using ECB m ode and the other is using CBC mode.
See Block cipher mode of operation, specifically ECB and CBC modes.
ECB mode encrypted each block independly, CBC mode xors the previous block with the data to be encrypted and for the first block the IV. Since the IV is all 0x00 values no change is made to the first block.
Options can be specified in the createion of the encryptor, you need to see the documentation page, good luck with that.
It would look something like:
encryptor = crypto.createCipheriv( mode, key, iv)
where mode is one of: 'des-ede', 'des-ede-cbc', 'des-ede3', 'des-ede3-cbc', 'des3'

Related

Decryption of TripesDES algorithm in CryptoJS returns nothing

I tried encrypting a data using TripleDES in CryptoJS library. I created an encryption example using this tool https://www.devglan.com/online-tools/triple-des-encrypt-decrypt and generated a string encryption data of NbU4PoYHR9IJtSLmHRubpg==
Now I want to decrypt this NbU4PoYHR9IJtSLmHRubpg== generated from the site using javascript code with CryptoJS library.
I have generated also a SharedKey with this string 36fd14ddcd755bb37879cbe99ca26c92
Here is my code:
export const decryptData = (params) => {
const {ClientSecret, ApiKey} = params;
const SharedKey = `${ClientSecret}:${ApiKey}`;
const data = 'NbU4PoYHR9IJtSLmHRubpg==';
let CryptoSharedKey = CryptoJS.MD5(SharedKey).toString();
const ct = CryptoJS.enc.Base64.parse(data);
console.log('decrypt',CryptoJS.TripleDES.decrypt(ct, CryptoSharedKey).toString(CryptoJS.enc.Utf8))
}
now the problem is when i console log the result, it gives me nothing but an empty string.
The web tool simply UTF-8 encodes the key, so in the CryptoJS code the key derivation via MD5 must be removed.
Also, the web tool automatically truncates too long keys to 24 bytes, the key length used by TripleDES in the 3TDEA variant. Since the key you apply is 32 bytes in size, it is truncated accordingly. CryptoJS also shortens keys that are too long implicitly (though it is more transparent to shorten them explicitly).
Furthermore, in the CryptoJS code, the key material must be passed as WordArray to be interpreted as key (if passed as string, it will be interpreted as password and a key derivation function will be applied). For conversion to a WordArray the key has to be parsed with the Utf8 encoder.
In addition, the ciphertext must be passed as CipherParams object or Base64 encoded (the latter is implicitly converted to a CipherParams object).
Moreover, since the ECB mode was used for encryption in the web tool, this mode must also be applied in the CryptoJS code. For this, ECB must be explicitly set, since CBC is the CryptoJS default.
Hence, the code is to be changed as follows:
var key = CryptoJS.enc.Utf8.parse('36fd14ddcd755bb37879cbe99ca26c92'.substr(0, 24)); // Explicitly shorten the key to 24 bytes; parse the key into a WordArray
var data = 'NbU4PoYHR9IJtSLmHRubpg==';
var decrypted = CryptoJS.TripleDES.decrypt(data, key, {mode: CryptoJS.mode.ECB}); // Pass data Base64 encoded; apply ECB mode (default is CBC)
console.log('decrypt: ', decrypted.toString(CryptoJS.enc.Utf8));
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
Note that TripleDES is outdated and that ECB is insecure.

Implement C# encryption in CryptoJS

I have situation where I need to create the same encryption method which is already up and running in C#. The concept behind this is, from where ever this encrypted key is logged, we will use the same C# project to decrypt it.
Below is the logic used in C#:
using var aes = new AesCryptoServiceProvider
{
Key = Encoding.UTF8.GetBytes(key),
Mode = CipherMode.CBC,
Padding = PaddingMode.PKCS7
};
aes.GenerateIV();
using var encrypter = aes.CreateEncryptor(aes.Key, aes.IV);
using var cipherStream = new MemoryStream();
using (var tCryptoStream = new CryptoStream(cipherStream, encrypter, CryptoStreamMode.Write))
using (var tBinaryWriter = new BinaryWriter(tCryptoStream))
{
cipherStream.Write(aes.IV);
tBinaryWriter.Write(Encoding.UTF8.GetBytes(encryptMe));
tCryptoStream.FlushFinalBlock();
}
return Convert.ToBase64String(cipherStream.ToArray());
Key is the same key used in both C# and JavaScript. But still I am not able to generate the same encryption value as in C#.
I tried to go through other Stack Overflow posts related to this topic, but unable to figure the missing part in JavaScript. Can any one please help?
The key used in the C# code is UTF-8 encoded, so on the CryptoJS side the key must be parsed into a WordArray using the UTF-8 encoder (CryptoJS only interprets the key material as key if it is passed as a WordArray; if it is passed as string, it is interpreted as password and a key derivation function is applied, which would not be compatible with the C# code).
Also, the C# code concatenates IV and ciphertext, which must also happen in the CryptoJS code. This is necessary because the IV is required for decryption.
Fixed code:
var plaintext = 'The quick brown fox jumps over the lazy dog';
var key = CryptoJS.enc.Utf8.parse('01234567890123456789012345678901'); // Fix 1: parse as WordArray
var iv = CryptoJS.lib.WordArray.random(128 / 8);
var encrypted = CryptoJS.AES.encrypt(plaintext, key, {iv: iv}); // CBC, PKCS#7 padding by default
var ivCiphertext = iv.clone().concat(encrypted.ciphertext).toString(CryptoJS.enc.Base64); // Fix 2: concatenate IV and ciphertext
console.log(ivCiphertext); // e.g. e9iXcQ2sZ6AA2ne1c3490pAPWOrTGf4UttSSX1lOiKUqwP0oWRPFF83VhZQZMMBu9JKNWIfgS+9D5V39bI4rqg==
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
For a test, the ciphertexts cannot simply be compared because, due to the random IV, each encryption produces different ciphertexts.
One option for a test is to temporarily use the same IV in the C# code and in the CryptoJS code for the test (and only for the test, since a static IV is insecure!), which would produce the same ciphertexts that could then be compared.
Another option for a test is to decrypt the ciphertext produced with the CryptoJS code with the C# code for decryption.

When using AES-CBC in WebCrypto, why does a wrong key cause an OperationError but a wrong ciphertext or IV don't?

As far as I understand, using a wrong key (with the correct size) to decrypt something with AES-CBC should just output some garbage. CBC doesn't have any sort of MAC, so you really can only look at the results of the decryption and decide for yourself if that is the plaintext you want.
However, when decrypting with SubtleCrypto, a wrong key will cause an OperationError, but a wrong ciphertext will not, and neither will a wrong IV. I would have expected all these three cases to have similar behaviours.
How is it possible for the implementation to know that the key was wrong and not any of the other inputs? Do keys have to have a specific structure, other than the size? In that case, the key space would be smaller than the advertised bit length of the key, no?
async function simpleCryptoTest() {
// all zeroes plaintext, key and IV
const iv = new ArrayBuffer(16)
const key = new ArrayBuffer(32)
const plaintext = new ArrayBuffer(64)
const algorithm = {name: 'AES-CBC'};
const correctCryptoKey = await crypto.subtle.importKey('raw', key, algorithm, false, ['encrypt', 'decrypt'])
const ciphertext = await crypto.subtle.encrypt({...algorithm, iv: iv}, correctCryptoKey, plaintext)
console.log("ciphertext", ciphertext)
const decryptedCorrect = crypto.subtle.decrypt({...algorithm, iv: iv}, correctCryptoKey, ciphertext)
const wrongCiphertext = new Uint8Array(ciphertext)
wrongCiphertext[0] = ~ciphertext[0] // flipping the first byte should be enough
const decryptedWrongCiphertext = crypto.subtle.decrypt({...algorithm, iv: iv}, correctCryptoKey, wrongCiphertext)
const wrongIv = new Uint8Array(iv)
wrongIv[0] = 1 // we know the correct IV is all zeroes
const decryptedWrongIv = crypto.subtle.decrypt({...algorithm, iv: wrongIv}, correctCryptoKey, ciphertext)
const wrongKey = new Uint8Array(key)
wrongKey[0] = ~key[0]
const decryptedWrongKey = crypto.subtle.importKey('raw', wrongKey, algorithm, false, ['decrypt']).then((wrongCryptoKey) => {
return crypto.subtle.decrypt({...algorithm, iv: iv}, wrongCryptoKey, ciphertext)
})
const results = await Promise.allSettled([decryptedCorrect, decryptedWrongCiphertext, decryptedWrongIv, decryptedWrongKey])
console.log("decrypted with the correct key", results[0])
console.log("decrypted with corrupted ciphertext", results[1])
console.log("decrypted with corrupted IV", results[2])
console.log('decrypted with the wrong key', results[3])
}
simpleCryptoTest()
/*
decrypted with the correct key → {status: "fulfilled", value: ArrayBuffer(64)}
decrypted with corrupted ciphertext → {status: "fulfilled", value: ArrayBuffer(64)}
decrypted with corrupted IV → {status: "fulfilled", value: ArrayBuffer(64)}
decrypted with the wrong key → {status: "rejected", reason: DOMException} // e.name == 'OperationError'
*/
Please note that I am aware that CBC has no authentication, and I am aware that GCM exists. I need CBC because I am implementing a variation of the Signal Protocol, which I most certainly do not intend to roll out in production without a proper crypto review. Thanks :-)
Also, I tested this on Firefox 77.0.1 and Chromium 83.0.4103.97 for Linux.
There is no MAC, but there is padding. I'm not very familiar with WebCrypto but chances are you are using PKCS7 padding in your encryption algorithm specification -- either explicitly or by default. The padding bytes added to the end of the plaintext have the value k k ... k, where k is number of padding bytes needed, 1 <= k <= 16. Upon decryption, a check is made if the last byte k is in the range specified, and if the last k bytes are equal to k. If that check fails then something has gone wrong and the OperationError is returned.
Now, as for corrupted IV and corrupted ciphertext, the reason it works is a "feature" of CBC mode. If you look carefully at the diagram of the decrypt direction of CBC mode you'll note that following facts (remember, this is on decryption):
A corrupted IV affects only the first block of plaintext. All the rest decrypt correctly
A corrupted ciphetext affects only the current block of plaintext and the next block. All the blocks before and after decrypt correctly.
Therefore, try changing the ciphertext block before the last block and you should see your OperationError. However, the padding check is no substitute for a real MAC, and even with a corrupted key or last or next-to-last ciphertext block there is still a decent chance that the padding check will succeed. If the last byte of the final decrypted block equals 1 then the padding check succeeds. This probability of this is 1/256 for the corrupted items listed. (It is actually a little higher because if the last two bytes are equal 2, or the last 3 bytes are equal to 3,... etc., then the padding check also succeeds). So as an experiment try changing two bytes of the key about 500 or so times and you should 1 or 2 instances where the decryption succeeds without error.

Why is AES function returning different value?

Why is AES with same secret phrase and message returns different values each time?
Let's say we have a same salt for each PBKDF2 functions( I know it's bad, it's just for an example). Let's say we are entering same passphrase each time.
var salt = "5J07c/a7+2bf=15$56aQc75Ub55=60&0";
console.log(req.body.password);
console.log(salt);
var PBKDF2hash = crypto.PBKDF2(req.body.password, salt, { keySize: 256/32 });
console.log(PBKDF2hash.toString());
var AEScipher = crypto.AES.encrypt(req.body.password, PBKDF2hash);
console.log(AEScipher.toString());
In this case we receive same PBKDF2hash (as expected), but any time AES provides different chipher.
zz
5J07c/a7+2bf=15$56aQc75Ub55=60&0
3949676666ed318087a52896be98dc80b0cad99f4b662d48565283f71a2ace80
U2FsdGVkX19O1pqgL+V6Chk8NdiJQhf15N1uEfYXgxw=
zz
5J07c/a7+2bf=15$56aQc75Ub55=60&0
3949676666ed318087a52896be98dc80b0cad99f4b662d48565283f71a2ace80
U2FsdGVkX1/C7GAmLJvfFAHyOYj7LKZI5278/ZoeA3M=
These answers says the thing is salt is differrent and cbc mode matters. In my case salt is constant and I've switched to other modes. Output is still different each time.
The initialization vector used in CBC mode is a random block, so each encryption will be different. An IV is sort of like a salt, except when encrypting a message there is no reason to ever specify any specific IV to use (a random IV should always be used). The IV is often put in front of the first block of the encrypted message.

CryptoJS AES pattern always ends with =

I'm using CryptoJS to encrypt some usernames and passwords, it's working well enough I think.
But I have some questions regarding the encrypted data plaintext.
No matter what the key or data is it always starts with "U2FsdGVkX1...".
The encrypted data changes constantly even if the input data remains the same as shown below:
U2FsdGVkX1/BshMm2v/DcA6fkBQGPss6xKa9BTyC8g0=
U2FsdGVkX1/uc5OTSD7CfumdgqK1vN2LU4ISwaQsTQE=
U2FsdGVkX1/8OOLOTZlfunN4snEVUdF2ugiL7SeAluE=
U2FsdGVkX1+c8j3l1NRBJDb1byHwOmmNSmbTci22vsA=
username_encrypted = CryptoJS.AES.encrypt(username, key);
password_encrypted = CryptoJS.AES.encrypt(password, key);
console.log(username_encrypted.toString());
console.log(password_encrypted.toString());
console.log(CryptoJS.AES.decrypt(username_encrypted, key).toString(CryptoJS.enc.Utf8));
console.log(CryptoJS.AES.decrypt(password_encrypted, key).toString(CryptoJS.enc.Utf8));
Is this the way it is supposed to work or am I doing something wrong? Because on some online AES encryption sites I get very different results, encrypted data not changing all the time for one.
That's correct. CryptoJS uses the OpenSSL proprietary format. If it uses a salted password to derive a key, it has a magic value in front. E.g. your first base64 translates into
53616C7465645F5F C1B21326DAFFC3700E9F9014063ECB3AC4A6BD053C82F20D
in hex. Connoisseurs will immediately recognize the first 8 bytes as being ASCII,
Salted__ | C1B21326DAFFC370 0E9F9014063ECB3AC4A6BD053C82F20D
So what you have is first a 8 byte magic, then an 8 byte salt, then the ciphertext.
The actual key and IV are derived from the key in your code (which is actually interpreted as being a passphrase). As the salt is always random, so are the derived key and IV. This is how it should be as otherwise you could distinguish plaintext that start with (or is fully identical) to other plaintext.

Categories

Resources