Case Study
Yapp.
A real-time chat app where the server only ever sees scrambled text.

The challenge
Most chat apps encrypt messages on the way to their server, and then the server can read them. I wanted the opposite: encrypt in the sender's browser, decrypt only in the recipient's, and have the server store and pass along nothing it can read.
That raises awkward questions. RSA can't encrypt a long message, the sender still needs to read their own sent messages, and if the private key only lives in one browser, signing in on a new device would lose every conversation.
How it works
What happens to your keys when you sign up, and to every message you send:
Key decisions
- 01
Encrypt each message with its own key
Every message gets a fresh AES-GCM key, which handles text of any length. That key is then encrypted twice with RSA-OAEP, once with the recipient's public key and once with yours, so both of you can read the thread and nobody else can.
- 02
Let your password carry your private key
The private key is locked in the browser with a key derived from your password, and only the locked copy goes to the server. Sign in on any device and it unlocks locally. After a reload, an unlock screen asks for your password before the key goes back into memory.
- 03
Real time, without trusting the socket
A FastAPI WebSocket server relays ciphertext, delivery receipts and online status. Messages sent to someone offline are stored and marked delivered when they connect, and the client reconnects on its own with a refreshed token.
![Code from lib/crypto.ts:
async function deriveWrappingKey(
password: string,
saltB64: string,
): Promise<CryptoKey> {
const enc = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
"raw",
enc.encode(password),
"PBKDF2",
false,
["deriveKey"],
);
return crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: BASE64.decode(saltB64),
iterations: 100_000,
hash: "SHA-256",
},
keyMaterial,
{ name: "AES-KW", length: 256 },
false,
["wrapKey", "unwrapKey"],
);
}](/case-studies/yapp-code.avif)
Decision 02, in code
lib/crypto.ts
- Lines 1–4
Takes your password and a random salt stored with your account.
- Lines 5–12
Imports the password as raw key material that can only be used to derive other keys.
- Lines 14–25
Stretches it through 100,000 SHA-256 rounds into an AES-KW key that can only lock and unlock other keys.
Results
- 0messages the server can read
- 2048bit RSA keys, generated in your browser
- 100kPBKDF2 rounds guarding your private key
- 2encrypted copies of each message key: theirs and yours
Screens


