SH
I
EMO
← All projects

Yapp.

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

Yapp on desktop
Role
Design & full-stack development
Timeline
2 days
Type
Personal project
Stack
Next.jsTypeScriptWebSocket

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:

Signing upSending a message
Browser generates an RSA key pair
Your password locks the private key
Server keeps the public key and the locked copy
A fresh AES-GCM key encrypts each message
That key is encrypted for them and for you
Server stores and relays only ciphertext
Their private key unlocks the AES key, then the message
Signing upSending a message
Browser generates an RSA key pair
Your password locks the private key
Server keeps the public key and the locked copy
A fresh AES-GCM key encrypts each message
That key is encrypted for them and for you
Server stores and relays only ciphertext
Their private key unlocks the AES key, then the message

Key decisions

  1. 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.

  2. 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.

  3. 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"],
  );
}

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

Yapp on desktop
Yapp on mobile