How to Encrypt / Decrypt with AES (CCM & GCM) in Node.JS

First of all we have to understand what is CCM mode and GCM mode. Roughly:

CCM

CCM mode (Counter with CBC-MAC) is a mode of operation for cryptographic block ciphers. It is an authenticated encryption algorithm designed to provide both authentication and confidentiality. CCM mode is only defined for block ciphers with a block length of 128 bits.

GCM

Galois/Counter Mode (GCM) is a mode of operation for symmetric-key cryptographic block ciphers widely adopted thanks to its performance. GCM throughput rates for state-of-the-art, high-speed communication channels can be achieved with inexpensive hardware resources. The operation is an authenticated encryption algorithm designed to provide both data authenticity (integrity) and confidentiality. GCM is defined for block ciphers with a block size of 128 bits.

These the modes are quite similar to one another but they have specific restrictions and requirements that have to be met before actually trying to encrypt or decrypt anything. I recommend that you read cyrpto’s documentation before implementing the code below, it will help you understand what is happening. In any case there are comments throughout the code to help you as well.

The more you dig into cryptography, the crazier things get. Still fun nonetheless.

Down to the code! You can run the following program in Nodejs (I am running v12.16.1) and see the results.

var crypto = require("crypto");

var key128 = crypto.randomBytes(128 / 8); // 16 bytes
var key192 = crypto.randomBytes(192 / 8); // 24 bytes
var key256 = crypto.randomBytes(256 / 8); // 32 bytes

var iv = "Hook'em Horns"; // The length of the initialization vector (iv) N must be between 7 and 13 bytes (7 ≤ N ≤ 13).
var plainText = "Texas Longhorns"; // The length of the plaintext is limited to 2 ** (8 * (15 - N)) bytes. (Min: 65536 bytes when IV length is 13 bytes, Max: 18446744073709551616 bytes when IV length is 7 bytes)

console.log("\n >>> AES CCM mode demo >>>");

runAESCCMDemo("aes-128-ccm", key128);
runAESCCMDemo("aes-192-ccm", key192);
runAESCCMDemo("aes-256-ccm", key256);
runAESCCMDemo("id-aes128-CCM", key128);
runAESCCMDemo("id-aes192-CCM", key192);
runAESCCMDemo("id-aes256-CCM", key256);

console.log("\n >>> AES GCM mode demo >>>");

runAESGCMDemo("aes-128-gcm", key128);
runAESGCMDemo("aes-192-gcm", key192);
runAESGCMDemo("aes-256-gcm", key256);
runAESGCMDemo("id-aes128-GCM", key128);
runAESGCMDemo("id-aes192-GCM", key192);
runAESGCMDemo("id-aes256-GCM", key256);

function runAESCCMDemo(algorithm, key) {

    try {

        // The options argument controls stream behavior and is optional except when a cipher in CCM or OCB mode is used (e.g. 'aes-128-ccm').
        // The authentication tag length must be specified during cipher creation by setting the authTagLength option and must be one of 4, 6, 8, 10, 12, 14 or 16 bytes.

        var cipher = crypto.createCipheriv(algorithm, key, iv, {
            authTagLength: 16
        });

        var decipher = crypto.createDecipheriv(algorithm, key, iv, {
            authTagLength: 16
        });

        console.log("\n" + algorithm + ':');

        // Encrypting
        var encText = cipher.update(plainText, 'utf8', 'hex');
        encText += cipher.final('hex');
        console.log("E: " + encText);

        const tag = cipher.getAuthTag();
        decipher.setAuthTag(tag);

        // Decrypting
        var decText = decipher.update(encText, 'hex', 'utf8');
        decText += decipher.final('utf8');
        console.log("D: " + decText);

        console.log("MATCH: " + (decText == plainText));

    } catch (e) {

        console.log(e);

    }

}

function runAESGCMDemo(algorithm, key) {

    try {

        // The options argument controls stream behavior and is optional except when a cipher in CCM or OCB mode is used (e.g. 'aes-128-ccm').
        // In GCM mode, the authTagLength option is not required but can be used to set the length of the authentication tag that will be returned by getAuthTag() and defaults to 16 bytes.

        var cipher = crypto.createCipheriv(algorithm, key, iv);
        var decipher = crypto.createDecipheriv(algorithm, key, iv);

        console.log("\n" + algorithm + ':');

        // Encrypting
        var encText = cipher.update(plainText, 'utf8', 'hex');
        encText += cipher.final('hex');
        console.log("E: " + encText);

        const tag = cipher.getAuthTag();
        decipher.setAuthTag(tag);

        // Decrypting
        var decText = decipher.update(encText, 'hex', 'utf8');
        decText += decipher.final('utf8');
        console.log("D: " + decText);

        console.log("MATCH: " + (decText == plainText));

    } catch (e) {

        console.log(e);

    }

}

Your console output should be similar to this:

Z:\Code\NodeJS\AES_CCM_GCM>node aes-ccm-gcm-demo.js

>>> AES CCM mode demo >>>

aes-128-ccm:
E: 24cd20dfeb41b9807ba6fca3fecca6
D: Texas Longhorns
MATCH: true

aes-192-ccm:
E: aaa488514083a0487c6b0477eb229b
D: Texas Longhorns
MATCH: true

aes-256-ccm:
E: eb9c09b15be40aa29909e545749404
D: Texas Longhorns
MATCH: true

id-aes128-CCM:
E: 24cd20dfeb41b9807ba6fca3fecca6
D: Texas Longhorns
MATCH: true

id-aes192-CCM:
E: aaa488514083a0487c6b0477eb229b
D: Texas Longhorns
MATCH: true

id-aes256-CCM:
E: eb9c09b15be40aa29909e545749404
D: Texas Longhorns
MATCH: true

>>> AES GCM mode demo >>>

aes-128-gcm:
E: d5aae385c5f5f7adb72af2e9225743
D: Texas Longhorns
MATCH: true

aes-192-gcm:
E: 473b6cccc4cbd2c0934b15acc0fe31
D: Texas Longhorns
MATCH: true

aes-256-gcm:
E: ade5cd45a8dcecca11693430423615
D: Texas Longhorns
MATCH: true

id-aes128-GCM:
E: d5aae385c5f5f7adb72af2e9225743
D: Texas Longhorns
MATCH: true

id-aes192-GCM:
E: 473b6cccc4cbd2c0934b15acc0fe31
D: Texas Longhorns
MATCH: true

id-aes256-GCM:
E: ade5cd45a8dcecca11693430423615
D: Texas Longhorns
MATCH: true

Z:\Code\NodeJS\AES_CCM_GCM>

Just as a final note. The only difference between runAESCCMDemo() and runAESGCMDemo() methods is that authTagLength is not required at the time of creating cipher and decipher objects.

Sources: