1

I need to integrate with a third-party REST API. The body of my request (as well as the response) need to be encrypted using AES encryption using an AES key that was communicated to us. There are a few requirements:

  • Use a Rijndael cipher
  • Electronic Code Book mode (ECB)
  • No built-in padding

Edit 1:

This is an extract from the Integration Guide I was provided:

All URL parameters must be encrypted using AES with a shared key. Additionally, we are able to support all widely used encryption algorithms. Standard AES encryption algorithm may be used by the client system to generate the secure token (Integration Token). The following AES cipher attributes should be used:

  • Rijndael cipher
  • Electronic Code Book mode (ECB)
  • No built in padding (such as PKCS)
  • Resultant encrypted buffer should be manually padded with spaces to achieve the total length a multiple of 32. That is: length(encrypted padded string) mod 32 = 0

For example, instantiate the cipher and initialize it using Java standard SunJCE cryptological library the code would contain: Cipher cipher = Cipher.getInstance("Rijndael/ECB/NoPadding", "SunJCE");

End of Edit 1

I tried to play a little bit with some snippets of code that I found mostly here and I wrote the following sequence:

  var clearMessage = "The brown fox jumps over the laxy dog";
  console.log("Clear message: ", clearMessage);
  var encodingKey = CryptoJS.enc.Hex.parse("0123456789ABCDEF0123456789ABCDEF");
  var encryptedMessage = CryptoJS.AES.encrypt(CryptoJS.enc.Utf8.parse(clearMessage), encodingKey, {
                                              mode: CryptoJS.mode.ECB,
                                              padding: CryptoJS.pad.NoPadding});
  console.log("Encrypted message: ", encryptedMessage.ciphertext.toString());


  const decryptedMessage = CryptoJS.AES.decrypt(encryptedMessage.ciphertext.toString(), encodingKey, {
                                                mode: CryptoJS.mode.ECB,
                                                padding: CryptoJS.pad.NoPadding});
  clearMessage = decryptedMessage.toString(CryptoJS.enc.Utf8);
  console.log("Decrypted message: ", clearMessage);

In the console of my browser, I see this output:

Clear message:  The brown fox jumps over the laxy dog
Encrypted message:  56aaf639f44c106889aa4a765d2bf7c83a7860a379d776982991c7575eb63fd31f7c9ce3db

crypto-js.js:523 Uncaught Error: Malformed UTF-8 data
at Object.stringify (crypto-js.js:523:24)
at WordArray.init.toString (crypto-js.js:278:38)
at encryptDecrypt (index.html:157:39)
at HTMLButtonElement.onclick (index.html:46:60)

Spelling mistake aside ("laxy dog" ?!!?), what is wrong with my decrypting sequence? The error message is triggered on this line of code:

clearMessage = decryptedMessage.toString(CryptoJS.enc.Utf8);

Edit 2:

This is what chatgpt has generated for this question:

// Import the necessary libraries
var CryptoJS = require("crypto-js");

// Set the plaintext message and secret key
var message = "This is a secret message!";
var secretKey = "ThisIsASecretKey";

// Convert the key and message to WordArrays (required by CryptoJS)
var key = CryptoJS.enc.Utf8.parse(secretKey);
var plaintext = CryptoJS.enc.Utf8.parse(message);

// Encrypt the plaintext message using AES with Rijndael cipher, ECB mode, and no padding
var ciphertext = CryptoJS.AES.encrypt(plaintext, key, {
  mode: CryptoJS.mode.ECB,
  padding: CryptoJS.pad.NoPadding,
  cipher: CryptoJS.algo.Rijndael
});

// Print the encrypted ciphertext in Base64 format
console.log(ciphertext.toString());

// Decrypt the ciphertext message using AES with Rijndael cipher, ECB mode, and no padding
var decrypted = CryptoJS.AES.decrypt(ciphertext, key, {
  mode: CryptoJS.mode.ECB,
  padding: CryptoJS.pad.NoPadding,
  cipher: CryptoJS.algo.Rijndael
});

// Convert the decrypted message back to plaintext and print it
console.log(decrypted.toString(CryptoJS.enc.Utf8));

It gives me the exact same error when I try to decode the cipher.

