Skip to Main Content
May 06, 2025

Application Layer Encryption with Web Crypto API

Written by Drew Kirkpatrick
Application Security Assessment

Overview

In web and mobile applications, we’ve been fortunate over the years to have such widespread use of HTTPS by way of TLS. The proliferation of HTTPS is in no small part due to Let’s Encrypt, which provides free and easily automated TLS certificates. In 2013 when Let’s Encrypt was founded, 27% of websites used HTTPS. Today, over 85% of websites are using HTTPS (Electronic Frontier Foundation (EFF) reference). The use of public Wi-Fi often resulted in use of VPNs by the more security minded of us years ago, but with HTTPS in use on most sensitive applications, this is rarely needed these days, even on untrusted networks.

Still, there are scenarios where the robust security of TLS encryption might not be quite enough. Untrusted end-users of an application can easily view and manipulate web traffic before TLS encryption. Intermediary proxies or TLS termination points also may not be trusted. It’s common to perform TLS termination in third-party vendors like Cloudflare, but some applications may consider that undesired exposure of their data.

There’s also the possibility that the server itself doesn’t need to know the unencrypted data and is simply storing or forwarding the data on behalf of the client. Some sneaky individuals may also be obfuscating C2 traffic using application-level encryption :eyes:

These scenarios are where application layer encryption can be useful, and the Web Crypto API is built into web browsers just for this use case and is accessible through JavaScript. It provides several efficient encryption and secure random number generation features to JavaScript code.

A Key Starting Point

Let’s start with a simpler example of using symmetrical keys in Web Crypto. Note that while symmetrical key encryption/decryption is very efficient, you must find a way to securely transmit the key. In the case where the server will never decrypt the traffic and is simply storing or forwarding the encrypted data, symmetrical is likely a fine design choice with the client-side holding onto the key.

A more common use is to leverage public/private keys to establish a secure channel to share a symmetrical key and then shift to using the symmetric key for efficiency. We’ll demo symmetric key usage, public/private key usage, and then the hybrid approach where public/private keys are used to share a symmetric key securely.

For the example of symmetrical keys, we’ll generate an encryption key, encrypt a message, and send it to the server to decrypt and return. For the server to be able to decrypt this, it will need the encryption key, which we’ll simply add to the message for demonstration purposes. In practice, you would have transmitted the key using asymmetric keys or some other mechanism.

In a simple webapp we have an input element where we can enter our message to encrypt. The following JavaScript retrieves that string and encodes it as a stream of bytes (Uint8Array).

Fig1 Kirk Web Crypto

Figure 1 - Converting String Input

Next, we can use the Web Crypto API to generate a symmetric key. If the server provided you a key, you could use the importKey method instead of the generateKey method shown here:

Fig2 Kirk Web Crypto

Figure 2 - Generating Symmetrical Key

There are a few interesting points to note here with the key generation. You can see the true value passed in; this determines if the key is extractable. This means JavaScript can retrieve the encryption key. If you set this to false, JavaScript will no longer be able to retrieve the encryption key value. But it can still use the key. This is an important security consideration, and you likely will want to make your keys not extractable to protect them. This is one of the best features of the Web Crypto API.

Notice also that this key is set up to perform encryption and decryption. You can also set this to be one-way, e.g., encrypt only. A one-way key is useful for thwarting malicious users. If you’re trying to prevent end-users from trivially viewing and modifying parameters, use a non-extractable, encrypt-only key with Web Crypto API. Even if your Web Crypto key object is found in JavaScript, the encrypted message can't be passed to it for decryption if you’ve configured it to only support encryption. This will raise the bar for the malicious user as they’ll have to further reverse engineer the process to capture the original key secret.

Now that we have a symmetrical key configured, we can set up our initialization vector (IV). This is simply a random value.

Fig3 Kirk Web Crypto

Figure 3 - Generating a Random IV

The benefit in symmetrical encryption of an IV is that sending the same message twice does not result in identical ciphertext. By changing the random value of the IV, the ciphertext is changed even if the message is identical. The IV is not secret and can be transmitted in the clear along with the ciphertext.

Now that we have both our IV and symmetric Web Crypto key object, we can encrypt our message.

Fig4 Kirk Web Crypto

Figure 4 - Encrypting Our Message

We can now send this ciphertext to the server that will decrypt the message and return it. Note that we’re sending the encryption key along for demo purposes so the server can decrypt the message—you would not do this in practice.

