Web Cryptography

Hash-Based Message Authentication Code

HMAC is a mechanism for message authentication based on the use of cryptographic hash algorithms. HMAC can be employed with any iterative hash function in combination with a secret key. The key is shared by parties participating in the exchange of security-sensitive data.

Web Cryptography implementation in major browsers supports hash-based MACs: the recognized algorithm name for calculating message authentication codes is HMAC. There are five operations available to a JavaScript application dealing with keyed hashing: generation of an HMAC key; the export of the key material converting the secret key to the opaque sequence of bytes or to a JSON data structure; the import of an external key; generation of an HMAC (in other words - message signing); verification of the previously generated authentication code.

HMAC Key Generation

The generateKey() method of the SubtleCrypto accepts the following arguments required to create an HMAC key:

  • an HmacKeyGenParams object with the name, hash and length properties; the hash is another object exposing the name of the associated hash function; the length sets the key size in bits; if the parameter is omitted, a default value is used: the value is equal to the block size of the inner hash function;
  • a boolean value reflecting the "exportability" of the key;
  • an array of key usage values; the allowed elements of the array for HMAC keys are 'sign' and 'verify'.

The code below declares key generation parameters for a key of 512 bits:

var hmacKeyGenParams = {
 name: 'HMAC',
 hash: {
  name: 'SHA-256'
 }
};
var extractable = true;
var keyUsage = ['sign', 'verify'];

The same code might be rewritten to specify the key length explicitly:

var hmacKeyGenParams = {
 name: 'HMAC',
 hash: {
  name: 'SHA-256'
 },
 length: 512
};
var extractable = true;
var keyUsage = ['sign', 'verify'];

Creating a secret key is a Promise-based operation:

crypto.subtle.generateKey(hmacKeyGenParams, extractable, keyUsage).then(keyGenSuccess, keyGenFailure);

Two named handlers monitor the state of the Promise:

function keyGenSuccess(key) {
 console.info('The key has been generated successfully');
 console.dir(key);
}

function keyGenFailure(e) {
 console.log('%cKey generation has failed: %s', 'color: red', e.message.toLowerCase());
}

If the Promise is fulfilled, the dir() method of the global console object will print out a JavaScript representation of the generated CryptoKey:

algorithm: Object
 hash: Object
  name: "SHA-256"
 length: 512
 name: "HMAC"
extractable: true
type: "secret"
usages: Array[2]
 0: "sign"
 1: "verify"

The properties of the CryptoKey hold information about parameters specified during its creation.

Key creation failure is handled by the function showing the reason of the error, e.g. an attempt to generate a key with a hash algorithm not supported by the cryptographic engine of the browser will result in the following error message:

Key generation has failed: an invalid or illegal string was specified

HMAC Key Export

The CryptoKey can be immediately employed by the message originator to compute an HMAC of a message - a file or some other type of binary data. The message originator is a party of the protected data exchange that applies the secret key to a message to produce a MAC. Authenticating the source of the message and its integrity, however, will prove useless if a prospected receiver of the message does not possess the same secret key. To pass the generated keying material to a receiving party, the originator must first transform the CryptoKey into a format suitable for the network transfer, persistent storage or further cryptographic protection.

The exportKey() method accepts the string designation of the key format and the CryptoKey object as its arguments. For secret keys the primary encoding format is 'raw':

crypto.subtle.exportKey('raw', key).then(keyExpSuccess, keyExpFailure);

function keyExpSuccess(buffer) {
 console.info('The key of %i bytes has been exported successfully', buffer.byteLength);
}

function keyExpFailure(e) {
 console.log('%cThe key cannot be exported: %s', 'color: red', e.message.toLowerCase());
}

The raw key is returned as an ArrayBuffer. The buffer can be converted to other formats, e.g. a hexadecimal string:

var abv = new Uint8Array(buffer);
var arr = Array.from(abv);
arr.forEach((e, i, a) => {
 a[i] = new Number(e).toString('16');
 if(a[i].length == 1){
  a[i] = '0' + a[i];
 }
});
var hex = arr.join('');

The buffer-to-hex conversion above is based on the array functions introduced by ECMAScript 6. The static from() accepts an iterable object and returns an instance of JavaScript Array. The forEach() method performs an action for each element in an array. A callback passed to the forEach() can be declared with up to three parameters: the value of the current array element, the index of the processed element, and the array that is being iterated.