End of Edit 2

Any ideas or suggestions?

TIA, Ed

Eddie
  • 271
  • 4
  • 18
  • 1
    Your requirements are unclear: ECB is a block cipher mode, i.e. the length of the plaintext must be an integer multiple of the block size, i.e. 16 bytes for AES. If this is not fulfilled, padding is *mandatory*. So what does *No built-in padding* mean with regard to ECB? That the length criterion is met and you don't need padding? Or that you should implement a user-defined padding (which sounds more like homework)? Note that ECB is insecure. – Topaco Mar 02 '23 at 08:06
  • 1
    Calling `toString` on the ciphertext is probably a bad idea, try directly feeding it `encryptedMessage`. @Topaco Well, there is such a thing as ciphertext stealing, but it isn't used much (and certainly not by CryptoJS). – Maarten Bodewes Mar 02 '23 at 12:29
  • @Topaco - I am not an expert in cryptography, hence my question here. I've added an extract from the Integration Guide as the Edit 1 above. – Eddie Mar 02 '23 at 14:18
  • Rijndael actually defines more block and key sizes than AES. But the SunJCE provider uses in `"Rijndael/ECB/NoPadding"` Rijndael as a synonym for AES, i.e. `"Rijndael/ECB/NoPadding"` means AES in ECB mode without padding, at least according to my tests (btw in later versions e.g. 17 this identifier is not supported anymore). – Topaco Mar 02 '23 at 15:41
  • The 4th requirement *Resultant encrypted buffer...* describes the padding to be implemented: padding with spaces to a length of 32 bytes. The 32 bytes are a bit irritating, since the block size of AES is 16 bytes (but maybe this padding is a legacy). – Topaco Mar 02 '23 at 15:42
  • I was given a Java code snippet that it is said to be working, but I have a hard time translating that into JavaScript code. Can you take the code from above and change it to run without errors and check that it decrypts the message properly? – Eddie Mar 02 '23 at 16:05
  • @MaartenBodewes If I do that, I get this error in my browser's console: Uncaught TypeError: base64Str.indexOf is not a function – Eddie Mar 02 '23 at 16:06
  • Changing the input of `decrypt()` doesn't solve the issue, since CryptoJS produces a *corrupted* ciphertext for the combination *block cipher mode / no padding* (whenever the last block is incomplete). When this is fixed, the ciphertext is to be passed to `decrypt()` as `CipherParams` object. – Topaco Mar 02 '23 at 16:37
  • *...I was given a Java code snippet that it is said to be working...* Post this snippet please so that the CryptoJS code can be tested. – Topaco Mar 02 '23 at 16:37

1 Answers1

1

These are the requirements regarding the padding:

  • No built in padding (such as PKCS)
  • Resultant encrypted buffer should be manually padded with spaces to achieve the total length a multiple of 32. That is: length(encrypted padded string) mod 32 = 0

Although it is not explicitly mentioned in the requirements, it is assumed for the implementation that no padding is applied if the plaintext length already corresponds to an integer multiple of the block size. If this is to be different, the padding must be adjusted accordingly.

Be aware that the specified padding is unreliable, since the plaintext could have spaces at the end, which would also be removed during unpadding.
A reliable padding is e.g. PKCS#7 padding which contains the information about the number of padding bytes so that during unpadding the removal of data of the actual plaintext is prevented.


And these are the requirements regarding encryption:

  • Rijndael cipher
  • Electronic Code Book mode (ECB)
  • using Java standard SunJCE cryptological library the code would contain: Cipher cipher = Cipher.getInstance("Rijndael/ECB/NoPadding", "SunJCE");

The SunJCE provider uses in "Rijndael/ECB/NoPadding" the designation Rijndael as a synonym for AES, i.e. "Rijndael/ECB/NoPadding" means AES in ECB mode without padding. In later versions, e.g. Java 17, this naming is no longer supported. Also keep in mind that Rijndael and AES are in fact not the same, AES is just a subset of Rijndael, see here. Note that ECB mode is insecure, see here.


A possible implementation of encryption and decryption with CryptoJS including padding/unpadding that meets the specification above is then:

