end-to-end encryption with sveltekit
published
Introduction
Now that Magiedit is kind of done (except for user experience, I guess), I figured I had to tackle the security / sync of notes; at the beginning of this project, I thought I could store everything locally (first versions did not even have authentication), and later on implement a solution to sync back and forth with a remote server using something like couchdb and pouchdb. When I tried implementing things like those, however, I ran into all kinds of problems, namely making it play nice with sveltekit (and vite) and syncing different models (articles and settings for each user). And so I decided, at least for now, to ditch storing articles locally, and simply store them on a remote db (right now turso, btw). The thing is, I didn’t want to store raw article data for reasons like data privacy and privacy in general (I don’t want to know what you write using Magiedit). To solve this problem, I started looking into cryptography, specifically the web-cryptography api.
How this all works
Genering a master password
When a user creates an account, it is now required to create a master password; this will then be stored (hashed, obviously) on the server and at each new session they will need to enter it again. This master password is the origin for every cryptographic key used to encrypt and decrypt the articles.
NB: except for the master password creation and unlocking, the clear password is never sent to the server and is instead stored in the browser’s session storage
How and where things happen
How it works (algorithms and stuff)
DISCLAIMER: I’m not an expert in cryptography, so take everything I say with a grain of salt
- The key is based on a hash of the master password using SHA-256
- The algorithm used for encryption is AES-CBC, since it is symmetric and does not make me manage private and public keys (they would work better, but let’s be honest; they are just articles, after all)
Where it works
Let’s first take a look at this schema:
When a user creates, loads (from a file) or saves an article, the same steps happen:
- A cryptographic key is generated using the master password hashed using SHA-256
const keyBytes = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(keyData));
const key = await crypto.subtle.importKey('raw', keyBytes, 'AES-CBC', false, ['encrypt']);
- We generate an iv (initialization vector, kind of like a seed)
function generateIv() {
const data = new Uint8Array(16);
crypto.getRandomValues(data);
return data;
}
- The text is then encrypted using the key and the iv
const encodedContent = await crypto.subtle.encrypt({ name: 'AES-CBC', iv }, key, new TextEncoder().encode('write here'));
- It is then converted to base64
const base64 = btoa(String.fromCharCode(...new Uint8Array(encodedContent)));
- and finally sent to the server (along with its iv) to get it stored securely
As for decrypting, it is the same process, but in reverse:
- get the article from the db
- convert the base64 string to a buffer
- generate a key from the master password
- decrypt the buffer
- convert the buffer back to a string
I really like this gif, you know?
Why end to end encryption ?
Because I wanted users to know that even if I wanted to (and I don’t) read what they wrote before publishing, I couldn’t, because I wanted to be in line with current regulations about data privacy and, most of all, because it looked like it could be fun!