The callback shown in the example above is a lambda function modifying the original array: a sequence of numbers is replaced with a sequence of hexadecimal strings. The strings are then concatenated by calling the join() method.

The generated key can be saved to a local file:

var blob = new Blob([buffer]);
var url = URL.createObjectURL(blob);
var link = document.createElement('a');
link.href = url;
link.download = 'secret-key.dat';
link.textContent = 'download HMAC key';
document.body.appendChild(link);

JSON Web Key

An alternative way of exporting a CryptoKey is its conversion to a JSON Web Key object:

crypto.subtle.exportKey('jwk', key).then(keyExpSuccess, keyExpFailure);

function keyExpSuccess(jwk) {
 console.info('The key has been exported successfully');
 console.dir(jwk);
}

The exported key has the following structure:

{
 alg: "HS256",
 ext: true,
 k: "tBrDrMNe2L8JSOgNSZpQQKDgfC5I9eIdDNUJmShnAyuhk3TjqGH6tBKFs8nAEJkyCWI36oeQgOg1tOXO0OEQ2A",
 key_ops: Array[2],
  0: "sign"
  1: "verify"
 kty: "oct"
}

The alg property designating a cryptographic algorithm has the value of HS256: in the list of JSON Web Algorithms this is the abbreviated form of the "HMAC using SHA-256" . The other possible values for HMACs based on the hash functions from the SHA-2 family are HS384 and HS512.

The ext and key_ops properties show the key exportability and limitations imposed on its usage.

The key type property, or kty, declares the key as symmetric: the oct stands for "octet sequence".

The k is the key itself. Its representation is founded on the Base64 encoding with an URL and filename safe alphabet.

HMAC Key Import

If exporting converts a CryptoKey to a binary buffer or a JSON object, then the import of HMAC keys is the reverse operation: the client-side code parses key material previously exported or created by an external application and obtains a valid CryptoKey.

For illustration, the keying material in the example below will be generated by a Java application: first the program will create a class with members equivalent to the properties of the JSON Web Key, then an instance of the class will be converted to its JSON representation with the help of the Gson library.

The class creating a 512-bit HMAC key uses Java engine classes from the javax.crypto package. The Base64 is a utility class from Apache Commons Codec:

class HMACKeyGenerator {
 String alg = "HS256";
 boolean ext = true;
 String k = "";
 String[] key_ops = {"sign", "verify"};
 String kty = "oct";
 public HMACKeyGenerator() {
  try {
   KeyGenerator keyGenerator = KeyGenerator.getInstance("HmacSHA256");
   keyGenerator.init(512);
   SecretKey secretKey = keyGenerator.generateKey();
   byte[] key = secretKey.getEncoded();
   k = Base64.encodeBase64URLSafeString(key);
  } catch (Exception e) {
   System.err.println("The requested operation cannot be performed");
  }
 }
}

A 64-byte key is generated when an instance of the class above is created. If the operation proves successful, the object with its properties can be passed to the toJson() method of a Gson:

HMACKeyGenerator keyGen = new HMACKeyGenerator();
if(keyGen.k != "") {
 Gson gson = new Gson();
 String jwk = gson.toJson(keyGen);
 try(FileWriter fileWriter = new FileWriter("jwk.json")) {
  fileWriter.write(jwk);
 }
}

The key saved as a JSON file is loaded by the client application:

input element for key selection
<input type = 'file' name = 'key-selector' onchange = 'readKey(this.files[0])'>

client script
function readKey(jwkFile) {
 var fileReader = new FileReader();
 fileReader.onload = function(loadEvent) {
  var txt = loadEvent.target.result;
  var jwk = JSON.parse(txt);
  console.info("The JSON Web Key is loaded and parsed");
  importKey(jwk);
 };
 fileReader.readAsText(jwkFile);
}

If the file is read without errors, the load event is dispatched. The event handler attached to the FileReader object gets the file contents as string. The string is passed to the static parse() method of the global JSON object. The resultant JSON Web Key is now ready for import:

function importKey(jwk) {
 var hmacKeyImportParams = {
  name: 'HMAC',
  hash: {
   name: 'SHA-256'
  },
  length: 512
 };
 var extractable = true;
 var keyUsage = [
  'sign', 'verify'
 ];
 crypto.subtle.importKey('jwk', jwk, hmacKeyImportParams, extractable, keyUsage).then(
  key => {
   console.info('The key has been imported successfully');
   console.dir(key);
  },
  e => {
   console.log('%cThe key cannot be imported: %s', 'color: red', e.message.toLowerCase());
  }
 );
}