// Encryption
var keyWA = CryptoJS.enc.Hex.parse("0123456789ABCDEF0123456789ABCDEF");
var plaintext = "The brown fox jumps over the laxy dog";
var plaintextWA = CryptoJS.enc.Utf8.parse(plaintext);
var paddedPlaintextWA = padLazy(plaintextWA, 32);
var ciphertextCP = CryptoJS.AES.encrypt(paddedPlaintextWA, keyWA, {mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.NoPadding});
var ciphertextHex = ciphertextCP.ciphertext.toString();
var ciphertextB64 = ciphertextCP.toString();
console.log(ciphertextHex);
console.log(ciphertextB64);

// Decryption
var decryptedWA = CryptoJS.AES.decrypt(ciphertextB64, keyWA, {mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.NoPadding}); 
//var decryptedWA = CryptoJS.AES.decrypt({ciphertext: CryptoJS.enc.Hex.parse(ciphertextHex)}, keyWA, {mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.NoPadding}); // works also
//var decryptedWA = CryptoJS.AES.decrypt(ciphertextCP, keyWA, {mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.NoPadding}); // works also
var decrypted = unpad(decryptedWA);
console.log(">" + plaintext + "<");
console.log(">" + decrypted.toString(CryptoJS.enc.Utf8) + "<");

function padLazy(dataWA, blockSize){
  var oversize = dataWA.sigBytes % blockSize;
  if (oversize != 0) {
    var paddingWA = CryptoJS.enc.Latin1.parse(" ".repeat(blockSize - oversize));
    dataWA = dataWA.clone().concat(paddingWA); // clone, otherwise dataWA is changed
  }
  return dataWA;
}

function unpad(dataWA){
  var data = dataWA.toString(CryptoJS.enc.Latin1);
  data = data.replace(/ *$/g, '');
  return CryptoJS.enc.Latin1.parse(data);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>

Note that encrypt() returns a CipherParams object (s. here) and decrypt() expects one (s. here). In the above example, ciphertextCP, {ciphertext: CryptoJS.enc.Hex.parse(ciphertextHex)} or, as is usually the case for convenience, the Base64 encoded ciphertext ciphertextB64 (which is automatically converted to a CipherParams object) could be passed to decrypt().
However, encryptedMessage.ciphertext.toString(), like in the current code, would not work (as already noted in the comments).


The main problem here is the implementation of the custom padding and the flawed behavior of CryptoJS for the constellation block cipher mode without padding. The latter will be briefly addressed in the following:

For a block cipher mode, the plaintext length must correspond to an integer multiple of the block size (16 bytes for AES). Since plaintexts generally do not meet this length criterion, padding is used, i.e. the plaintext is padded at the end following a certain algorithm until the length criterion is met (for some padding even when the length criterion is already met from the beginning).
An alternative to padding is ciphertext stealing (see the comments), where the resulting ciphertext has the plaintext length. However, ciphertext stealing does not play a role here and is therefore not considered further (more on this can be found on the web).

Most libraries throw a corresponding exception for a plaintext that does not meet the length criterion when padding is disabled in a block cipher mode.
CryptoJS, however, does not generate an exception, but produces a ciphertext of the length of the plaintext. At first glance, this looks like ciphertext stealing, but in fact it has nothing to do with it. It is simply a corrupted ciphertext, which is produced according to the following logic: CryptoJS pads the plaintext with 0x00 values up to the required length, then performs the encryption, and then truncates the ciphertext to the plaintext length. This results in a corrupted last ciphertext block.
During decryption, the ciphertext is similarly padded with 0x00 values to the required length, then decryption is performed, and then the plaintext is truncated to the ciphertext length. This results in a corrupted last plaintext block.
There is an open CryptoJS issue about this behavior: #282 (since May 10, 2020).

Regarding the ChatGPT code: This code does practically nothing to solve the problem (as expected), but at least recognizes the small lapse in passing the ciphertext to decrypt().

Topaco
  • 40,594
  • 4
  • 35
  • 62
  • Thanks! I ran your code and saw that it does what I was looking for. The Integration Guide contains an example of the the encryption and running your code I got the same result as specified in the guide. – Eddie Mar 03 '23 at 02:32