Fig5 Kirk Web Crypto

Figure 5 - Sending Ciphertext to Server and Receiving Cleartext Back

Fig6 Kirk Web Crypto

Figure 6 - Symmetrical Key Demo

Filling in a string and sending to the server results in this:

Fig7 Kirk Web Crypto

Figure 7 - Decrypted Server Response

A more realistic example is using asymmetric encryption with public/private keys. We’ll first fetch the server’s public key, which is not secret.

Fig8 Kirk Web Crypto

Figure 8 - Retrieving Public Key From Server

You can see we’ve used fetch to get the public key from the server and used the Web Crypto importKey method to create a key object that allows extraction of the key (again, public keys don’t need to be secret), and the object can encrypt data (but not decrypt it). When you’re performing this asymmetric encryption, you can only encrypt with the public key and decrypt with the private. If you want to send encrypted data the other direction, you need to switch who has the public/private key.

Now we can encrypt our encoded message data using that public key.

Fig9 Kirk Web Crypto

Figure 9 - Encrypting Message With Public Key

We can then send out ciphertext to the server, have it decrypt it with the private key, and return to use the cleartext.

Fig10 Kirk Web Crypto

Figure 10 - Sending Encrypted Message

Fig11 Kirk Web Crypto

Figure 11 - Decrypted Server Response

The use of asymmetric is nice in that we no longer have to deal with an IV, and most critically we don’t have to share our secret key, which could compromise the encryption. The downside of course is performance.

Make it Hybrid

If we want the best of both worlds, we go with a hybrid approach. We have our JavaScript client generate its own public/private key pair and send its public key to the server. The server generates a symmetric encryption key, encrypts that key with the client’s public asymmetric key, and sends it to the JavaScript. The client then decrypts the now shared symmetric key and uses it for further encrypted communications.

First in our JavaScript client, we generate the public/private key pair.

Fig12 Kirk Web Crypto

Figure 12 - Create Public/Private Key Pair

Note that this example sets the key as extractable and then uses the exportKey method to extract the public key. Given the short life of this key, as it’s only going to be used to exchange the future symmetric key, this could be a reasonable choice. If you wanted to keep the private key non-extractable, you could generate the key pair outside of the Web Crypto API and import it.

This public key is sent to the server at the exchange-key endpoint.

Fig13 Kirk Web Crypto

Figure 13 - Sending Public Key to Server

The response to that API call is the encrypted symmetric key the server generated. This is decrypted using our private key.

Fig14 Kirk Web Crypto

Figure 14 - Decrypting Symmetric Key

Now that we have a shared symmetric key that both the client and server know, but was kept secret thanks to asymmetric encryption, we can generate a new Web Crypto key object using that key, with extraction disabled, and the key object set up to only encrypt data. We can now securely and efficiently send encrypted data to the server.

Fig15 Kirk Web Crypto

Figure 15 - Importing Symmetric Encryption Key

Fig16 Kirk Web Crypto

Figure 16 - Hybrid Demo

First, we generate our public key and send it to the server to get back our encrypted symmetric key.

Fig17 Kirk Web Crypto

Figure 17 - Hybrid Key Exchange

And now we have a secret, but shared, symmetric key we can efficiently use to communicate with.

Fig18 Kirk Web Crypto

Figure 18 - Secure Use of Symmetric Key Encryption

Wrap-Up

Shown in this blog were some of the ways to use the Web Crypto API for application-level encryption. This is not something we see commonly, as for most applications TLS encryption is more than sufficient. But if you’re trying to hide data from your users or intermediary proxies, this is certainly a good approach to consider.

Remember that a browser will typically only allow Web Crypto API in secure contexts, either HTTPS or localhost. The demo server runs HTTP but will be trusted to use Web Crypto if being accessed from localhost. The demo server repo can be found here:

https://github.com/hoodoer/Web_Crypto_API_Demo

To install and run the demo server:

mkdir webCryptDemo
python3 -m venv webCryptoDemo
source webCryptoDemo/bin/activate
cd webCryptoDemo
git clone https://github.com/hoodoer/Web_Crypto_API_Demo.git
cd Web_Crypto_API_Demo
pip3 install -r requirements.txt
python3 server.py

Once the server is running the demo app can be accessed at:

http://127.0.0.1:5000

The reference for the Web Crypto AP can be found here:

https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API