Replacing CryptoJS with Web Cryptography (2024)

in Software Last modified at:

Replacing CryptoJS with Web Cryptography (1)

Alternative title: You Might Not Need CryptoJS. In this post I’ll show how to replace CryptoJS’ AES module with the Web Cryptography API for improved performance and security.

With the spread of Service Worker and PWAs, a growing number of projects will have encrypted client-side storage as a requirement. In this post we’ll examine a commonly used JS cryptography library, do some basic due diligence on it, and then show a path towards replacing it with the browser’s own Web Cryptography API.

  • Basic Due Diligence
  • Replacing CryptoJS
    • Overview
    • The CryptoJS Cipher Prefix
    • Dangerously Deriving Parameters
    • The EVP Key Derivation Function
  • Usage

Basic Due Diligence

CryptoJS is a popular library for doing cryptography in the browser. After doing some basic research, I’m convinced that it is not a production-quality cryptography library. Not by a long shot.

What I found is a library of quasi-unknown origin, maintained by an amateur, unsafe default parameters, no bug bounty program, no published audit of any kind, and no discernable source of income, otherwise known as The Greatest Hits of Soon to be Broken Cryptography Libraries1.

Replacing CryptoJS

In this post we’ll be replacing CryptoJS’ AES module with the Web Cryptography API. The biggest challenge when phasing out CryptoJS is dealing with data previously encrypted by it. We can keep the dependency around for that purpose, but personally I’d rather delete it immediately and use standard APIs for decrypting old data instead.

CryptoJS uses the standard AES-CBC algorithm which also ships as part of the Web Cryptography API. Web Crypto only includes a single padding scheme for non-block-sized payloads, but it’s the same one used by CryptoJS by default.

However it gets more complicated with respect to key derivation. A peak under the hood reveals that the algorithm to derive a key from the passphrase is not standardized and therefore not part of Web Crypto, and it uses the broken MD5 hash that is also not part of Web Crypto. Unfortunately, we’ll have to support this in order to decrypt old data, but we can take some measures to prevent make it harder to abuse.

Overview

Our high level function is going to look like this:

async function decryptCryptoJSCipherBase64(cryptoJSCipherBase64, password, { keySizeInt32 = 256 / 32, ivSizeInt32 = 128 / 32, iterations = 1,} = {}) { const { salt, ciphertext } = parseCryptoJSCipherBase64(cryptoJSCipherBase64); const { key, iv } = await dangerouslyDeriveParameters(password, salt, keySizeInt32, ivSizeInt32, iterations); const plaintextArrayBuffer = await crypto.subtle.decrypt({ name: "AES-CBC", iv }, key, ciphertext); return new TextDecoder().decode(plaintextArrayBuffer);}

A couple of notes:

  • The default parameters are taken from CryptoJS.
  • I assume the cleartext is a string. If not you’ll have to remove the text decoding part.
  • We accept the cipher in base64 because that’s the default returned by CryptoJS’ toString function. You’ll have to adjust the code slightly to accept other formats.
  • I’m using verbose variable names because it’s really easy to mess up types and encodings otherwise.
  • All sizes in this article are given in multiples of 32-bit. I chose this unit because it is also used by CryptoJS, making it easier for the fast-moving npm-hacker to copy-paste the code and get it to work. To communicate this admittedly odd choice to everyone else, I borrow the DWORD designation from Microsoft where it is commonly known to be 32 bits I’ve updated the code to use the Int32 designation instead of DWORD.
  • I follow the React-tried-and-true practice of giving dangerous functions a verbose and uncomfortable name.

The CryptoJS Cipher Prefix

Next we look at parsing the input ciphertext. CryptoJS prefixes the ciphertext with Salted__ (exactly 64 bits), followed by a 64-bit salt.

+----------+----------+------------------------| Salted__ | <salt> | <ciphertext...+----------+----------+------------------------| 64 bit | 64 bit | variable length

