main
277
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4000bde857 |
v6.8.1: the downloads point at desktop 1.0.3
Desktop 1.0.3 fixes video on a call placed after a group call — the two shared one peer connection, the second call opened a second video section, and the two H264 profiles collided, so the call carried sound and no picture. It also ships the STUN and TURN list this client uses, which is what lets a browser and a desktop agree on a path instead of one of them offering no relay at all. The version lives in two places on this side (the landing page and the download grid), and the consistency test also reads the README badge, the changelog and the cryptography document, so all of them move together. Riding along: the group status dot now takes its colour from the theme in the call and group views, which is what the desktop's copy of groupsStore.js was compared against. Claude-Session: https://claude.ai/code/session_019RCW2Fm7dkAaU2zeHDksei |
||
|
|
9e63cf65a4 |
v6.8.0: light theme
The palette lived as ~620 hex literals in inline styles plus a few hundred more in
the stylesheets, so there was no single thing to change. It is now 113 custom
properties in src/styles/theme.css, in two blocks.
src/scripts/theme-boot.js decides the theme before first paint — blocking, in <head>,
above the stylesheet, because a deferred script paints dark first and corrects itself.
It stores the mode ('system' | 'light' | 'dark'), never the colour it resolved to, and
stamps data-theme so an explicit choice can beat the media query. The switcher in the
header is a view onto it.
A filled accent stays the brand colour in both themes — the ink on it is near-black
either way — while an accent used as text darkens to clear 4.5:1 on white. A colour
reaches a fill by four routes (a style property, a constant, a helper argument, an SVG
source string), and tests/theme-switching.test.mjs covers all four.
The dark theme is unchanged: every colour declaration the previous build produced comes
out of this one identically once the properties are resolved.
Also: the roadmap drops its status chips on mobile, and Roadmap.jsx no longer splits a
colour with parseInt at runtime, which a var() reference cannot survive.
|
||
|
|
1c153b290a |
readme: screenshots of the web app as it looks now
The two that were here showed an older interface and neither had a caption. Replaced with five from securebit.chat: opening a channel, comparing the safety code, an open conversation, a group call, and an incoming one — each captioned with what the screen is actually for. Claude-Session: https://claude.ai/code/session_014KjzTXxrhzYoDDWChYQ4u2 |
||
|
|
414c27fda6 |
v6.7.3: faster loading, and pages search engines can read
The bundles carried all thirteen translations at once and a page fetched them a third time as raw source; each page now loads only its own language. Alongside that: JavaScript is minified, the eight stylesheets are served as one file, the QR scanner is fetched after the app is up instead of on every visit, Inter ships once rather than five copies of the same file, and Font Awesome is subset to the 82 icons this app draws instead of all 2468. 1.85 MB across 43 requests becomes under 700 KB across 33. On mobile the page starts drawing in 1.6 s instead of 6.3 s and is usable in 4.5 s instead of 11 s. Pages also carry their text in the HTML now. Everything was drawn by JavaScript into an empty div, so crawlers saw correct metadata around nothing, and twelve of the thirteen language pages had never been shown to anyone. The documentation is published under /docs/ with a new FAQ, and unknown addresses return a real 404. Separately: the localized shells were served with the year-long immutable cache header meant for static assets, which pinned anyone who opened /de/ or /ru/ to that build. The header is fixed and the service worker refreshes what it cached. Claude-Session: https://claude.ai/code/session_014KjzTXxrhzYoDDWChYQ4u2 |
||
|
|
0691ce618c |
web: Snap Store badge, served from here
The badge image is vendored rather than hotlinked from snapcraft.io. The CSP is img-src 'self' data: and would block it anyway, but the reason to leave the CSP alone is the page's own claim: fetching a badge from someone else's server hands them the address of every visitor to a page that says no servers are involved. The README badge stays dynamic — it reports the published version, and GitHub proxies images, so no reader is exposed by it. |
||
|
|
30e335f9e9 |
web 6.7.2: downloads point at desktop 1.0.1
1.0.1 is the first desktop build that works on Linux at all. It also carries the desktop.callsNotHere string these dictionaries now define for it — calls are not available on the Linux desktop yet and the app says so rather than failing. |
||
|
|
2294e38e53 |
web 6.7.1: desktop download links, and a changelog to match
6.7.0 shipped yesterday, so the download-link fix is a patch on top of it rather than a re-cut of that release. |
||
|
|
c5b759e120 |
build: 6.7.0 bundles carrying the 1.0.0 download links
The deployed bundles are what the page loads; the source .jsx files are not fetched at runtime. Rebuilding is what actually ships the change. |
||
|
|
b4e12c2a98 |
web: the desktop version lives in two files, and only one was bumped
src/app.jsx carries its own SB_DESKTOP_VERSION for the compact download table; src/components/ui/DownloadApps.jsx carries DESKTOP_VERSION for the grid. Bumping one leaves the other pointing at a tag whose filenames no longer exist — the failure tests/desktop-download-links.test.mjs was written for, and which it caught here. The i18n scan also learns that these dictionaries feed two clients. The desktop client is a separate repository, so its keys can never appear in a scan of this src/ and were being reported as dead strings to delete. |
||
|
|
8d7b41b5fd |
web: point the downloads at desktop 1.0.0
1.0.0 is the first release whose artifacts all carry updater signatures, so Windows and Linux are offered updates for the first time. |
||
|
|
6ea6e26c6c |
assets: version the desktop screenshots
nginx serves these paths with max-age=31536000, immutable, and applies it to a 404 exactly as readily as to an image. A request for a file that had not shipped yet — one of our own checks, as it happens — pinned a 404 in Cloudflare's cache for a year, and redeploying does not dislodge it: the edge never asks the origin again. The version in the name gives the store listing a URL the cache has never seen. It also makes the screenshots honest about which release they show. |
||
|
|
3f863299e2 |
deploy: ship the desktop client's store screenshots
assets/screenshots was excluded from the image because it held README material and nothing served it. That stopped being true: the desktop client's AppStream metainfo names five of these files by URL, and Flathub fetches them from this domain — a reviewer opens each one, and a 404 is a rejection. Only the desktop-* set is un-ignored. The README's full-size captures stay out, which is what the original rule was for. |
||
|
|
46be54b59f |
i18n: strings the desktop client needed, and its screenshots
The dictionaries here are the single source both clients read: the desktop
copies src/i18n/generated.js byte for byte, so a key added on either side is
available to the other.
* desktop.* — sentences only the desktop says. It has no QR display and no
browser to grant permissions, so its copy differs where the browser's would
mislead: "allow the microphone in system settings", not "for this site".
* msg.after5s and the rest of the disappearing-message timers, which this
client renders from hardcoded English today and can now translate too.
* secLevel.HIGH / MEDIUM / LOW, which the security panel needed and the
dictionary only had MAXIMUM for.
All of it translated into the full thirteen. Nothing here changes what this site
renders — the web client does not reference the new keys yet.
assets/screenshots/desktop-*.png are captures of the desktop client at 1.0.0,
for its Flathub listing. A reviewer opens each URL, so they have to be served
from this domain rather than from a code host.
|
||
|
|
8ad4b4558d |
chore(release): renumber group calls as v6.7.0
Group calls are a new capability, and 6.6.6 only landed as a patch because the version was bumped once per deploy while the feature was being tested. The number is the release's, not the deploy's. No code changes: version, badge, changelog heading, docs and build stamp. Claude-Session: https://claude.ai/code/session_01XSxAkET3hQTkYDQfbjCQwZ |
||
|
|
113bb107d3 |
feat(groups): audio and video calls in group chats; release v6.6.6
A group call is N-1 ordinary 1:1 calls, one to each other member, each riding the pairwise session that member already has — a transport a human already authenticated by comparing the safety code. No mixer, no SFU, no point at which two people's media meets anywhere but on a device. Call control is separate from call media, because the two reach different sets of people. Who opened a call, who joined and who left travels as group frames signed with the sender's group identity key, so it reaches members currently reachable only through a relay — and a relaying member can drop one but cannot write one. Media flows only where a direct link exists, so a member without one shows as connecting rather than being omitted. Frames carry a per-sender sequence checked before the action, so a captured leave cannot end a later call, and simultaneous calls converge on the lower random call id. One capture is shared across every leg rather than one getUserMedia per member, and legs answer without prompting: the flag permitting that is set only locally, only while this user is in the call, and cleared when they leave. UI: a gallery that sizes itself from the space it has, a spotlight view, an active-speaker indicator read from the waveform, and the call surface in the same visual language as the 1:1 one. Also in this commit, the v6.5.0 language-suggestion work that had not been pushed yet; its notes are in the changelog. And two fixes: the safety-code input asks for digits rather than text, and starting a new chat from inside a group no longer creates it behind the group where it cannot be seen — which had made it impossible to connect to anyone new, or to add anyone to a group, while a group was open. Claude-Session: https://claude.ai/code/session_01XSxAkET3hQTkYDQfbjCQwZ |
||
|
|
5e32f547b9 |
feat(i18n): Arabic, Hebrew, Persian and Urdu, and a layout that mirrors; release v6.4.0
Four right-to-left languages at /ar/, /he/, /fa/ and /ur/ — thirteen in all. The layout no longer has a left and a right, it has a start and an end: the margins, insets and corners are CSS logical properties now, so they follow dir on <html>. Directional glyphs flip; keys, safety codes and session descriptors are pinned left-to-right so bidi cannot reorder what two people compare against each other's screens. Also fixed: the Service Worker was registered as './sw.js', which 404s from every locale subdirectory, so twelve of the thirteen pages had no worker at all. And the bundler ran before the dictionaries were generated, so a newly added language could reach the page ahead of the app that renders it. |
||
|
|
98d42ac2fb |
feat(i18n): nine languages, each at its own address; release v6.3.0
The site now speaks German, French, Spanish, Ukrainian, Russian, Chinese, Korean and Hindi alongside English — 623 strings per language, 5,607 translations, covering the landing page, the key exchange, the chat, group calls and every error along the way. Each locale is a real page at a real URL (/de/, /fr/, …), generated at build time from locales/*.json. That is the whole point: the app is client-rendered, so a language that only swaps strings at runtime has no address for a crawler to index and no link anyone can share. Each page carries its own canonical, a reciprocal hreflang cluster and translated schema.org. The URL decides the language, always. A stored preference only applies where the address does not say, and nothing ever redirects on Accept-Language — that is how sites become invisible to search engines outside one country. robots.txt and sitemap.xml did not exist before; both are generated now. Bugs found on the way, each with a test that would have caught it: - partner logos used page-relative paths and 404'd from any /xx/ page - post-build stamped ?v= only into the root shell, leaving locales behind - "Back online" appeared on every page load, not just after being offline - update timestamps were hard-coded to US format for every reader - t() threw on a partial window, taking whole components down with it |
||
|
|
943e04e7ff |
feat(ui): motion that answers the finger, and honours reduced-motion; release v6.2.0
The mobile menu becomes a surface you drag rather than one that appears and vanishes: it tracks the finger, carries its velocity when flicked, can be caught mid-flight, and resists past the edge instead of stopping dead. Buttons react on press rather than on release, and the browser's tap delay is gone — nothing here zooms, so the wait bought nothing. prefers-reduced-motion, prefers-reduced-transparency and prefers-contrast are now respected instead of ignored: looping decoration stops, transitions fade rather than slide, the chat jumps to the newest message, frosted panels turn solid and edges get drawn. What stops is the movement, never the information — recording indicators and other status stay. Also bumps the desktop download buttons to 0.5.0. The version lives in one constant per source file and tests/desktop-download-links.test.mjs fetches every generated URL, so the links are proven to serve a real asset rather than merely looking well-formed. Claude-Session: https://claude.ai/code/session_01ARFZ9G6P1e1B4w75kFaeNw |
||
|
|
e00c3bd413 |
feat(groups): group chats, and a mesh rather than a star; release v6.1.1
A group is an orchestration layer over the pairwise sessions the app already holds. It owns no transport and no shared key: every frame leaves over a chat that is already SAS-verified and already ratcheted, so a removed member simply stops being sent anything. Membership is a roster the admin signs, ordered by epoch, and the safety code is a commit-then-reveal round over every member's fingerprint and nonce. Delivery was the part that did not match its own description. The admin held a link to everyone and nobody else held a link to anybody, so the relay path — the documented fallback — was in fact the entire topology, and the admin going away partitioned the group. Now, once the code is confirmed, each pair without a link dials one over that relay path. The descriptors are compact enough to ride a group frame and are signed with the sender's group identity key, so the relaying member can drop a dial but cannot substitute one. The member with the smaller fingerprint dials, which is the whole glare protocol. Mesh links are released without a human comparing digits. Twenty-eight codes for a group of eight is not a check anyone performs; the guarantee moves rather than disappears, since the descriptor was signed by a key the signed roster names and the group code covers. markGroupLinkVerified refuses any session whose in-band exchange has not completed and whose peer has not proved possession of that key. An existing 1:1 chat between two members is adopted instead of re-dialled, via a probe bound to that session's own key fingerprint so it cannot be replayed onto another chat to impersonate its author. Security fix: g_hello was accepted on any session from anyone who knew the group id, so any member could publish an identity the admin never invited and have the admin sign and broadcast a roster containing it. It is now accepted only on a session an invitation went out on, which also confines it to a direct link. Mesh connections are kept out of the chat registry and muted from the document events the header listens to, so a routing detail cannot tear down the display of a conversation the user actually opened. |
||
|
|
6a98e2eb00 |
fix(mobile): make the chat layout responsive on phones; release v5.9.2
The chat did not lay out correctly on phones, iPhone worst of all: the header would not stay at the top, and the layout shifted as you scrolled or opened the keyboard. The root cause was a floor, not a position. The app shell and column both carry .minimal-bg, which sets min-height:100vh — and min-height always beats height. On iOS 100vh is the URL-bar-retracted *large* viewport, so with the bar showing the shell was held some 60-100px taller than the visible area. That surplus made the document scrollable, and a scrolling document is how the header rode off the top. It also left the visual-viewport tracking inert: forcing --sb-vh to 700px left the shell at 844px. With the floor removed it follows to 700px and the document does not scroll. Also: the app shell is now sized from visualViewport rather than pinned, the message list is the only scroller and can actually shrink, safe-area padding applies (viewport-fit=cover) and collapses under the keyboard, and the online/offline toast no longer sits on top of the header. Adds ?preview=chat, which renders the chat layout with canned content and no connection — presentational only, no peer manager, keys, or network. Verified in an emulated iPhone viewport at rest and with the keyboard open: header pinned at 0-64, composer flush to the keyboard line, document unscrollable. |
||
|
|
556727eb6f |
fix(qr): accept a one-frame invitation; release v5.9.1
The scanner waited for four frames when shown a single one. Its chunk assembler was written for SB1, which the generator always cuts into exactly four frames, and its fallback branch claims any non-JSON string longer than 100 characters — which a 151-character SBQ2 invitation is. A complete invitation was filed as chunk 1 of 4, and the scan never finished. SBQ2 payloads are now recognised as complete before any assembly runs, in both the text and raw-byte forms, and the hard-coded frame count is marked as belonging to SB1 so it is not read as a general rule. |
||
|
|
808fd99b73 |
docs: describe the minimal invitation and where its security comes from
The README, ARCHITECTURE.md and CRYPTOGRAPHY.md still described the old handshake: keys and a session salt travelling inside the invitation, and a safety code derived from the two DTLS fingerprints. None of that has been true since 5.9.0. Adds a "The invitation" section to the README covering what the exchange was reduced to and why that is a security change and not only a smaller QR code: less material exposed before anyone is authenticated, the DTLS fingerprint as the anchor, substituted keys failing closed on the commitment instead of on a human comparison, a safety code that now covers the whole transcript rather than two fingerprints, and the plain fact that a single QR is scanned in person where a four-frame animated one pushes people to paste the invitation through a chat app. Session lifecycle in ARCHITECTURE.md gains the in-band key exchange as its own step. CRYPTOGRAPHY.md now states that the salt is derived from the transcript rather than transmitted, and describes the transcript SAS and the signature that replaced the challenge/response. DESCRIPTOR-SBQ2.md is listed in the doc index and in the CONTRIBUTING impact table. |
||
|
|
32b642f0e6 | docs: record SBQ2 phases 2 and 3 as shipped in v5.9.0 | ||
|
|
5f22b7dab5 |
feat(handshake): shrink the connection exchange; release v5.9.0
The invitation carried the whole session: both public keys, their signatures, a salt, a challenge and the full SDP. It now carries only what brings up DTLS — ICE credentials, the certificate fingerprint, candidates, an expiry — plus a 16-byte commitment to the key material. The key material itself moves to the DataChannel and is checked against that commitment before it is parsed. Measured on the live site: 2274 characters across 4 animated QR frames became 151 characters in a single frame. The safety code is now computed over a transcript of both descriptors and both key blobs, and the HKDF salt is derived from that same transcript instead of being transmitted. authProof is replaced by one signature over it. SBQ2_SEND_ENABLED is the single value that reverts new invitations to SB1. |
||
|
|
fb959d7ca7 |
fix(descriptor): rebuild SDP that Firefox accepts; release v5.8.1
Live cross-browser testing found three defects in the SBQ2 SDP template, all
invisible to Chrome and all fatal to Firefox.
The candidate lines omitted raddr/rport on srflx and relay candidates. RFC 8839
section 5.1 makes rel-addr and rel-port mandatory for non-host candidates even
though ICE never reads them; Chrome tolerates the omission and Firefox drops the
candidate. Relay-only connections to Firefox failed 0/8 against 8/8 for the
browser's own SDP. The STUN and TURN profiles hid it because a host pair
connected instead -- the relay candidates were never actually needed there.
The template also advertised ice-options:trickle without ever closing the
candidate set, though a descriptor is a complete one-shot set with no channel to
trickle over, and hard-coded the m= port and c= line to the 9 / 0.0.0.0 null
default candidate, which is the trickle convention for "nothing gathered yet".
Both are now correct: no trickle, an explicit a=end-of-candidates, and the most
publicly reachable candidate as the default.
All three are serializer-side and cost zero descriptor bytes; sizes are
unchanged at 98-149 bytes, QR version 6-8. Verified 48/48 across all 16
combinations of {Chrome, Firefox} squared and four network profiles, with every
relay-only pair now connecting over the relay.
|
||
|
|
6e82cfcae2 |
feat(descriptor): SBQ2 connection descriptor format; release v5.8.0
The SB1 invitation runs 2000-2400 characters and needs QR version 38-40, past the point where a single code is scannable, so the app falls back to an animated multi-frame QR. SBQ2 is a fixed binary layout carrying only what brings up DTLS -- ICE credentials, certificate fingerprint, candidates -- with the SDP rebuilt from a template by a strict serializer. Measured on real Chrome and Firefox SDP across four network profiles: 98-149 bytes, QR version 6-8. Key material is meant to move to the DataChannel, bound by a commitment in the descriptor. That half does not exist yet, so nothing calls this module: the format is landed for review and freeze, not wired into the connection path. doc/DESCRIPTOR-SBQ2.md records the gate on phase 3. The decoder is a parser of hostile input: fixed offsets, explicit lengths, deny-by-default on reserved values and unknown TLV extension types, trailing bytes rejected, ICE credentials alphabet-checked so a CRLF cannot reach the serializer. No compression -- DEFLATE adds bytes on this payload, and dropping it removes the decompression-bomb surface with it. Candidate pruning keeps coverage before count: one candidate per (family, type, transport) survives before any surplus, so an IPv6-only or UDP-blocked path cannot be pruned away by a v4-first sort. Tests cover round-trip against captured Chrome and Firefox SDP, IPv6 and NAT64 addresses, ICE-TCP candidates, the TLV area, clock skew, one-shot binding and SAS transcript coverage. |
||
|
|
3212138a0d |
docs: reorganise documentation; derive header version from package.json; release v5.7.2
No protocol or message-protection changes. The version in the application header was a literal and had fallen behind, showing v5.6.0 while running 5.7.1. It now comes from package.json, and a test fails if a hard-coded one reappears or if meta.json, the README badge, the changelog and the docs disagree about the release. Documentation reorganised so that everything technical lives in doc/ with an index, and the root keeps only what belongs there by convention: README, SECURITY, CHANGELOG and LICENSE. - SECURITY.md rewritten. It listed a supported release line three major versions out of date and made claims the software does not make. It now states what is guaranteed, what is not, and how to report a problem. - SECURITY_DISCLAIMER.md and RESPONSIBLE_USE.md merged into doc/USE-POLICY.md, which says what the software cannot protect against rather than listing generic advice. - doc/SECURITY-ARCHITECTURE.md renamed to doc/ARCHITECTURE.md and rewritten around the session lifecycle, what verification gates, and how recovery works. - doc/CRYPTOGRAPHY.md rewritten: key schedule, the Double Ratchet, framing, and memory handling, with values taken from the source rather than restated. - doc/CONFIGURATION.md rewritten with the real file-type policy, ICE and TURN guidance, and the deployment caching rules that matter. - docs/webrtc-config.md moved to doc/CALLS.md and rewritten; the obsolete docs/webrtc-audit.md, a working document full of stale line numbers, removed along with the docs/ directory. - doc/CONTRIBUTING.md records what the recent regressions taught us about writing tests that can actually fail. - doc/README.md added as an index. Internal security review notes are excluded from the repository via .gitignore. Those describe attack paths against specific releases in enough detail to reproduce them, which is useful privately and harmful in public while users are still updating. |
||
|
|
27279ae7c6 |
feat(crypto): Double Ratchet forward secrecy; hardening pass; release v5.7.1
Adds the Double Ratchet (Signal's design) on top of the existing ECDH session keys, so message protection no longer rests on one set of keys lasting the whole conversation. Every message gets its own key, derived through a one-way function and discarded after use, and each change of direction introduces a fresh ECDH key pair that re-keys the session root. The ratchet needed no handshake change: both peers already hold each other's authenticated ECDH public key, and the safety code compared during verification covers exactly those keys. Its root is derived from the existing shared secret through its own branch of the key schedule. Support is negotiated in the invitation and response and used only when both sides have it; a peer on an earlier release falls back to per-session keys. The security panel reports which of the two is actually in force. Out-of-order delivery is supported within fixed bounds (512 skipped keys per chain, 1024 retained, five-minute expiry), and inbound frames are authenticated before any ratchet state is committed, so a malformed frame cannot desynchronise a live session. Also in this release: - Verification is enforced as a gate, not a label: control frames (reconnection signalling, call setup, message deletion, delivery receipts) are acted on only after both peers have compared the safety code, and verified state is set in a single guarded place. - Chat content reaches the interface through one authenticated path; an older, weaker inbound path was retired. - The security panel measures what it displays — several checks previously returned a fixed result and now exercise the subsystem they describe. - Invitation data is no longer kept in local storage, and entries left by earlier versions are cleared on first launch. - View-once and disappearing messages no longer place their text in system notifications. - Shared-secret buffers are overwritten once derivation completes; scanned QR codes are decompressed with a size limit; voice notes are validated against audio type and size budgets before skipping the consent prompt; the master password is collected by the app rather than a browser dialog. - Connection setup no longer fails on networks where STUN/TURN are unreachable: it proceeds as soon as usable candidates exist and only waits while there are none. Test suite grows from 27 to 41 files, covering forward secrecy, post-compromise re-keying, out-of-order delivery across ratchet steps, the skipped-key bounds, tamper resistance, negotiation fallback, and byte-level key-derivation compatibility with 5.6.0. |
||
|
|
2a7142c722 |
feat(webrtc): recover a dropped connection without a signalling server; release v5.6.0
A chat no longer dies when the network moves under it. A NAT rebind, a lift, a Wi-Fi radio parking itself, a phone that dozed: the session repairs its own network path in place, and the messages typed meanwhile go out when it returns. Recovery is an ICE restart, which renegotiates only the transport path — the DTLS handshake, the session keys and the SCTP association carrying the data channel all sit above ICE and survive it. The renegotiation SDP therefore travels over the existing end-to-end encrypted, SAS-verified channel: no signalling service enters the design, and an attacker who cannot already decrypt the session cannot inject a reconnection. A restart is refused outright unless the DTLS fingerprint in the incoming SDP matches the live session's, so recovery can never re-point a conversation at a different peer. When the path is gone for good the session is ended and its data wiped rather than left half-alive: with no server there is nothing to re-signal through, and a conversation whose transport is gone should not leave its plaintext in an open tab. The two cases where that is already certain are recognised in seconds instead of being retried for two minutes — a channel that has delivered nothing at all since the drop cannot carry a renegotiation, and an ICE agent left bound to a network that no longer exists reports zero candidate pairs on every restart. Judging liveness was the hard part. Silence is not evidence of death: browsers freeze backgrounded tabs outright, and a frozen peer answers nothing while being perfectly healthy. What survives that freeze is ICE consent, which the browser runs in its network stack rather than on the page's thread — so a connected ICE state means a silent peer is asleep, and only a degraded one turns an unanswered probe into a teardown. The grace window before a restart is sized to the browser's own timings: 'disconnected' arrives after ~5s of missed consent responses and is held ~25s before 'failed', and that window exists for self-healing, so restarting at the start of it broke connections that were about to recover. Several long-standing bugs surfaced along the way and are fixed here: - handleHeartbeat() was dispatched to but never defined, so every inbound heartbeat threw a TypeError and peer liveness was never observed at all. - Heartbeats were folded into the 5-minute maintenance cycle instead of running on their own timer, far too coarse to notice a dead path. - ondatachannel can hand over a channel that is already open, so the answering side's 'open' event had been dispatched before the handler was assigned and never fired, leaving that side with no heartbeat, no watchdog and no file-transfer init. The peer whose network was fine kept showing "connected" indefinitely because nothing was running to notice. - Answering a heartbeat required the peer to have finished verifying, but the two sides confirm a SAS code at different moments; for that whole window one of them could not reply and was declared dead on a healthy connection. - Sending on a channel that was not ready returned in silence: the text stayed in the box, nothing was transmitted, and nothing said why. - The send path gated on navigator.onLine and the offline/online events, which report whether an interface exists rather than whether anything is reachable. A tab the OS froze misses the 'online' edge, and this side then queued every message forever: one tick on everything it sent, while incoming messages kept arriving. Sending is now decided by the data channel, and queues drain by polling rather than on an edge, so a missed event cannot strand them. - A false offline modal appeared on a working session, because the offline event was taken at face value. tests/session-recovery.test.mjs covers the state machine, the backoff and its serialisation, the offline hold, the sleeping-peer discriminator and the identity check. |
||
|
|
60bf037ef9 |
Fix the desktop download buttons; release v5.5.4
The buttons still led to a dead GitHub page. 5.5.3 updated one of the two places these links live — the platforms menu on the connection screen keeps its own DOWNLOADS table, and it was missed, so it stayed on 0.1.0. Why it looked like a working link that did nothing: the stale entries used /releases/latest/download/<file>, and GitHub resolves `latest` by redirecting to the newest tag. Once 0.3.0 shipped, a link written for 0.1.0 resolved to /releases/download/v0.3.0/SecureBit.Chat_0.1.0_x64-setup.exe — a file that never existed under that tag. The browser navigated to GitHub and downloaded nothing. Both places now build their URLs from a DESKTOP_VERSION constant with the tag pinned, so the version is written once per file and a link cannot silently become invalid when a new release goes out. Adds tests/desktop-download-links.test.mjs, which fails the build if this drifts again: every source must derive URLs from that constant, /latest/ and hardcoded versions in filenames are rejected, and each generated URL is fetched to prove the asset exists. SKIP_NETWORK=1 skips the fetches offline. |
||
|
|
fcee4216e8 |
Point desktop downloads at 0.3.0; release v5.5.3
The Download block still offered desktop 0.1.0 — the build from before in-app updates, the voice-note fixes and the verification hardening. Windows, macOS and Linux now link to 0.3.0. The version was repeated across three URLs in two different forms: two resolved through /releases/latest/download/ and one pinned a tag. That is how they drifted out of date, so it is now one constant. The tag stays pinned on purpose. Release filenames carry the version, so a /latest/ link breaks the moment a newer release exists, while a pinned tag keeps serving a working installer — the safer way to fail if the constant is ever left behind. |
||
|
|
6152a77b51 |
security: fix SAS verification bypass and unauthenticated frame injection; release v5.5.2
A security review of the transport and verification layers. Every item is a fix
to how untrusted peer input is handled; no features changed.
- SAS verification could be bypassed. `verification_both_confirmed` is an
unauthenticated frame on a channel that is not yet trusted, but it was taken as
proof that both sides had compared their codes — so a peer who completed the
signalling exchange could send it right after the data channel opened and drive
the other side to a "verified" session while the user never looked at the code.
It is now only an acknowledgement: refused unless this side already confirmed
locally, and _setVerifiedStatus() independently rejects any SAS-based
transition without a local confirmation. Holding ECDH-derived keys was never
proof of identity — a MITM has those too.
- Unauthenticated frames could be injected into the chat. A bare
{type:"message"} frame, a raw non-JSON frame and a binary frame were each
decoded and rendered, bypassing decryption, the HMAC check and the verification
gate; the injected text was indistinguishable from a genuine message. Chat
content now reaches the UI only through the authenticated enhanced_message
path.
- A peer could supply the verification code. `sas_code` announcements were
adopted verbatim when no local SAS had been derived yet. They may now only
corroborate the locally derived code.
- Anti-replay never ran. The sequence-number and AAD validators were defined on
SecureKeyStorage instead of the connection manager, so every call site failed
with a TypeError and the sliding replay window was dead code. Moved onto the
manager, wired into the live chat path, and a missing or non-numeric sequence
number now fails closed instead of sailing through the range checks.
- File transfers are gated on verification in both directions. Control frames are
written straight to the data channel by the transfer system; sending was
already gated, receiving now is too.
- Tighter CSP: connect-src and img-src no longer allow arbitrary https: hosts
(nothing in the app talks to a third party), plus base-uri 'none'.
- The SAS is no longer written to logs, and is compared in constant time on every
path. Fixed SecureMasterKeyManager.isUnlocked() testing a field renamed long
ago, so it never actually gated anything.
- Fixed the header showing "Secure undefined%": getRealSecurityLevel() became
reachable for the first time by the move above and returned only per-feature
booleans, while the header renders `level` and `score` directly. It now runs
the same verified scoring as every other consumer.
Adds regression tests for the verification gate, inbound frame authentication and
the security-level shape.
|
||
|
|
b3fcf54670 |
feat(webrtc): end-to-end encrypted voice & video calls with adaptive codecs
Add 1:1 voice and video calling over the existing SAS-verified peer connection. Audio and video tracks ride the same RTCPeerConnection as the chat, bundled onto one DTLS-SRTP transport, so media inherits the session's end-to-end encryption. SDP offer/answer is renegotiated in-band over the verified data channel — no signalling server, so the media's DTLS fingerprints are authenticated end-to-end. Calls are gated on a connected, SAS-verified session. Codecs & adaptation: - Opus tuned for lossy links (in-band FEC, DTX, RED redundancy); audio is bandwidth-prioritised and never throttled. - VP9/AV1 single-encoding SVC with H.264/VP8 fallback; video degrades by spatial/temporal layer. - Runtime NetworkAdaptationController trims video bitrate on loss/RTT and recovers as the link clears — no renegotiation. Live connection-quality indicator (Excellent/Good/Fair/Weak) in the call UI. In-call controls: mute, camera on/off (voice→video upgrade in-band), camera flip, minimize-to-widget, hang up, and accept/decline for incoming calls. Production logging disabled (DEBUG_MODE=false); temporary call diagnostic logger removed. Codec rationale in docs/webrtc-config.md. |
||
|
|
0de8ab2d54 |
Replace free TURN with self-hosted coturn; fix Safari connectivity; release v5.4.10
- config/ice-servers.prod.js: swap ExpressTURN for self-hosted coturn at turn.securebit.chat (TURN udp/tcp on 3478, TURNS/TLS on 443). Long-lived REST-API credential (expiry capped at int32 max for coturn compatibility). - Add raw-IP STUN/TURN fallback (144.172.96.126): Safari's WebRTC layer fails to resolve STUN/TURN hostnames on some networks and gathers no srflx/relay candidates; reaching the server by IP fixes cross-browser (Safari<->Chrome) connections. Harmless to other browsers. - deploy/nginx.conf: never long-cache /config/ice-servers.js so clients don't lock onto a stale server list. - Bump version to 5.4.10 (header + init banner). |
||
|
|
c98a01b1d5 |
Add end-to-end encrypted voice messages; release v5.4.5
- Record voice notes in-browser, sent over the chunked AES-GCM file-transfer channel (per-file session key + signed SHA-256 integrity). - Captured as PCM and encoded to WAV for universal playback (incl. iOS/Safari); auto-accepted and played inline from an in-memory blob, never written to disk. - Composer mic button with live waveform + timer; desktop shows mic + send side by side, mobile swaps mic to send when typing. - CSP media-src now allows blob: so recorded/received audio can play. - Roadmap: Desktop Edition -> 5.0, new 5.5 'Secure Voice & Calls', later milestones shifted; version bumped to 5.4.5. - Update README, docs (security/API/cryptography), and CHANGELOG. |
||
|
|
0e3e3a2974 |
Add multiple-session support: run several independent encrypted chats at once
Each conversation now runs its own WebRTC session with separate keys and SAS verification, so chats never mix. Adds a side panel to switch between open chats with unread badges, a New chat action that leaves existing chats connected, per-chat local labels stored only on this device, and an availability status (Available, Away, Busy, Invisible) shared end-to-end with connected peers. Also includes vendored Prism syntax highlighting, more reliable PWA update handling, and offline send queueing fixes. Version 4.10.0. |
||
|
|
db5d6e481d | seo: update page title to "SecureBit.chat — Private, Encrypted Messenger" | ||
|
|
ef2f13d053 |
seo: refine social card — SVG mark, more spacing, cache-busting filename
- Use the transparent SVG brand mark instead of the dark-background PNG. - Increase spacing between the headline and the feature chips. - Rename the card to assets/social-card.png so browser/CDN/social caches fetch the new image instead of the stale og-image.png; repoint og:image, twitter:image and JSON-LD accordingly. |
||
|
|
62567fb5cc |
seo: rework title/description, add structured data, refine social card
- New keyword-focused <title> and meta description. - Add robots (index, follow, max-image-preview:large) and og:locale. - Add schema.org JSON-LD (WebSite + WebApplication) with feature list, free/MIT offer and GitHub sameAs — non-executable data block, so it passes the strict script-src CSP. - Regenerate the 1200x630 social card without the redundant URL line. |
||
|
|
ae6900d0c7 |
seo: add social preview image and fix Open Graph / Twitter meta
Link previews (LinkedIn, X, etc.) were broken: og:image pointed at a GitHub-hosted favicon (.ico, too small, likely 404) and og:url pointed at the GitHub repo instead of the live site. - Add a branded 1200x630 social card at assets/og-image.png. - Point og:url/canonical at https://securebit.chat/ and og:image at the new PNG via absolute URLs; add og:image:width/height/type, og:site_name, og:image:alt and twitter:image. - Refresh the meta description to match the product (no longer version-stamped). |
||
|
|
5eaf57c8da |
Fix formatting for Privacy by design section in README
fix text |
||
|
|
ab49d7485d |
Fix formatting and headings in README.mdfix
fix text |
||
|
|
96fffab765 |
release: v4.9.1 — redesigned QR scanner, UI fixes, README overhaul
UI / design
- Rework the camera scan modal to the new "Start Secure" design: green
viewfinder with corner brackets, animated scan line, spinner + live
frame counter, and a blurred dark backdrop. Keep the Html5Qrcode
#qr-reader video feed, styled to fill the square viewfinder.
- Fix Advanced (network) settings: the fixed landing header (z-50) was
covering the panel's close button — raise the embedded overlay to z-60.
- Stack the connection-screen footer buttons ("Download desktop app" /
"Advanced settings") full-width on mobile and tablet instead of in a row.
Docs
- Rewrite README to follow GitHub best practices: capability-oriented
Features, How it works, and Security model sections; move release notes
out of the README and point to CHANGELOG.md. Keep logo and screenshots.
Chore
- Bump version to 4.9.1 (header, package.json, manifest) and rebuild bundles.
|
||
|
|
cbf5c5f834 |
release: v4.9.0 — full redesign + reworked offline mode
Ground-up visual redesign across the entire surface (landing, connection setup, chat header, security verification report, file transfer, PWA install/update/offline dialogs). Offline reworked: store-and-forward queue (send while offline → queued, delivered on reconnect), WhatsApp-style per-message delivery status (sending/sent/delivered/not-sent) via delivery receipts, offline buffering for messages to an offline peer, and offline state no longer leaking into the connection indicator. Resilient chunked file transfer with retransmission and auto-save. README + screenshots added. |
||
|
|
b39f9ecd2c |
release: v4.8.20 secure chat tools — completed, fixed and polished
Completes the messaging controls from v4.8.14 and fixes the bug that made them appear broken for recipients. Fixed: - Per-message metadata was silently dropped for recipients. NotificationIntegration wrapped onMessage and deliverMessageToUI with 2-arg shims that called the originals without the 3rd argument (meta); with notifications enabled, view-once, disappearing timers and unsend all failed on the receiving side. Both wrappers now forward all arguments. Added tests/notification-meta-forwarding.test.mjs. - Chat would not open after SAS: composer props were threaded into the wrong component (EnhancedConnectionSetup vs EnhancedChatInterface) -> ReferenceError nowTick on the verified re-render. Props moved to the chat component. Changed: - Code blocks: lightweight dependency-free syntax highlighting via React nodes (no innerHTML/remote scripts); code mode expands the input; copy auto-clears the clipboard after ~30s. - View-once: configurable visible-after-open time (5s/15s/30s/1m) via meta.onceTtl. - Disappearing timer: duration picker (Off/30s/5m/1h) instead of click-cycling. - Composer toolbar moved next to "Send files"; borderless buttons, brand-orange active state; pickers open upward and are mobile-friendly. - Sender bubble background lightened to rgba(249,115,22,0.05). Removed: - Panic wipe button (disconnect already wipes keys and clears session state). Transport unchanged: per-message metadata travels inside the encrypted envelope, whitelisted/bounded by _sanitizeMessageMeta. Full suite: 19 files, all passing. Docs (README, CHANGELOG) updated; version bumped to 4.8.20. |
||
|
|
cb72b9cb1c |
fix: chat not opening after SAS in v4.8.14 (nowTick scope) — v4.8.15
The new composer props (nowTick, codeMode, view-once/timer setters, unsend/expire handlers) were threaded into EnhancedConnectionSetup, but the message list and composer live in the sibling EnhancedChatInterface. After SAS confirmation the verified-state re-render referenced an out-of-scope `nowTick`, throwing "ReferenceError: Can't find variable: nowTick" so the chat never rendered. Move the prop destructuring and pass-through onto EnhancedChatInterface (where the chat UI actually is) and revert the mistaken additions on EnhancedConnectionSetup. No behavioural change to the v4.8.14 features otherwise. Bumps to 4.8.15. |
||
|
|
15173a9278 |
release: v4.8.14 secure chat tools (code blocks, view-once, disappearing, unsend, panic)
New privacy-focused messaging controls in the composer: - Code blocks: button wraps the message in a fenced block; both peers render a monospace code window with a copy button (clipboard auto-clears after ~30s). Window is built from sanitized text via React nodes — no new XSS surface. - View-once: recipient sees a blurred bubble, reveals on tap, then it is wiped. Honestly cooperative (not screenshot-proof). - Disappearing messages: optional 30s/5m/1h timer auto-deletes on both sides with a live countdown; incoming TTL clamped to [5s, 24h]. - Unsend (delete for everyone) via new MESSAGE_TYPES.message_delete control. - Panic wipe: clears chat, wipes keys and disconnects (behind a confirm). Transport: - Per-message metadata (id / view-once / timer) travels inside the encrypted envelope, not in the sanitized text, so content cannot spoof these controls. - _sanitizeMessageMeta whitelists + bounds metadata on send and receive. - AAD/replay protection, SAS gate and receive-side DOMPurify are unchanged. Adds tests/secure-chat-features.test.mjs (full suite: 17 files, all passing). Bumps version to 4.8.14 across package.json, package-lock.json, manifest.json, index.html, meta.json, README, SECURITY_DISCLAIMER, header and init banner. |
||
|
|
cf36656341 |
release: v4.8.13 message integrity & transport hardening
Bumps version to 4.8.13 across package.json, package-lock.json, manifest.json, index.html, meta.json, README, SECURITY_DISCLAIMER, the site header and the in-app init banner (previously desynced at 4.8.10/4.8.11/4.8.12). Ships the security-review fixes already on main: - removed the over-broad send-path keyword blocklist that silently rejected legitimate messages (real XSS defense remains receive-side DOMPurify) - preserve newlines/tabs/indentation in outgoing message sanitization - stop logging raw AAD (sessionId + keyFingerprint) on validation failure - add Strict-Transport-Security and Permissions-Policy headers - add outgoing-message-integrity regression tests |
||
|
|
42be55aaeb |
fix(security): restore outgoing message integrity, add HSTS/Permissions-Policy
- Remove send-path keyword blocklist that silently rejected legitimate messages (e.g. "constructor", "global", "document.", literal "javascript:") without adding protection. XSS is enforced at the rendering boundary by the receive-side DOMPurify pass and by sanitizeMessage() before encryption. - Preserve newlines/tabs/indentation in _sanitizeInputString; stop collapsing all whitespace which destroyed multi-line messages and code snippets. - Stop logging raw AAD (sessionId + keyFingerprint) on validation failure; log length only, in both message and file-message AAD validators. - Add Strict-Transport-Security (2y + preload) and Permissions-Policy (camera=self for QR, rest denied) to nginx.conf and .htaccess. - Add tests/outgoing-message-integrity.test.mjs regression suite. |
||
|
|
6f36fce8c6 |
release: v4.8.12 chat notification & file-transfer UI fixes
fix(file-transfer): announce received file once, not many times The per-transfer lock used a single `if` check, so when 3+ chunk operations queued on the same fileId they awaited the same in-flight lock and then ran concurrently, breaking assembly atomicity. The lock now loops until the slot is free (true serialization) and file assembly is idempotent, so `File received` shows exactly once per file. fix(verification): stop duplicate connection-setup system messages handleVerificationBothConfirmed had no guard, so when both peers sent verification_both_confirmed symmetrically one side ran both the local detection path and the peer-notification path, emitting "Both parties confirmed!" and the verified transition (and "Secure connection established") twice. It now bails out if both confirmations are already recorded. fix(ui): wrap long DTLS fingerprint inside the chat bubble The message text column is a flex child with default min-width:auto, so the long unbroken fingerprint overflowed. Added min-w-0 so break-words can wrap it. chore(release): bump version to 4.8.12 in header, init banner, manifest |