0
$\begingroup$

I'm building an open source, end-to-end encrypted real time collaborative text editor. My implementation uses the window.crypto APIs in client side JavaScript. I'm quite new to cryptography, so I'd like to have a few more sets of eyes checking that what I'm doing is no completely idiotic.

The full code is on GitHub and the app is hosted live here but I've summarized the crypto part below.

I'm using arbitrary values for the key length (256) and IV length (128), not sure if that's reasonable.

Use case

A user goes to picnic.lecaro.me, clicks the "create document" button and gets redirected to a new empty document with a random ID. A symmetric key is generated and added to the URL as a hash.

The user types in the editor. The key is used to encrypt the text. The encrypted text is sent to the server and saved.

The user shares the link to the document (including the document ID and symmetric key) to a friend. The friend opens the link and receives the encrypted doc from my server. He uses the key to decrypt the document locally.

Here's a typical URL for a doc

https://picnic.lecaro.me/DOCUMENT_ID#BASE64_SYMMETRIC_KEY

Algorithm choice

By design, I want every reader to be also able to write, to make sure that the service is never used to spread harmfull content. Anyone that can read the text should also be able to delete it. So I'm using a symmetric encryption algorithm.

I want the encryption to happen client side, to keep the server very simple, so I need to use one of the supported client side js algorithms. I'm aware that from a security standpoint, client side encryption in JS is not really better than server side.

RSA-OAEP is asymmetric, and MDN seems to recommend AES-GCM over AES-CBC and AES-CTR. However, I'm not sure that I'm taking advantage of the "built-in authentication" that this paragraph mentions.

It's strongly recommended to use authenticated encryption, which includes checks that the ciphertext has not been modified by an attacker. Authentication helps protect against chosen-ciphertext attacks, in which an attacker can ask the system to decrypt arbitrary messages, and use the result to deduce information about the secret key. While it's possible to add authentication to CTR and CBC modes, they do not provide it by default and when implementing it manually one can easily make minor, but serious mistakes. GCM does provide built-in authentication, and for this reason it's often recommended over the other two AES modes.

Keys generation

When you create a new document, the client generates a key for the AES-GCM algorithm. It's immediately exported and set as the location hash.


const key = await window.crypto.subtle.generateKey(
  {
    name: "AES-GCM",
    length: 256,
  },
  true,
  ["encrypt", "decrypt"]
);

const exported = await window.crypto.subtle.exportKey("raw", key);

history.replaceState(null, null, "#" + bufferToS(exported));

function bufferToS(buffer) {
  return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)));
}

If there's already a location hash set, the app decodes it.


const key = await window.crypto.subtle.importKey(
  "raw",
  sToBuffer(window.location.hash.slice(1)),
  "AES-GCM",
  true,
  ["encrypt", "decrypt"]
);

function sToBuffer(base64_string) {
  return Uint8Array.from(atob(base64_string), (c) => c.charCodeAt(0)).buffer;
}

Encryption

When the user makes changes, the full text is encrypted with the key and a random iv buffer


const iv = await window.crypto.getRandomValues(new Uint8Array(128));

const encrypted = await window.crypto.subtle.encrypt(
  {
    name: "AES-GCM",
    iv: iv,
  },
  key,
  new TextEncoder().encode(textarea.value)
);

// encrypted and iv are sent to the server

This encrypted data and the iv are then sent to the server. The server saves it as is plain text JSON file in the /data folder and forwards it as-is to all clients connected to this document.

Decryption

All clients then receive the update from the server and decrypt it :


// encrypted and iv are received from the server

const decryptedButBinary = await window.crypto.subtle.decrypt(
  {
    name: "AES-GCM",
    iv,
  },
  key,
  encrypted
);

textarea.value = new TextDecoder().decode(decryptedButBinary);
New contributor
Renan Le Caro is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
$\endgroup$
13
  • 3
    $\begingroup$ See: What are the rules for using AES-GCM correctly?, Stick to 96 bit IV since other than there is extra GHash call that the IV collision is not easy to detect. Well the library is not gain attention. Server uses JS! Where is the authentication tag? If you don't use it why AES-GCM? This is rather off-topic here. $\endgroup$
    – kelalaka
    Jan 11 at 14:20
  • 2
    $\begingroup$ Have you checked your implementation against the test vectors in csrc.nist.gov/groups/STM/cavp/documents/mac/gcmtestvectors.zip ? That's the obvious way to check the correctness of your implementation. Of course, your question extends beyond that to 'am I doing the right thing'; however correctness is part of it $\endgroup$
    – poncho
    Jan 11 at 15:14
  • 3
    $\begingroup$ @kelalaka The server could at any time release new/different JS that decided to snoop on the document.hash value. However, if the server is honest, it will not have visibility over the key, because document.hash is never sent as part of an HTTP/HTTPS request. Ideally you'd just turn off HTTP altogether. Some would argue that server-provided JS isn't a world apart from having auto-updating standalone encryption software which gets compromised as a result of new code pushed to a user, where the new code simply leaks all encryption keys to the software provider. $\endgroup$
    – knaccc
    Jan 11 at 15:24
  • 4
    $\begingroup$ In CodeReview.SE there are lots of AES GCM review that you may benefit and some of them are checked/reviewed by our Mod Maarten Bodewes there there must be full running code to review... $\endgroup$
    – kelalaka
    Jan 11 at 15:42
  • 2
    $\begingroup$ @RenanLeCaro: I have looked at your code. 1) You dont't implement AES-GCM. You just use it. Thus the title of your question is wrong. Please edit it. 2) What you have implemented makes to me no sense at all. The client encrypts the text, but the server keeps the text decrypted, i.e. plain text. Server encrypts the text when it sends it to other clients. This all is much weaker than TLS / HTTPS. Thus you introduce a lot of obscurity and do communication much worse compared to normal TLS / HTTPS. I suggest to close this question. $\endgroup$
    – mentallurg
    Jan 11 at 22:17

Browse other questions tagged or ask your own question.