General Purpose Encrypt and Decrypt Functions with Jose

Submitted on Sep 16, 2024, 11:19 p.m.

Here’s a PSA for general-purpose encrypt and decrypt functions using jose that are compatible with Next.js middleware.

According to the documentation: "jose is a JavaScript module for JSON Object Signing and Encryption, providing support for JSON Web Tokens (JWT), JSON Web Signature (JWS), JSON Web Encryption (JWE), JSON Web Key (JWK), JSON Web Key Set (JWKS), and more."

jose is an impressive project, but the documentation can be... well, a little challenging to follow — at least, that was our experience.

Next.js middleware-compatible decryption functions are useful for decrypting session cookies or JWT tokens in middleware-protected routes that require authentication or authorization.

And so here you go...

You can use generateCryptoKey to create a new random CryptoKey and exportKeyToBase64 to export this key to a string that can be stored (securely) somewhere by your app.

// NOTE: encrypt and decrypt functions using jose are Next.js Middleware / Edge safe
// and can be used to retrieve encrypted objects like session cookies.
import { CompactEncrypt, compactDecrypt } from 'jose'
/**
* exportKeyToBase64
*
* @param cryptoKey
* @returns
*/
export async function exportKeyToBase64(cryptoKey: CryptoKey): Promise<string> {
const exported = await crypto.subtle.exportKey('raw', cryptoKey)
const buffer = new Uint8Array(exported)
return Buffer.from(buffer).toString('base64')
}
/**
* importKeyFromBase64
*
* @param base64Key
* @returns
*/
export async function importKeyFromBase64(base64Key: string): Promise<CryptoKey> {
const buffer = Buffer.from(base64Key, 'base64')
const keyData = new Uint8Array(buffer)
return await crypto.subtle.importKey('raw', keyData, { name: 'AES-GCM' }, true, [
'encrypt',
'decrypt',
])
}
/**
* generateCryptoKey
*
* @returns
*/
export async function generateCryptoKey() {
const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, [
'encrypt',
'decrypt',
])
return key
}
/**
* encrypt
*
* @param plaintext
* @param base64SecretKey
* @returns
*/
export async function encrypt(plaintext: string, base64SecretKey: string) {
const secretKey = await importKeyFromBase64(base64SecretKey)
const payload = new TextEncoder().encode(plaintext)
const jwe = await new CompactEncrypt(payload)
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
.encrypt(secretKey)
return jwe
}
/**
* encryptObject
*
* @param obj
* @param base64SecretKey
* @returns
*/
export async function encryptObject(obj: any, base64SecretKey: string) {
return await encrypt(JSON.stringify(obj), base64SecretKey)
}
/**
* decrypt
*
* @param ciphertext
* @param base64SecretKey
* @returns
*/
export async function decrypt(ciphertext: string, base64SecretKey: string) {
const secretKey = await importKeyFromBase64(base64SecretKey)
const { plaintext, protectedHeader } = await compactDecrypt(ciphertext, secretKey)
return new TextDecoder().decode(plaintext)
}
/**
* decryptObject
*
* @param ciphertext
* @param base64SecretKey
* @returns
*/
export async function decryptObject(ciphertext: any, base64SecretKey: string) {
const plaintext = await decrypt(ciphertext, base64SecretKey)
return JSON.parse(plaintext)
}