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

Where it works

Let’s first take a look at this schema: magiedit’s data flow diagram

When a user creates, loads (from a file) or saves an article, the same steps happen:

const keyBytes = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(keyData));
const key = await crypto.subtle.importKey('raw', keyBytes, 'AES-CBC', false, ['encrypt']);
function generateIv() {
	const data = new Uint8Array(16);
	crypto.getRandomValues(data);
	return data;
}
const encodedContent = await crypto.subtle.encrypt({ name: 'AES-CBC', iv }, key, new TextEncoder().encode('write here'));
const base64 = btoa(String.fromCharCode(...new Uint8Array(encodedContent)));

As for decrypting, it is the same process, but in reverse:

Art Drawing GIF by GEICO 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!