Search code examples
javascriptencryptionaesrijndael

Encrypt/Decrypt using AES encryption with a Rijndael cipher, ECB, NoPadding in JavaScript


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


Solution

  • 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().