Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

38 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Sotto

sotto voce - "in a low voice." A privacy-first web messenger.

Register with a handle and password, then chat 1:1 with end-to-end encryption, disappearing messages, and an ephemeral secret-chat mode. The server only ever sees ciphertext, and never sees your private key.

What's inside

  • End-to-end encryption. Every message is sealed to the recipient's public key with X25519 crypto_box before it leaves the browser.
  • No personal data. Handle + password only. No phone, no email.
  • Disappearing messages. Per-message timer; the server purges expired rows and the client counts them down and removes them.
  • Secret chats. Never written to disk - relayed only while both parties are online, dropped if the recipient is away.
  • Safety-number verification. Compare a symmetric 60-digit number derived from both public keys to rule out anyone in the middle.
  • Identity at a glance. Deterministic identicons from each public key.
  • Stays logged in. Session (and unwrapped private key) persists locally so a refresh keeps you in. Delivered ticks, typing indicators, presence, day separators, unread counts in the tab title.
  • Six themes. Charcoal, Midnight, Plum, Terminal, Mist, Sand - switchable in Settings and remembered per device.

Security model

  1. From your password, the browser derives two keys with Argon2id: an authKey (sent to the server to log in) and a wrapKey that never leaves the device.
  2. Your X25519 keypair is generated locally. The public key is uploaded; the private key is encrypted with wrapKey and only the opaque blob is stored.
  3. Messages are encrypted client-side; the server stores/relays ciphertext only.

The server is honest-but-curious: it can see who talks to whom and when, but not what is said. Forward secrecy (double-ratchet) is a documented non-goal for v1.

Persistence tradeoff: keeping you logged in across refreshes means the unwrapped private key sits in localStorage. It never leaves the device, but a successful XSS could read it. Log out to clear it. A future hardening step is to re-derive the key from the password on each launch instead.

Stack

  • Server: Node + Fastify + ws + SQLite (better-sqlite3), Argon2id, JWT.
  • Client: Vite + React + TypeScript, libsodium-wrappers-sumo, Satoshi + JetBrains Mono.

Develop

npm install

# terminal 1 - API + WebSocket on :3300
npm run dev --workspace @sotto/server

# terminal 2 - Vite dev server on :5173 (proxies /api and /ws to :3300)
npm run dev --workspace @sotto/client

Open the Vite URL, create two accounts in two browser profiles, message between them.

Test

npm test --workspace @sotto/server   # db, auth, keys, messages, http, ws
npm test --workspace @sotto/client   # crypto, identicon, safety number, session, time, theme

Environment

Var Default Purpose
PORT 3300 Server listen port
DB_PATH sotto.db SQLite file path
JWT_SECRET dev-secret-change-me Session signing secret (set in prod)

About

Privacy-first web messenger. E2E encryption (X25519 crypto_box), disappearing messages, ephemeral secret chats, safety numbers. Handle + password, no phone, no email — server only ever sees ciphertext.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages