Mar 06, 10:30 PM
Back in January, I began a new project: A messaging service with end-to-end encryption. Here are my initial thoughs.
An Extremely M, Probably not V, P
I now have something that works (the UI is still in the execrable stage, sorry). Here’s an outline of how it’s going so far:
- Encryption handled by a WASM module; this code is not anything approaching efficient, but because it generally only has to work at “human-scale” speed, it’s plenty fast. I will get around to some of the lower-hanging fruit eventually.
- Outside of the WASM module, all data is base64 encoded. Chunks of data sent over the wire essentially look like this:
{ "Chunk": { "to":"uKDq9FgBY1YsP9EjFhg+xEDuGjScJYiHPk6z6DjnZzY=", "from":"KcU1F3ACEZONC1EDShNtQW/J4os51q98zHf4TX7H100=", "time":1678135624299, "nonce":"h/JmKaa3hQ4K6TGe65hyeMTnsSmk9t8R", "tag":"Dh/M7qxuW14qLYgr6GFoWg==", "data":"mDbrlnwad+BU... (1K of data in base64) ..." } }
- When a message hits the server, it is immediately stored in the database;
the time is converted to a
chrono::DateTime<Utc>
(which ends up in Postgres as aTIMESTAMP WITH TIME ZONE
), but everything else is left asTEXT
. - An acknowlegement of the message is returned to the sender.
{ "Ack": { "nonce":"h/JmKaa3hQ4K6TGe65hyeMTnsSmk9t8R", } }
- If the recipient is logged in, it gets sent.
- As of yet there is no provision for indicating which messages have been sent to their intended recipients.
- There is also not yet any provision for removing messages from the
database; it can be done manually by mucking about in the database
with some SQL
DELETE
ing.
Some frontend details:
- Contacts are stored in
window.localStorage
, indexed by their public keys, with the option to add a descriptive name to each. - Contacts can be added manually by public key. When a message from a non-contact comes in, that contact is automatically added.
- When a contact is selected for the first time in a session, a request will be sent to the server for “history” with that contact. Currently this is a request for all messages exchanged with this contact in the last week.
- Public key and contact list can be exported as a JSON file, and the JSON file can be imported on another machine/browser.
- The frontend has ended up with a sort of “layered” architecture, which I hope to further compartmentalize:
---------------------------------- websocket I/O human I/O, DOM manipulation Javascript Layer saving/loading/exporting/importing ---------------------------------- | A V | ---------------------------------- encoding/decoding to/from base64 WASM "flow" module assembling multi-part messages ---------------------------------- | A V | ---------------------------------- encryption/decryption WASM "crypt" module ----------------------------------
Some server details:
- There is no locking; each mutable resource is managed by its own task; the tasks communicate via channels.
- Each connection has its own task; the database has its own task; there’s a “main” task that communicates with the database task and manages return channels to the connection tasks.
- When a “history” is requested, the main task sends a clone of the
Sender
end of the requesting connection’s channel with the request to the database task; the database then spawns a new task with its results and the channel end to deliver the data to the requesting client. This way the database task doesn’t get tied up writing a bunch of data to a single channel.
Further Plans
Some of these ideas are actually in the queue of things to do; some are just dreams for in the future if I figure them out and have the time.
- Despite the nice layered structure of the frontend, the JS layer is a mess. It’s divided up into a few modules to try to separate the functionality, but they still all have their hands in each others’ pockets. Once I have the core features all worked out, this really needs a refactor.
- The “look’n’feel” is terrible. There’s no cohesive aesthetic; it’s been growing in a sort of ad-hoc manner as capability increases. This also needs a refactor. I have considered trying to learn a framework for this, but am as-of-yet undecided.
- I would eventually like to switch to a binary wire protocol, and store binary data. This will make it easier on the server, because it’ll have to do less work decoding JSON, and also easier on the frontend because it’ll have to do less conversion to/from base64. The JSON protocol was easy to get started with (particularly at the JSON layer level of the frontend).
- Currently message history is only requestable by timestamp range. I’d like to both add a column to the database to note whether a message has been delivered, and also have history be requestable by number of messages. Ultimately, the frontend should be able to ask for “all undelivered messages from X pubkey to Y pubkey plus the previous 10 (or however many) messages”. And then “the 10 messages before that”, etc.
- Automated database maintenance. Messages should be purged after some arbitrary age (six weeks?).
- A little bit of database normalization. Extract public keys into their own table indexed by ID; have that ID be a foreign key in the messages table.
- Shrink the raven icon; it doesn’t need to be 500 by 500 pixels or whatever it is.
- Blocking: A user should be able to block contacts; this should be implemented at the server level. The blockee’s messages should never get delivered to the blocker; the blocker shouldn’t be able to send any messages to the blockee, either.
The frontend/aesthetic stuff is going to be the most difficult for me. More to come.