To retrieve the salt and ciphertext as Uint8Arrays we use the following. I should note that the following code is mostly extracted from CryptoJS and rewritten using modern JS idioms and APIs, specifically typed arrays.

const HEAD_SIZE_INT32 = 2;const SALT_SIZE_INT32 = 2;function parseCryptoJSCipherBase64(cryptoJSCipherBase64) { let salt; let ciphertext = base64ToUint8Array(cryptoJSCipherBase64); const [head, body] = splitUint8Array(ciphertext, HEAD_SIZE_INT32 * 4); // This effectively checks if the ciphertext starts with 'Salted__'. // Alternatively we could do `atob(cryptoJSCipherBase64.substr(0, 11)) === "Salted__"`. const headDataView = new DataView(head.buffer); if (headDataView.getInt32(0) === 0x53616c74 && headDataView.getInt32(4) === 0x65645f5f) { [salt, ciphertext] = splitUint8Array(body, SALT_SIZE_INT32 * 4); } return { ciphertext, salt };}

I’ll provide the helper functions at the end, but given how verbose the names are, it’s suffice to say that they do exactly what they say they do.

Notice the multiplications by 4 to go from Int32s to bytes, and the offset of 4 on getting the second Int32 value for the same reason.

Dangerously Deriving Parameters

Next we shift our attention towards the enigmatic dangerouslyDeriveParameters function. This is where we take a cryptographically weak passphrase and turn it into a supposedly strong cryptographic key. Given the default parameters this is not actually the case.

async function dangerouslyDeriveParameters(password, salt, keySizeInt32, ivSizeInt32, iterations) { const passwordUint8Array = new TextEncoder().encode(password); const keyPlusIV = dangerousEVPKDF(passwordUint8Array, salt, keySizeInt32 + ivSizeInt32, iterations); const [rawKey, iv] = splitUint8Array(keyPlusIV, keySizeInt32 * 4); const key = await crypto.subtle.importKey("raw", rawKey, "AES-CBC", false, ["decrypt"]); return { key, iv };}
  • Note how both the key and IV are derived from the password. I think this would be okay if the KDF wasn’t so weak and the salt is random and unique, but this is not something the Web Cryptography API’s deriveKey function would support if it implemented the EVPKDF.
  • We only allow the key to be used for decryption. This is to prevent accidental or intentional use for encryption, for which it is too weak.

The EVP Key Derivation Function

Next we turn to the dangerousEVPKDF function. It is the same key derivation function used by OpenSSL (google EVP_BytesToKey for details) and not dangerous by itself, but in the case of hard-coding MD5 as its hash function it certainly is.

This is the only part where we rely on an external dependency, because MD5 is not provided by the Web Cryptography API. I’m using js-md5 here because it supports array buffers, but did not do any research on it otherwise. I figured there’s no such thing as a safe MD5 implementation anyway. You have been warned.

import * as md5 from 'js-md5'function dangerousEVPKDF(passwordUint8Array, saltUint8Array, keySizeInt32, iterations) { let derivedKey = new Uint8Array(); let block = new Uint8Array(); while (derivedKey.byteLength < keySizeInt32 * 4) { block = md5.arrayBuffer(concatUint8Arrays(block, passwordUint8Array, saltUint8Array)); for (let i = 1; i < iterations; i++) { block = md5.arrayBuffer(block); } derivedKey = concatUint8Arrays(derivedKey, new Uint8Array(block)); } return derivedKey;}

Note that this function could be made much more time and space efficient by pre-allocating and reusing typed arrays. However, it was easier to get it right this way, and it should also be easier to follow along.

Usage

That’s it. We can now decrypt data perviously encrypted with CryptoJS at a fraction of the code size, mostly asynchronously, and mostly using fast native code.

import AES from 'crypto-js/aes';const cleartext = "This is a message to be encrypted and decrypted";const password = "passw0rd!";const cryptoJSCipherBase64 = AES.encrypt(cleartext, password).toString();decryptCryptoJSCipherBase64(cryptoJSCipherBase64, password).then(x => { console.log(x); console.log(x === cleartext);});

As promised, here is the link to the full example including utility functions. Keep in mind that these were not written with performance in mind and could all be improved.

In Part II we will deal with deriving a stronger key from the same passphrase and encrypting the data again. This is mostly run-of-the mill Web Crypto code and examples are readily available on the web. However, there’s some specialty involved with respect to dealing with the Salted__ prefix and integrating this nicely with the framework we’ve established in this post.

  1. The fact that it is downloaded close to 1 million times per week speaks volumes to the un-sophistication of the average npm user and the lack of engineering standards in the frontend community at large. There are exceptions however, e.g. A Responsible Developer’s Encounter with CryptoJS in Two Acts.↩︎

Replacing CryptoJS with Web Cryptography (2024)
Top Articles
Choosing an accountant or tax adviser - TaxAid
You may still have to visit your bank branch for making high-value transactions
Calvert Er Wait Time
Durr Burger Inflatable
Canya 7 Drawer Dresser
Fredatmcd.read.inkling.com
Napa Autocare Locator
Celebrity Extra
PRISMA Technik 7-10 Baden-Württemberg
Wmu Course Offerings
Www.craigslist Augusta Ga
Gore Videos Uncensored
DENVER Überwachungskamera IOC-221, IP, WLAN, außen | 580950
What happens if I deposit a bounced check?
Gw2 Legendary Amulet
Ncaaf Reference
Cube Combination Wiki Roblox
Shemal Cartoon
Cnnfn.com Markets
Wildflower1967
Binghamton Ny Cars Craigslist
Dump Trucks in Netherlands for sale - used and new - TrucksNL
Raleigh Craigs List
I Wanna Dance with Somebody : séances à Paris et en Île-de-France - L'Officiel des spectacles
Craiglist Tulsa Ok
Yard Goats Score
Qhc Learning
Panolian Batesville Ms Obituaries 2022
yuba-sutter apartments / housing for rent - craigslist
Rust Belt Revival Auctions
A Christmas Horse - Alison Senxation
Geico Car Insurance Review 2024
Vera Bradley Factory Outlet Sunbury Products
Medline Industries, LP hiring Warehouse Operator - Salt Lake City in Salt Lake City, UT | LinkedIn
Hwy 57 Nursery Michie Tn
My Dog Ate A 5Mg Flexeril
Publix Daily Soup Menu
In Branch Chase Atm Near Me
Whas Golf Card
Ixl Lausd Northwest
Everything You Need to Know About NLE Choppa
Foolproof Module 6 Test Answers
The Vélodrome d'Hiver (Vél d'Hiv) Roundup
San Bernardino Pick A Part Inventory
Doordash Promo Code Generator
Panorama Charter Portal
Disassemble Malm Bed Frame
Powerspec G512
Po Box 101584 Nashville Tn
Rise Meadville Reviews
The Significance Of The Haitian Revolution Was That It Weegy
Bunbrat
Latest Posts
Article information

Author: Dean Jakubowski Ret

Last Updated:

Views: 5828

Rating: 5 / 5 (70 voted)

Reviews: 93% of readers found this page helpful

Author information

Name: Dean Jakubowski Ret

Birthday: 1996-05-10

Address: Apt. 425 4346 Santiago Islands, Shariside, AK 38830-1874

Phone: +96313309894162

Job: Legacy Sales Designer

Hobby: Baseball, Wood carving, Candle making, Jigsaw puzzles, Lacemaking, Parkour, Drawing

Introduction: My name is Dean Jakubowski Ret, I am a enthusiastic, friendly, homely, handsome, zealous, brainy, elegant person who loves writing and wants to share my knowledge and understanding with you.