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.
- Generate AES key
- Encrypt our data
- ???
- 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']
)
}
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
}
}
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,...]
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)
}
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'
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']
)
}
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']
)
}
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'
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)
Part 2 please!
Indeed curious to read Part 2. Thanks for this clear article.