UNPKG

fernet-web

Version:

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

235 lines (202 loc) 8.19 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 { private signingKey: CryptoKey; private encryptionKey: CryptoKey; /** * 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. */ private constructor(signingKey: CryptoKey, encryptionKey: CryptoKey) { 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. */ private static async initializeKeys(secretKeyBuffer: Uint8Array): Promise<{ signingKey: CryptoKey, encryptionKey: CryptoKey }> { 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. */ public static async create(secretKey_b64: string | null): Promise<Fernet> { 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. */ public async encrypt(plainText: string): Promise<string> { 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: string[] = []; 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. */ public async decrypt(token_b64: string): Promise<string> { // 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: string) { super(message); } }