UNPKG

fernet-web

Version:

A library that implements the Fernet algorithm using the Web Crypto API.

172 lines (171 loc) 7.41 kB
/** * @author Rich Miles * @date December 05, 2022 * * This code provides an implementation of the Fernet symmetric encryption algorithm * using the Web Crypto API. * * @warning This code is provided as is, without any warranty or guarantees. Use at your own risk. * * @example * // Create a new Fernet instance using a secret key * const fernet = await Fernet.create("-lf4DsgLkOaE1GbtIQKNGU1NPQByMDKP2a6Enl9rclE="); * * // Encrypt a message * const encryptedToken = await fernet.encrypt("Hello world!"); * * // Decrypt the encrypted message * const decryptedMessage = await fernet.decrypt(encryptedToken); * * // Print the decrypted message * console.log(decryptedMessage); // Hello world! */ /** * Fernet is a simple and secure way to encrypt and decrypt messages using symmetric encryption. */ export default class Fernet { /** * Create a new Fernet instance with the provided signing and encryption keys. * @param signingKey - A CryptoKey to use for signing messages. * @param encryptionKey - A CryptoKey to use for encrypting and decrypting messages. */ constructor(signingKey, encryptionKey) { this.signingKey = signingKey; this.encryptionKey = encryptionKey; } /** * Generate signing and encryption keys from a secret key. * @param secretKeyBuffer - A Uint8Array containing the secret key to use. * @returns A tuple containing the signing and encryption keys. */ static async initializeKeys(secretKeyBuffer) { const signingKeyBuffer = secretKeyBuffer.slice(0, 16); const encryptionKeyBuffer = secretKeyBuffer.slice(16); const signingKey = await crypto.subtle.importKey("raw", signingKeyBuffer, { name: "HMAC", hash: "SHA-256", }, false, ["verify", "sign"]); const encryptionKey = await crypto.subtle.importKey("raw", encryptionKeyBuffer, "AES-CBC", false, ["encrypt", "decrypt"]); return { signingKey, encryptionKey }; } /** * Create a new Fernet instance with a secret key. * @param secretKey_b64 - A base64-encoded string representation of the secret key to use. * If null, a new secret key will be generated. * @returns A new Fernet instance. */ static async create(secretKey_b64) { var secretKeyBuffer = new Uint8Array(32); if (secretKey_b64 != null) { // Decode the URL-safe base64-encoded secret key secretKeyBuffer = Uint8Array.from(window.atob(secretKey_b64.replace(/_/g, '/').replace(/-/g, '+')), c => c.charCodeAt(0)); } else { // Create a new secret key from scratch to use for this session crypto.getRandomValues(secretKeyBuffer); } const keys = await Fernet.initializeKeys(secretKeyBuffer); return new Fernet(keys.signingKey, keys.encryptionKey); } /** Encrypt a message into a Fernet token. @param plainText - The message to encrypt. @returns A base64-encoded Fernet token. */ async encrypt(plainText) { const plainTextBuffer = new TextEncoder().encode(plainText); const ivBuffer = new Uint8Array(16); // create a new array to store the IV // generate the random IV crypto.getRandomValues(ivBuffer); // Encrypt the plain text const encryptedMessage = await crypto.subtle.encrypt({ name: "AES-CBC", iv: ivBuffer }, this.encryptionKey, plainTextBuffer); // Version is always the first byte and always 128 (0x80) const version = new Uint8Array([0x80]); // The time bytes are a big-endian encoded uint64 of unix epoch time (seconds) const timeBuffer = new ArrayBuffer(8); const view = new DataView(timeBuffer); const currentTime = Date.now(); const unixEpochTime = Math.round(currentTime / 1000); view.setBigUint64(0, BigInt(unixEpochTime), false); const timestamp = new Uint8Array(timeBuffer); // Convert the IV to a Uint8Array const iv = new Uint8Array(ivBuffer); // Convert the encrtyped message to a Uint8Array const ciphertext = new Uint8Array(encryptedMessage); // Concatenate the unsigned component const unsigned_token = new Uint8Array([ ...version, ...timestamp, ...iv, ...ciphertext, ]); // Compute the HMAC signature const hmac = new Uint8Array(await crypto.subtle.sign({ name: "HMAC", hash: "SHA-256", }, this.signingKey, unsigned_token)); // Concatenate the header with the signature const signed_token = new Uint8Array([ ...version, ...timestamp, ...iv, ...ciphertext, ...hmac ]); // Convert the signed token array to a URL-Safe base64 string // Note: The signed_token array is a Uint8Array, but the btoa function expects a string. // Originally, this was using String.fromCharCode(signed_token), but that was causing // a stack overflow with large arrays. This is a workaround that avoids the stack overflow. let tokenChars = []; signed_token.forEach(byte => { tokenChars.push(String.fromCharCode(byte)); }); const fernetToken = window.btoa(tokenChars.join("")) .replace(/\+/g, "-") .replace(/\//g, "_"); return fernetToken; } /** * Decrypt a Fernet token and return the plain text message. * @param token_b64 - A base64-encoded Fernet token. * @returns The decrypted message as a string. */ async decrypt(token_b64) { // Decode the base64-encoded secret key // Convert the URL-safe base 64 value to a Uint8Array const tokenBuffer = Uint8Array.from(window.atob(token_b64.replace(/_/g, '/').replace(/-/g, '+')), c => c.charCodeAt(0)); // Decompose the token into its various parts const version = tokenBuffer.slice(0, 1); const timestamp = tokenBuffer.slice(1, 9); const iv = tokenBuffer.slice(9, 25); const ciphertext = tokenBuffer.slice(25, -32); const hmac = tokenBuffer.slice(-32); // Decrypt the cipher text const decrypted = await crypto.subtle.decrypt({ name: "AES-CBC", iv: iv, }, this.encryptionKey, ciphertext); const decryptedMessage = new TextDecoder().decode(decrypted); // Verify the HMAC signature const unsigned_token = new Uint8Array([ ...version, ...timestamp, ...iv, ...ciphertext, ]); const verification = await crypto.subtle.verify({ name: "HMAC", hash: "SHA-256" }, this.signingKey, hmac, unsigned_token); if (!verification) { throw new InvalidTokenError("Token is invalid or has been tampered with"); } return decryptedMessage; } } /** * The InvalidTokenError class is used to indicate that a Fernet token failed to verify. */ class InvalidTokenError extends Error { /* Create a new InvalidTokenError instance. @param message - A string describing the error that occurred. */ constructor(message) { super(message); } }