End-to-end encryption challenges

I’m building an end-to-end encrypted note taking app using Yjs and I’ve been wondering about the quality of the choices I’ve made and how to improve them. Any help is appreciated.

  1. End-to-end encryption algorithm: I chose to use classical end-to-end encryption (not nearly as advanced as Serenity Notes) based on the algorithms of Bitwarden and LastPass, mainly because I lack time and knowledge about double-ratchet-based end-to-end encryption. I’m wondering if the current level of end-to-end encryption is acceptable or if I should invest time to bring double-ratchet into the app. I’m also wondering about the performance aspects of using double-ratchet in the realtime collaborative environment.

  2. Update syncing challenges: I followed @dmonad’s suggestion to store a linear history of all the document updates. The issue I’m facing is that my app generates a relatively high amount of updates, so sometimes the decryptions start to impact the document loading performance. I’m using libsodium for encryption/decryption, by the way. To alleviate this issue I made the client always send the full merged update back to the server after the initial sync. This doesn’t seem very scalable so I’m wondering how I can further improve this. The next step is probably to somehow throttle this act of merging all updates, but I’m wondering what would be the optimal heuristic in this case. There is also the issue that the act of replacing the small updates with the full updates together with the decryption performance impact affects the client’s ability to ask for a range of document updates, so I’m wondering how to deal with that too.

  3. Database performance challenges: At the beginning of development I decided to use Postgres as the main database. All encrypted updates are stored in a table called page_updates and I’m implementing a Redis wrapper to cache the reads of the updates and to buffer the writes, but I don’t think that’s very sustainable. Each page can easily take around 300kb of Redis’ memory. That size may lower too much the max capacity of concurrent users and the effectiveness of update caching. I saw @dmonad’s suggestion to use leveldb for caching, but I’m wondering how to do it and how much can leveldb help. I saw it can only be used by a single process. Should I write a server around it? I guess I just don’t understand the strengths of leveldb very well.

What is the problem with this?

Nick (from Serenity Notes) doesn’t always send the full state to the server. He only does it after a certain amount of updates. Once there are N updates in the queue, the client will send the full state to the backend and overwrite (delete) the previous N updates. It is important to have some randomness here, otherwise, all clients will store the state concurrently when there are N updates. It would be better to store the full state once there are N + R updates in the queue, where R is a random number between 10-N/10 (just guesses here)

If the server doesn’t know the secret, this is really the only way to do it.

Also, you probably shouldn’t block the thread and generate the full state when the client is initially syncing (it is more important to render some initial state).

I’d be interested in some numbers. How expensive is it to encode | decode 10000 messages?

Postgres can work here. However, it is not well suited for A LOT of small incremental updates. Collaborative text editing apps generate one update for every single keystroke. Postgres is not well suited to handle these kinds of use-cases. A fast (scalable) NoSQL database might work better here (e.g. dynamodb). Even better, use something like Redis as a cache until you store the (merged) update in Postgres.

1 Like