Advanced Encryption Standard

Cipher Block Chaining Mode

Part 4. AES CBC Decryption

To decrypt data protected by techniques of symmetric cryptography, a recipient of the data must possess both the secret key and the initialization vector used during encryption. A binary buffer encrypted by the client and sent to a remote server can be deciphered on the server side after the key has been shared with the server in a secure way.

If the encrypted data is kept as a local file, it can be restored to plaintext with the help of scripting languages and desktop tools. Here's a snippet of Python code parsing the file saved as XML in the previous AES example and extracting ciphertext in hexadecimal notation:

import xml.etree.ElementTree
eTree=xml.etree.ElementTree.parse("document.xml")
file=eTree.find("file")
print file.text

The output of the console application above is redirected to a new file storing ciphertext as a hexadecimal string:

python extract-encrypted-file.py>hex.dat

The hex-to-binary conversion can be performed by one of command line tools, e.g. certutil from the suite of Windows utilities:

certutil -decodehex hex.dat ciphertext.dat

If the file owner knows hexadecimal representation of both the secret key and the initialization vector he can decipher the encrypted file with openssl:

openssl AES-256-CBC -in ciphertext.dat -out plaintext.txt -d -K A76CDA8862F656E3A577D8C9880E0A9053CC5E2D708D1C8FD8DC58112D8B1B76 -iv 0BA2D62D64CC33BF3AF33F4931FF7554

AES CBC Decryption in JavaScript

Let's assume that the secret key created in the example from the previous article is kept locally, and the XML file containing the encrypted data is stored on the server. For CBC decryption, our code will obtain the XML document as an HTTP response payload, then import the key and decrypt the file.

The document is loaded by using a conventional XMLHttpRequest with the document response type:

var xhr=new XMLHttpRequest();
xhr.onreadystatechange=parseMarkup;
xhr.open("GET", "http://example.com/document.xml");
xhr.responseType="document";
xhr.send();

The parseMarkup handles the changes of the XHR readyState property. If the request status is OK, then the obtained XML document is parsed to extract the encrypted file and the accompanying data:

function parseMarkup(event) {
 if(event.target.readyState==4) {
  if(event.target.status==200) {
   var xmlDocument=event.target.responseXML;
   var fileName=xmlDocument.documentElement.firstChild.textContent;
   var ciphertext=hex2abv(xmlDocument.getElementsByTagName("file")[0].textContent);
   var iVector=hex2abv(xmlDocument.documentElement.lastChild.textContent);
   importKeyAndDecryptFile(fileName, ciphertext, iVector);
  }
 }
}

The hex2abv is a helper function for converting hexadecimal strings into ArrayBufferView objects:

function hex2abv(hex) {
 var abv = new Uint8Array(hex.length / 2);
 for (var i=0; i<abv.length; ++i) {
  abv[i] = parseInt(hex.substr(2*i, 2), 16);
 }
 return abv;
}

The importKeyAndDecryptFile function performs the following operations: first it prompts the user to select the local file containing the secret key, then it imports the key and decrypts the ciphertext obtained in the code above:

function importKeyAndDecryptFile(fileName, ciphertext, iVector) {
 var keySelector=document.querySelector("input[name=\"key-selector\"]");
 if(keySelector.files.length==0) {
  alert("Please select your secret key to decrypt the downloaded file");
 } else {
  var keyFile=keySelector.files[0]; // binary file with the key material
  var fileReader=new FileReader();
  fileReader.onload=function(loadEvent) { // file is read without errors
   var opaqueKey=loadEvent.target.result; // secret key as ArrayBuffer
   crypto.subtle.importKey("raw", opaqueKey, {name: "AES-CBC"}, isExtractable, keyOperations).then(
    function(key) { // the key is represented as an instance of Key/CryptoKey
     console.info("The secret key has been imported successfully.");
     var aesCbcParams={name: algorithm.name, iv: iVector};
     crypto.subtle.decrypt(aesCbcParams, key, ciphertext).then(
      function(plaintext) { // plaintext as ArrayBuffer
       console.info("File '"+fileName+"' has been decrypted.");
       saveDecryptedFile(plaintext, fileName);
      },
      fileDecryptionFailure
     );
    },
    function(eObj) {
     console.info("The secret key cannot be imported: "+eObj.message.toLowerCase()+".");
     console.error(eObj);
    }
   );
  };
  fileReader.readAsArrayBuffer(keyFile);
 }
}

The importKey() function has accepted five arguments: the key type (raw); the key represented as an ArrayBuffer; an instance of AlgorithmIdentifier ({name: "AES-CBC"}); two values utilized during the AES key generation (boolean isExtractable and keyOperations array).

If the Promise to import the key is fulfilled, the success handler receives the key as a Key/CryptoKey object.

The decrypt() function makes use of the same AesCbcParams object as was employed for encryption. The other arguments of the decrypt() are the AES key and ciphertext: the latter is exposed as an ArrayBufferView.

The fileDecryptionFailure is a named handler of the Promise rejected state; it can contain any code dealing with decryption errors:

function fileDecryptionFailure(eObj) {
 console.info("File decryption has failed: "+eObj.message.toLowerCase()+".");
 console.error(eObj);
}

The deciphered data is returned as an ArrayBuffer. It can be transformed into the primary format of the encrypted file:

function saveDecryptedFile(plaintext, fileName) {
 if(fileName.endsWith(".pdf")) {
  var file=new File([plaintext], fileName, {type: "application/pdf"});
  var link=document.createElement("a");
  link.href=URL.createObjectURL(file);
  link.download=fileName;
  link.textContent="save "+fileName;
  document.body.appendChild(link);
 }
}