octocat.dev.

A Developer's blog.About

Encrypting Data in .NET and Node with AES GCM

Shubhan Chemburkar
Shubhan Chemburkar
Cover Image for Encrypting Data in .NET and Node with AES GCM

In this post I would like to showcase how to encrypt and decrypt data in .NET and Node.

With front end frameworks supporting server side rendering, it should be possible to decrypt data sent from your API server in encrypted format. This requires support from both .NET and frontend server codebases like Node.

We can encrypt or decrypt data with multiple algorithms. For this post I am using AES GCM that is more secure than other algorithms.

Encryption in .NET

For Encryption, data is in form of ReadOnlySpan<byte>. If you have plain text or serialized JSON data, convert it to bytes using UTF8 encoding.

var data = System.Text.Encoding.UTF8.GetBytes(json);

The encryptionKey is 32 bytes and can also be derived from a user or application secret using any key derivation algorithms like PBKDF2.

For AES GCM, encrypted data bytes will be of same length as the original data.


ReadOnlySpan<byte> EncryptData(ReadOnlySpan<byte> data, ReadOnlySpan<byte> encryptionKey)
{
    // encryptionKey = 32-bytes/ 256 bits
    using (var aes = new AesGcm(encryptionKey))
    {
        // AesGcm.NonceByteSizes.MaxSize = 12 bytes
        // AesGcm.TagByteSizes.MaxSize = 16 bytes
        Span<byte> buffer = new byte[data.Length + AesGcm.NonceByteSizes.MaxSize + AesGcm.TagByteSizes.MaxSize];
        var nonce = buffer.Slice(data.Length, AesGcm.NonceByteSizes.MaxSize);
        RandomNumberGenerator.Fill(nonce);
        aes.Encrypt(nonce, data, buffer.Slice(0, data.Length), buffer.Slice(data.Length + AesGcm.NonceByteSizes.MaxSize, AesGcm.TagByteSizes.MaxSize));
        // buffer has encrypted data bytes + 12 bytes of Nonce + 16 bytes of Tag
        return buffer;
    }
}

Decryption in .NET

For Decryption, we will need the same encryptionKey and the encryptedData. We extract the tag, nonce from the encrypted data and the decrypt the actual plain text data.

ReadOnlySpan<byte> DecryptData(ReadOnlySpan<byte> encryptedData, ReadOnlySpan<byte> encryptionKey)
{
    // encryptedData has encrypted data bytes + 12 bytes of Nonce + 16 bytes of Tag

    var tag = encryptedData.Slice(encryptedData.Length - AesGcm.TagByteSizes.MaxSize, AesGcm.TagByteSizes.MaxSize);
    var nonce = encryptedData.Slice(encryptedData.Length - AesGcm.TagByteSizes.MaxSize - AesGcm.NonceByteSizes.MaxSize, AesGcm.NonceByteSizes.MaxSize);
    var cipherBytes = encryptedData.Slice(0, encryptedData.Length - AesGcm.TagByteSizes.MaxSize - AesGcm.NonceByteSizes.MaxSize);
    Span<byte> buffer = new byte[cipherBytes.Length];
    using (var aes = new AesGcm(encryptionKey))
    {
        aes.Decrypt(nonce, cipherBytes, tag, buffer);
    }
    return buffer;
}

Lets now look at the same code for Node.

Encryption/Decryption in Node

The equivalent of ReadOnlySpan<byte> encryptedData for Node is Buffer. We use the same methodology to encrypt with 12 bytes of nonce and 16 bytes of auth tag.

For this example, I'm using Typescript.

const NonceByteSizesMaxSize = 12
const TagByteSizesMaxSize = 16

We are using the cryptography methods from native crypto module.

import { createDecipheriv, CipherGCMTypes, createCipheriv, randomBytes } from 'crypto';

Encryption

const encryptData = (data: Buffer, encryptionKey: Buffer): Buffer => {

    const algo: CipherGCMTypes = 'aes-256-gcm';

    // 12 bytes of nonce
    var nonce = randomBytes(12); 

    var cipher = createCipheriv(algo, encryptionKey, nonce, { authTagLength: 16 });

    const d1 = cipher.update(data);
    const d2 = cipher.final();
    const encryptedData = Buffer.concat([d1, d2, nonce, cipher.getAuthTag()]);
    return encryptedData;
}

Decryption

const decryptData = (encryptedData: Buffer, encryptionKey: Buffer): Buffer => {
    const tag = encryptedData.subarray(encryptedData.length - TagByteSizesMaxSize, encryptedData.length);
    const nonce = encryptedData.subarray(encryptedData.length - TagByteSizesMaxSize - NonceByteSizesMaxSize, encryptedData.length - TagByteSizesMaxSize);
    const cipherBytes = encryptedData.subarray(0, encryptedData.length - TagByteSizesMaxSize - NonceByteSizesMaxSize);

    const algo: CipherGCMTypes = 'aes-256-gcm';
    var decipher = createDecipheriv(algo, encryptionKey, nonce, { authTagLength: 16 });
    decipher.setAuthTag(tag);

    const d1 = decipher.update(cipherBytes);
    const d2 = decipher.final();
    const data = Buffer.concat([d1, d2]);
    return data;
}

The above code works with modern frameworks like NextJs.

Recomendation

If you are storing any sensitive information that needs to be encrypted at application level or in transport between various layers of the application, we can use the above methods to interchangeably encrypt/decrypt data in .NET and Node.

Discussion

For any queries or feedback, please start a new discussion on GitHub Discussions or at Twitter @shubhan3009.

Cover image Credits

Photo by Towfiqu barbhuiya on Unsplash

The source code for this blog is available on GitHub.