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.
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).
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:
And the decryption:
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.