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)" > - +