From 69ce927b6d509bdffcb6c84d60509736168c6600 Mon Sep 17 00:00:00 2001 From: tajniak81 <13187254+tajniak81@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:56:28 +0200 Subject: [PATCH] Web client: E2EE on by default, trust shields, identity verification, QR - New rooms (public too) and direct messages are end-to-end encrypted by default. - Per-message trust: "Not verified" for sessions you haven't verified, and what is wrong for unsigned sessions, mismatched keys or plain text in an encrypted room (src/matrix/trust.ts). - Verify a person's identity in a room you share (your DM first), and QR-code verification both ways: show ours, scan theirs with the camera (qrcode-generator, jsqr). - Warnings when someone's identity changes, with acknowledge, verify again or withdraw verification. - Encrypted thumbnails for large images. - A tab-only session encrypts its crypto store with a key kept in sessionStorage; stores unused for two weeks are deleted. - Reset encryption fixed: new secret storage first, so the new cross-signing keys survive a stale /keys/query answer; status checks no longer query our own keys mid-setup. Co-Authored-By: Claude Opus 5.5 --- apps/web/README.md | 56 +++--- apps/web/package.json | 2 + apps/web/src/components/CreateRoomDialog.vue | 9 +- apps/web/src/components/QrCode.vue | 30 ++++ apps/web/src/components/QrScanner.vue | 73 ++++++++ apps/web/src/components/RoomTimeline.vue | 17 +- apps/web/src/components/SessionsDialog.vue | 35 +++- .../web/src/components/VerificationDialog.vue | 143 +++++++++++++-- apps/web/src/main.ts | 3 +- apps/web/src/matrix/client.ts | 106 ++++++++++- apps/web/src/matrix/crypto.ts | 164 ++++++++++++++---- apps/web/src/matrix/send.ts | 55 +++++- apps/web/src/matrix/timeline.ts | 14 ++ apps/web/src/matrix/trust.ts | 108 ++++++++++++ apps/web/src/stores/chat.ts | 27 ++- apps/web/src/stores/security.ts | 20 ++- apps/web/src/stores/session.ts | 21 ++- apps/web/src/views/RoomView.vue | 58 +++++++ pnpm-lock.yaml | 16 ++ 19 files changed, 845 insertions(+), 112 deletions(-) create mode 100644 apps/web/src/components/QrCode.vue create mode 100644 apps/web/src/components/QrScanner.vue create mode 100644 apps/web/src/matrix/trust.ts diff --git a/apps/web/README.md b/apps/web/README.md index d5ef2e9..1d46536 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -48,44 +48,44 @@ that one origin. - Read receipts, typing notifications, deleting messages. - End-to-end encryption, with the Rust crypto stack matrix-js-sdk loads (`matrix-sdk-crypto-wasm`): reading and sending in encrypted rooms, - files included (encrypted in the browser before they are uploaded). - A message this session can't decrypt says why, such as "sent before - this session signed in", not just "encrypted". -- Verification: verify someone's session from the room's member list, - or this session from one of your others, by comparing emoji (SAS). - Requests from others open a dialog to accept or decline. + files included (encrypted in the browser before they are uploaded, with + an encrypted thumbnail for large images). A message this session can't + decrypt says why, such as "sent before this session signed in", not + just "encrypted". +- Trust on every message: one from a session you haven't verified says + *Not verified*; one with something wrong (an unsigned session, + mismatched keys, plain text in an encrypted room) says what. +- Verification: verify a person once (in a room you share, your direct + message first) and every session they verify is trusted; or one session + at a time; or this session from one of your others. Either side scans + the other's QR code (the camera, in the browser) or both compare emoji. +- Warnings when someone's identity changes: a reset since you first saw + them asks you to acknowledge it; a reset since you *verified* them says + their sessions are no longer trusted, with Verify again or Withdraw + verification. - Encryption setup in Settings: cross-signing keys, secret storage and a key backup in one step, behind a recovery key shown once. A new session verifies itself with the recovery key (or another session) and restores older messages from the backup. Reset encryption when the key is lost. -- New private rooms and direct messages are end-to-end encrypted by - default (a direct message only when the other person has a session with - keys); Create room has the switch. -- Create a room (private, or public with an address), start a direct chat, - join by address or from the room directory, accept or decline invites, - leave. -- Room info: topic, members and their roles, invite, edit name and topic. +- New rooms (public ones too) and direct messages are end-to-end + encrypted by default; Create room has the switch for turning it off. - Settings: display name, avatar, theme, encryption. - Phone layout: one pane at a time, the room list is the home screen. -**Not yet:** per-message trust shields (a message from an unverified -session looks like any other), verifying a person's identity in a direct -message rather than one session at a time, QR-code verification, a warning -when someone's identity changes, encrypted thumbnails for images you send -(the receiving side shows the whole image), and a passphrase for the -crypto store. After that: replies, reactions, threads, spaces, -notifications. +**Not yet:** replies, reactions, threads, spaces, notifications. ### Encryption keys in the browser Each session keeps its keys in IndexedDB under its own name -(`warp-web-crypto::`), unencrypted at rest like any web -client's without a passphrase. Signing out deletes them; a session that -ends some other way (signed out elsewhere, the token revoked) has them -deleted when the page next starts. A session kept only for the tab -("Stay signed in" off) leaves its store behind when the tab closes; -the session can't be reused, but the keys stay until site data is -cleared. The `.wasm` file is most of the download (about 2 MB +(`warp-web-crypto::`). Signing out deletes them; a session +that ends some other way (signed out elsewhere, the token revoked) has +them deleted when the page next starts. A session kept only for the tab +("Stay signed in" off) encrypts its store with a random key that lives +with its credentials in `sessionStorage`: when the tab closes the key goes +with it, and the store left behind can't be read; it is deleted after two +weeks unused. A "Stay signed in" session's store is not encrypted at rest, +like any web client's without a passphrase. +The `.wasm` file is most of the download (about 2 MB compressed) and needs `script-src 'wasm-unsafe-eval'`, which warp-web's Content-Security-Policy allows; it allows no JavaScript `eval`. @@ -132,6 +132,8 @@ empty URL meaning "this origin". Content-Security-Policy, `X-Frame-Options: DENY` and `Referrer-Policy: no-referrer` with the client's pages. - Keep the proxy's read timeout above 30 seconds: `/sync` long-polls. +- Serve it over HTTPS: browsers only give the page a camera (for scanning + QR codes when verifying) and WebCrypto on a secure origin. - Allow request bodies as large as the homeserver's `media.max_upload_bytes`; uploads pass through warp-web. - warp-web adds `X-Forwarded-For`. The homeserver uses it for rate limits diff --git a/apps/web/package.json b/apps/web/package.json index 551d707..7e79c38 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -12,8 +12,10 @@ }, "dependencies": { "@warp/ui": "workspace:*", + "jsqr": "1.4.0", "matrix-js-sdk": "^44.0.0", "pinia": "^3.0.3", + "qrcode-generator": "1.4.4", "vue": "^3.5.22", "vue-router": "^4.5.1" }, diff --git a/apps/web/src/components/CreateRoomDialog.vue b/apps/web/src/components/CreateRoomDialog.vue index f16331a..74a8a66 100644 --- a/apps/web/src/components/CreateRoomDialog.vue +++ b/apps/web/src/components/CreateRoomDialog.vue @@ -26,9 +26,8 @@ watch(open, (v) => { error.value = '' }) -// End-to-end encrypted by default for a private room. A public one is -// readable by whoever joins anyway, and history is lost to newcomers. -watch(isPublic, (v) => (encrypted.value = !v && chat.canEncrypt)) +// End-to-end encrypted by default, public rooms too; turning it off is a +// choice made here, once: it can't be turned off later. // A public room's address, from its name: "Homelab ops" → homelab-ops. watch(name, (n, old) => { @@ -97,6 +96,10 @@ async function submit() {

+

diff --git a/apps/web/src/components/QrCode.vue b/apps/web/src/components/QrCode.vue new file mode 100644 index 0000000..328027e --- /dev/null +++ b/apps/web/src/components/QrCode.vue @@ -0,0 +1,30 @@ + + + diff --git a/apps/web/src/components/QrScanner.vue b/apps/web/src/components/QrScanner.vue new file mode 100644 index 0000000..6623925 --- /dev/null +++ b/apps/web/src/components/QrScanner.vue @@ -0,0 +1,73 @@ + + + diff --git a/apps/web/src/components/RoomTimeline.vue b/apps/web/src/components/RoomTimeline.vue index fd6d303..19c32a3 100644 --- a/apps/web/src/components/RoomTimeline.vue +++ b/apps/web/src/components/RoomTimeline.vue @@ -213,9 +213,20 @@ async function confirmDelete() { :title="formatDateTime(item.ts)" > - +