Microlearning: NestJS and JWE - How to use it

Learn how to secure data using JSON Web Encryption (JWE) in NestJS. This post compares JWE and JWT, focusing on data encryption and confidentiality. It provides a practical guide on setting up the node-jose library, managing keys, and implementing encryption and decryption services in NestJS.

Microlearning: NestJS and JWE - How to use it
Photo by Kelly Sikkema / Unsplash

JWE (JSON Web Encryption) is a compact, URL-safe means of representing encrypted content using JSON-based data structures. It is a standard for securing data by encrypting it, ensuring that only authorized parties can read the data. JWE is part of the larger suite of JSON Object Signing and Encryption (JOSE) technologies, which also includes JSON Web Signature (JWS) and JSON Web Token (JWT).

JSON Object Signing and Encryption (JOSE) Knowledge Graph

We can do a small and fast comparison between JWT (most common option) and JWE:

Feature/Aspect JWT (JSON Web Token) JWE (JSON Web Encryption)
Purpose Authentication and information exchange Data encryption to ensure confidentiality
Structure Header, Payload, Signature Protected Header, Encrypted Key, Initialization Vector (IV), Ciphertext, Authentication Tag
Security Data integrity and authenticity Confidentiality, integrity, and authenticity
Data Visibility Payload is base64-url encoded and signed, but not encrypted Payload is encrypted and thus not readable without decryption
Use Cases - Authentication tokens (e.g., OAuth) - Encrypting sensitive data (e.g., personal or financial information)
- Information exchange where data needs to be verified - Any scenario where data privacy and protection are crucial
- Stateless sessions

To start working with JWE in NestJS (or any other Node.JS framework) we can use the node-jose library. It's really simple to use, and this library implements (wherever possible) all algorithms, formats, and options in JWS, JWE and JWT, and uses native cryptographic support.

yarn add node-jose
yarn add @types/node-jose -D

Now we need to create a new service to hold our code:

nest g service Encryption
import { Injectable } from '@nestjs/common';
import { JWE, JWK } from 'node-jose';
import { readFileSync } from 'fs';
import { join } from 'path';

@Injectable()
export class EncryptionService {
  // Create a new keystore, which will hold the symmetric( or asymmetric) key.
  private keystore: JWK.KeyStore;

  constructor() {
    // Initialize the keystore
    this.keystore = JWK.createKeyStore();

    // Load the symmetric key into the keystore
    this.loadKey();
  }

  /**
   * Reads the symmetric key from a file and adds it to the keystore.
   */
  async loadKey() {
    try {
      const key = await this.getSymmetricKey();

      await this.keystore.add(key);
    } catch (e) {
      console.log('Error adding key');
    }
  }

  /**
   * Reads the key from a file and returns it as an object,
   * which can be added to the keystore.
   *
   * @returns symmetric key as an object
   */
  async getSymmetricKey() {
    try {
      const path = join(process.cwd(), 'src/encryption/key.json');

      const key = readFileSync(path, 'utf8');

      return JSON.parse(key);
    } catch (e) {
      console.log('Error reading key file');
    }
  }

  /**
   * Encrypts data using the first key in the keystore,
   * which should be the symmetric key used for encryption,
   * and returns the encrypted data in JWE format.
   *
   * @param payload Data to encrypt
   * @returns encrypted data in JWE format (JSON Web Encryption) as string
   */
  async encrypt(payload: object) {
    // Get the first key in the keystore
    const key = this.keystore.all({ use: 'enc' })[0];

    // Convert the payload to a string
    const input = JSON.stringify(payload);

    // Encrypt the payload using the key
    const encrypted = await JWE.createEncrypt({ format: 'compact' }, key)
      .update(input)
      .final();

    // Return the encrypted data
    return encrypted;
  }

  /**
   * Decrypts the token using the keystore and returns the decrypted payload.
   * @param token Encrypted data in JWE format
   * @returns decrypted payload as an object
   */
  async decrypt(token: string) {
    const decrypted = await JWE.createDecrypt(this.keystore).decrypt(token);
    const payload = JSON.parse(decrypted.payload.toString());
    return payload;
  }
}

With this code we can: read a key in json format (used to encrypt and decrypt the data) encrypt and decrypt the payload.

I put some comments in the code to explain each methods.

My demo key (symmetric key):

{
  "kty": "oct",
  "k": "k1JnWRfC-5zzmL72vXIuBgTLfVROXBakS4OmGcrMCoc",
  "alg": "A256GCM",
  "use": "enc"
}

Now we can create two routes to test the service:

  @Post('encrypt')
  async encrypt(@Body() payload: object) {
    return await this.encryptionService.encrypt(payload);
  }

  @Post('decrypt')
  async decryptData(@Body() payload: any): Promise<object> {
    return this.encryptionService.decrypt(payload.token);
  }

We can test and see the encryption working:

On the left is the payload to encrypt and on the right we get the encrypted token.

And the decryption:

On the left, we get the encrypted token and on the right, the payload is decrypted.

This is a simple but very useful implementation of JWE on NestJS. Do you have any questions about this or what to say something about this new format of post? Let me know!

This post is part of a serie called "microlearning", check more about what is that here.