DEV Community

Cover image for End-to-end encryption in the browser Part 1
Jakub Juszczak
Jakub Juszczak

Posted on

End-to-end encryption in the browser Part 1

What a time to be alive. We are living in an era where AI is on the rise, huge data breaches occur on a weekly basis and ransomeware is a daily threat.

Each and every service and app requires us to provide tons of personal information, which quite often can be found in the darknet a few months later. Data protection is a key here.

I guess nearly everyone heard of for example PGP and encryption. But to be honest PGP is kinda weird to use and not for the average user.

So, we have cool algorithms like RSA, AES (in various forms) and newer ones like ChaChaPoly. So we have the ability to protect data. But why are so few apps using this?

From one end to the other

So let's introduce this weird thing called End-to-end encryption (E2EE).

If we take a look at wikipedia it describes E2EE:

End-to-end encryption (E2EE) is a private communication system in which only communicating users can participate. As such, no one, including the communication system provider, telecom providers, Internet providers or malicious actors, can access the cryptographic keys needed to converse

So it is rather a broad definition. I've looked at many apps that advertised E2EE and trust me, companies have a very broken definition on what E2EE really is.

For some people like me, it is if you encrypt your data so only the other person can decrypt and see it.

For other people SSL/TLS is also E2EE. 🤡

Web Crypto API

But let's assume you are like me. And want real encryption. There are many libs out there that can help you with that. We have encryption libraries for nearly all languages: PHP, Ruby, Python, C++ and so on.

If we are in the web, we have a challenge. If our PHP backend is encrypting the data, we still send it cleartext to the server. The server could be compromised (now or later). And how to we even manage all the keys?!

To be really secure, we should only rely on clientside encryption. Where all the data is encrypted on the device and send encrypted to the server. No middleman!

And we even have official browser APIs for this. The Web Crypto API

And don't be afraid of encryption. It is not that hard! I promise!

Symmetric Encryption

There are different systems that we can use. One is Symmetric-key Encryption

It is quite easy to understand. You have a shared key, that you and your buddy know. And you encrypt your message with that key. Your buddy can then decrypt the message with the key and read it.

AES-GCM

A quite common standard is AES. It has different modes and all have advantages and disadvantages, but we won't go too much into detail here.

We will focus on AES-GCM in this example. Because for our use case it will be the most secure.

The steps we have to take are simple.

  1. Generate AES key
  2. Encrypt our data
  3. ???
  4. Safety!!

Lets start with the first step. Key generation:
(For the sake of sanity we will write some helper functions)

async generateAESKey (): Promise<CryptoKey> {
        return await window.crypto.subtle.generateKey(
            {
                name: 'AES-GCM',
                length: 256
            },
            true,
            ['encrypt', 'decrypt']
        )
    }
Enter fullscreen mode Exit fullscreen mode

Check out the documentation of generateKey for more info.

But in short, we are saying that we are using AES-GCM, we can extract the key and the key will be used to encrypt and decrypt.

And then we need to encrypt our data.

export interface EcryptedData {
    iv: Uint8Array
    encrypted: ArrayBuffer
}

async encrypt (data: string, aesKey: CryptoKey): Promise<EcryptedData> {
        const encodedData = new TextEncoder().encode(data)
        const iv = window.crypto.getRandomValues(new Uint8Array(128))
        // Encrypt with AES key
        const encryptedData = await window.crypto.subtle.encrypt(
            { name: 'AES-GCM', iv: iv },
            aesKey,
            encodedData
        )

        return {
            iv: iv,
            encrypted: encryptedData
        }
    }
Enter fullscreen mode Exit fullscreen mode

Well we have a bit more stuff going on here.
First we assume your data is a string like message. Ne need to convert it to a proper ArrayBuffer, which is needed for the encryption. We can use the TextEncoder for that.

Then, we need an IV (Initialization vector). It is important to note, that the IV should always be random!

To help us with real randomness (which is hard in computer systems), we have the getRandomValues function.

We are then passing our aesKey and encodedData to the function and crypto.subtle.encrypt will return us the encrypted data.