The importKey() method of the SubtleCrypto accepts a few arguments: the 'jwk' key format; key data represented as the JSON Web Key; an HmacImportParams object exposing such properties as name, hash and length; the boolean value showing whether the key is extractable; the array of two elements specifying the allowed key usage.

If the Promise to import the key is fulfilled, the Promise state handler obtains an instance of the CryptoKey. Both success and error handlers are implemented as arrow functions.

HMAC Generation

After the key has been generated or imported, it can be used to compute a MAC of a message. The message must be converted to an ArrayBuffer or a typed array:

var msg = Array.from('message');
msg.forEach((e, i, a) => {
 a[i] = e.charCodeAt(0);
});
var message = new Uint8Array(msg);

Web Cryptography API employs the sign() method to calculate an HMAC:

var algorithm = {
 name: 'HMAC'
};
crypto.subtle.sign(algorithm, key, message).then(
 hmac => {
  console.info('The hash-based MAC of %d bytes has been computed', hmac.byteLength);
 },
 e => {
  console.log('%cThe requested operation cannot be performed: %s', 'color: red', e.message.toLowerCase());
 }
);

The HMAC is returned as an ArrayBuffer. The buffer should be converted to a more user-friendly format:

var abv = new Uint8Array(hmac);
var arr = Array.from(abv);
arr.forEach((e, i, a) => {
 a[i] = new Number(e).toString('16');
 if(a[i].length == 1) {
  a[i] = '0' + a[i];
 }
});
var hex = arr.join('');

The same hexadecimal HMAC of the message would be computed with the OpenSSL dgst command:

export key=`cat hexkey.dat`
echo -n "message" | openssl dgst -sha256 -mac hmac -macopt hexkey:$key

HMAC Verification

The computed HMAC is not kept secret: it can be appended to the message and passed to a receiving party. Let's assume the message from the example above and its authentication code are saved as an XML file:

<?xml version = "1.0" encoding = "UTF-8"?>
 <message>
  <hmac> . . . hex HMAC . . . </hmac>
  <data>message</data>
 </message>

The receiving party already possessing the secret key can load the file through the use of the XMLHttpRequest API:

var xhr=new XMLHttpRequest();
xhr.onload = () => {
 if(xhr.status == 200) {
  var xmlDoc = xhr.response;
  var hmac = hex2abv(xmlDoc.querySelector('hmac').textContent);
  var message = str2abv(xmlDoc.querySelector('data').textContent);
  . . .
 }
};
xhr.open('GET', 'http://example.com/docs/message.xml');
xhr.responseType = 'document';
xhr.send();

Two auxiliary functions performs preliminary conversions necessary to represent both HMAC and the message as typed arrays:

function hex2abv(hex) {
 var arr = new Array();
 for(var i = 0; i < hex.length; i += 2) {
  arr.push((hex.substr(i, 2)));
 }
 arr.forEach((e,i,a) => {
  a[i] = Number.parseInt('0x' + e);
 });
 var abv = new Uint8Array(arr);
 return abv;
}

function str2abv(msg) {
 var arr = Array.from(msg);
 arr.forEach((e,i,a) => {
  a[i] = e.charCodeAt(0);
 });
 var abv = new Uint8Array(arr);
 return abv;
}

The verify() method accepts the algorithm identifier, the shared key, the HMAC and the message which integrity is to be checked:

var algorithm = {
name: 'HMAC'
};
crypto.subtle.verify(algorithm, key, hmac, message).then(
 result => {
  if(result == true) {
   console.log('%cThe message integrity is confirmed', 'color:blue');
  } else {
   console.log('%cAttention: the message integrity cannot be verified!', 'color: red; text-decoration:underline');
  }
 },
 e => {
  console.log('%cThe requested operation cannot be performed: %s', 'color: red', e.message.toLowerCase());
 }
);


Hash Functions

The table below shows the basic characteristics of hash functions supported by the current implementations of the Web Cryptography API:

  • the standard name of a function is employed to create HmacKeyGenParams and HmacKeyImportParams objects;
  • the default length of the generated key is the block size of the inner hash algorithm;
  • a computed HMAC returned as an ArrayBuffer has the byteLength property coinciding with the digest size of the associated hash function.
Algorithm Block Size Digest Size
SHA-1 512 bits 20 bytes
SHA-256 512 bits 32 bytes
SHA-384 1024 bits 48 bytes
SHA-512 1024 bits 64 bytes