End-to-end encryption in the browser
Excalidraw Blog | End-to-End Encryption in the Browser
Traditional Website ArchitectureIn a traditional website architecture, you’d save a scene by sending it to the server, which gives you a shareable URL. The recipient then downloads the scene data from the server.In that world, you trust the server to contain your information but you don’t have to trust the pipes in between the client and the server, because you use HTTPS to encrypt the data.https://excalidraw.com/#json=5649116445016064,yOfExolZoMhtGnysT3-LWAThis works well unless the server gets compromised. The attacker will have access to every single drawing ever made! This is something we’d like to avoid.End-to-End EncryptionWhatsApp popularized end-to-end encryption, a technique that allows various clients to communicate without the server being able to read the content of the communication.The idea is to encrypt the content before sending it to the server. The server would just store the encrypted blob and send it back to the client.https://excalidraw.com/#json=5645858175451136,8w-G0ZXiOfRYAn7VWpANxwThe biggest challenge with this architecture is how to distribute the key to encrypt the message in such a way that the server cannot see it.Thankfully, in the context of a website, we can exploit the hash part of the URL. Anything that’s added after the # doesn’t get sent to the server, but is readable from the client-side JavaScript code.https://excalidraw.com/#json=5660568841093120,vki3y9xuEulFVHDqt-PBMwShow me the codeFortunately, the Web Cryptography APIs are now widely available to all the browsers that let us implement this. That said, the APIs to deal with encryption, keys and binary data are not the most straightforward, this next section walks you through how to wire it all together.UploadWe generate a random key that will be used to encrypt the data.1const key = await window.crypto.subtle.generateKey(2  { name: "AES-GCM", length: 128 },3  true, 4  ["encrypt", "decrypt"],5);We encrypt the content with that random key. In this case, we only encrypt the content once with the random key so we don’t need an iv and can leave it filled with 0 (I hope…).1const encrypted = await window.crypto.subtle.encrypt(2  { name: "AES-GCM", iv: new Uint8Array(12)  },3  key,4  new TextEncoder().encode(JSON.stringify(content)),5);We upload the encrypted content to the server. Note that we don’t send the key to the server!1const response = await (2  await fetch("/upload", {3    method: "POST",4    body: encrypted,5  })6).json();We generate the shareable URL. We use the jwk encoding in order to extract a base64 version of the key instead of having a binary encoded one.1const objectURL = response.url;2const objectKey = (await window.crypto.subtle.exportKey("jwk", key)).k;3const url = objectURL + "#key=" + objectKey;4DownloadIn the opposite direction, we download the file back from the server.1const response = await fetch(`/download?id={id}`);2const encrypted = await response.arrayBuffer();The key that we encoded in the url is the k field of the jwk object that represents the key. In order to get back a full key object we need to reproduce all the other fields that are static. It’s pretty verbose but it works!1const objectKey = window.location.hash.slice("#key=".length);2const key = await window.crypto.subtle.importKey(3  "jwk",4  {5    k: objectKey,6    alg: "A128GCM",7    ext: true,8    key_ops: ["encrypt", "decrypt"],9    kty: "oct",10  },11  { name: "AES-GCM", length: 128 },12  false, 13  ["decrypt"],14);We decrypt the message, decode it to string and parse it back as JSON.1const decrypted = await window.crypto.subtle.decrypt(2  { name: "AES-GCM", iv: new Uint8Array(12) },3  key,4  encrypted,5);6const decoded = new window.TextDecoder().decode(new Uint8Array(decrypted));7const content = JSON.parse(decoded);ConclusionAs the maintainer of Excalidraw, I now sleep much better at night. If the hosting service gets compromised, it doesn’t really matter as none of the content can be decrypted without the key.It also gives me the peace of mind to use Excalidraw for work related projects knowing that nothing will leak.If you’re building a website that needs to store data on the server, you may want to add end-to-end encryption, it’s pretty easy!