Note there, that we need to store the encrypted data and the IV!

const aesKey = await generateAESKey()
const wallet = await encrypt('Hello World', aesKey)

// wallet.encrypted => 🔒
// wallet.iv => [150, 142, 234, 218, 156, 112,...]
Enter fullscreen mode Exit fullscreen mode

Sweet! Now we have military-grade encryption!

But, we also need to decrypt our message.
But first of all, lets think about how we can store our encrypted data. Like I said before, in the world of the web crypto api, we will work mostly with array buffers. However storing array buffers might not be the most elegant solution.

There are various ways of doing it, but for simplicity lets say we just convert the arraybuffer to base64.

To decrypt our secret message, we need our data, the AES key and the IV.

async decrypt (data: string, key: CryptoKey, iv: string): Promise<string> {
        const ivBytes = base64ToArrayBuffer(iv)
        const dataBytes = base64ToArrayBuffer(data)

        const decryptedData = await window.crypto.subtle.decrypt(
            { name: 'AES-GCM', iv: ivBytes },
            key,
            dataBytes
        )
        return new TextDecoder().decode(decryptedData)
    }
Enter fullscreen mode Exit fullscreen mode

And again, we will get an array buffer out of crypto.subtle.decrypt so now we are using the TextDecoder to get our message back.

const aesKey = await generateAESKey()
const wallet = await encrypt('Hello World', aesKey)

// wallet.encrypted => 🔒
// wallet.iv => [150, 142, 234, 218, 156, 112,...]

const message = await decrypt(wallet.encrypted, aesKey, wallet.iv)

// message => 'Hello World'
Enter fullscreen mode Exit fullscreen mode

We are done! We have now encryption in our application.
However in real applications we have other pain points than just the process of encryption.

  • How to we store the aes key?
  • We want maybe the user to define the key?
  • Use password as key?

PBKDF2

A common use case would be to use the users password to encrypt something. So we need a way to generate a solid crypto key from a string (password). PBKDF2 for the rescue!

With password based key derivation we can generate a secure high entropy key from low entropy input (password).

async getKeyFromPassword (password: string): Promise<CryptoKey> {
        const encoder = new TextEncoder()
        return await window.crypto.subtle.importKey(
            'raw',
            encoder.encode(password),
            'PBKDF2',
            false,
            ['deriveBits', 'deriveKey']
        )
    }
Enter fullscreen mode Exit fullscreen mode

So we use crypto.subtle.importKey to get a CryptoKey off a password. Important here, is that we can deriveBits and deriveKey.

We can't directly use the CryptoKey for AES. For that, we need to derive the key.

 async getAESKeyFromPBKDF (
        key: CryptoKey,
        salt: BufferSource
    ): Promise<CryptoKey> {
        return await window.crypto.subtle.deriveKey(
            {
                name: 'PBKDF2',
                salt,
                iterations: 100000,
                hash: 'SHA-256'
            },
            key,
            { name: 'AES-GCM', length: 256 },
            true,
            ['encrypt', 'decrypt']
        )
    }
Enter fullscreen mode Exit fullscreen mode

Now we can use a users password to generate an AES key.

const password = 'Please-use-proper-high-entropy-passphrases!'
const PBKDFSecret = await getKeyFromPassword(password)
const aesKey = await getAESKeyFromPBKDF(PBKDFSecret, salt)
const wallet = await encrypt('Hello World', aesKey)

// wallet.encrypted => 🔒
// wallet.iv => [150, 142, 234, 218, 156, 112,...]

const message = await decrypt(wallet.encrypted, aesKey, wallet.iv)

// message => 'Hello World'
Enter fullscreen mode Exit fullscreen mode

And we are done again. Congratulations. You have encryption!
In Part 2, we will take a look at asymmetric key encryption with RSA and some best practices on storing secrets, keys and what I've learned while building a whistleblowing software with clientside end-to-end encryption.

Top comments (2)

Collapse
 
kob369 profile image
Kob

Part 2 please!

Collapse
 
kasperkamperman profile image
Kasper Kamperman • Edited

Indeed curious to read Part 2. Thanks for this clear article.