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.
The descriptor no longer carries key material. It carries what brings up DTLS
plus a 16-byte commitment; the ECDH and ECDSA public keys travel as the first
frame on the DataChannel and are checked against that commitment BEFORE they are
parsed or imported. Measured on the live site, the invitation went from 2274
characters across 4 animated QR frames to 151 characters in one frame.
What the split buys is that nothing is trusted on the strength of having
completed a handshake. The fingerprint arrives over the channel the user
authenticated by looking at it, so DTLS completes only with whoever showed the
code; the commitment makes substituted key material fail closed automatically
instead of relying on the human comparison; and the SAS is computed over a
transcript covering both descriptors verbatim and both key blobs, so anything an
attacker can change anywhere in the handshake changes the digits.
Consequences for the rest of the crypto:
- the HKDF salt is derived from the transcript rather than transmitted, binding
every session key to both DTLS fingerprints and every candidate;
- authProof is replaced by one ECDSA signature over the transcript, proving the
same possession without echoing a nonce back across seven fields;
- Double Ratchet support is implied by the format rather than advertised in it.
SBQ2 postdates the ratchet, so the "peer is old, fall back to static keys"
branch is unreachable here -- it used to fire silently, since SBQ2 has no `dr`
field for _initializeRatchet to find.
Fail closed throughout. _handshakeMode is latched per connection, so a session
cannot be pushed back onto SB1 halfway through; commitment mismatch, wrong role,
duplicate blob, bad proof and timeout all tear the connection down with a
specific message rather than degrade. SBQ2 handshake frames are refused outright
on a non-SBQ2 session, where there would be no commitment to check them against.
Reception of both formats is unconditional and split by first byte (0x02 vs
ASCII 'S') and text prefix. SBQ2_SEND_ENABLED is the single value that governs
what we emit; flipping it to false and redeploying reverts new invitations to
SB1, and the animated multi-frame QR path stays for them.
Verified end to end in real browsers -- 12/12 across {Chrome, Firefox} squared
and three network profiles -- through to a confirmed SAS, an active ratchet, a
64-byte transcript-derived salt and a decrypted message on the far side.
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.
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.
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.
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.
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.
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.
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.
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.
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.
- 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).
- 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.
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.
- 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.
- 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.
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).
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.
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.
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.
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.
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.
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
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
fix(file-transfer): size chunks under the 64KB SCTP message limit
Each 64KB chunk became a ~87KB AES-GCM+Base64 file_chunk message,
exceeding WebRTC's 64KB SCTP message-size floor. The consent handshake
(small messages) succeeded, but no chunk was ever delivered on Safari
and cross-browser connections whose SDP omits a=max-message-size, so
files never transferred. Send chunk size is now 16KB (~22KB on the
wire); inbound chunks up to 64KB stay accepted for backward compat.
fix(file-transfer): make MIME advisory, drive validation by extension
The client-supplied MIME type is easily spoofed and varies across
browsers/OSes, yet was a hard gate: files with an empty MIME or a
cross-OS variant (application/x-zip-compressed, image/jpg) were wrongly
rejected. Extension allow-list plus BLOCKED_EXTENSIONS is now the
boundary; a blatantly foreign MIME on a safe extension is still rejected
and per-type size limits still apply.
Chrome enforces CSP connect-src for WebRTC ICE servers. Without the
stun/stuns/turn/turns schemes the browser silently dropped STUN/TURN
candidates (only host candidates remained), breaking custom-server
connectivity test results and real cross-network ICE.
- add header gear + connection-screen entry points to Advanced network settings
- render the ICE settings modal at the app root (reachable from any screen via event)
- remove the standalone relay-only toggle/description from the start screen
(relay-only now lives in the advanced settings panel)
- fix crash from referencing main-component state inside EnhancedConnectionSetup
- bump version to 4.8.10 across header, manifest, README, init message, disclaimer
- document the feature in CHANGELOG and README
- add iceServers.js: allowlist-based validation/normalization of user-supplied
STUN/TURN URLs (rejects javascript:/data:/http/ws, control chars, enforces limits)
- add iceSettingsStore.js: opt-in persistence encrypted at rest with a
non-extractable AES-GCM device key in IndexedDB; load/save/clear
- add IceServerSettings.jsx modal: public vs custom servers, JSON/line input,
live validation, relay-only toggle, 'Test servers' connectivity check,
save-on-device prompt, forget-saved action
- wire chosen servers/privacy mode into EnhancedSecureWebRTCManager construction
(priority: custom > operator override > built-in defaults)
- entry point on the connection-creation screen next to the relay-only toggle
- add ice-servers-validation.test.mjs to the suite
Complete the mandatory receiver-consent gate that was wired in the
backend but never connected to the UI callback chain:
- Add the missing onIncomingFileRequest (4th) callback to
setFileTransferCallbacks in app.jsx — its absence caused
handleFileTransferStart to auto-reject every incoming file.
- Remove independent callback registration from FileTransferComponent;
the component was overwriting app-level callbacks on mount and
nulling all four on unmount, silently breaking progress/received/
error handlers whenever the panel was hidden.
- Lift pendingIncomingFiles state to the root component so consent
prompts are shown regardless of panel visibility; auto-open the
panel on incoming request.
- Add getReceivedFileObjectURL / revokeReceivedFileObjectURL on
EnhancedSecureWebRTCManager for download buttons in the panel.
- Update file-transfer-ui-cleanup regression test to match the new
single-owner callback architecture.
- All 14 tests pass; clean production build.
- Move CSP frame-ancestors and report-uri to HTTP headers
- Fix font-src to allow fonts.gstatic.com
- Add MIME type configuration for .jsx files
- Improve Service Worker error handling with cache fallback
- Rebuild application
- Add UpdateManager and UpdateChecker for automatic version detection
- Add post-build script for meta.json generation and version injection
- Enhance Service Worker with version-aware caching
- Add .htaccess configuration for proper cache control
This ensures all users receive the latest version after deployment
without manual cache clearing.
- Deleted BluetoothKeyTransfer.js and related classes
- Removed BluetoothKeyTransfer.jsx UI component
- Cleaned up Bluetooth imports from app-boot.js and bootstrap-modules.js
- Removed Bluetooth buttons and handlers from main app
- Eliminated all Bluetooth functionality due to Web Bluetooth API limitations
- Browsers cannot create GATT servers or advertise devices
- Reduced bundle size by ~78KB
- Application now focuses on supported browser technologies (QR codes, manual key exchange, WebRTC)
- Cache only essential PWA assets (manifest, icons, core scripts)
- Use Network First for all other requests
- Remove aggressive caching of UI components and styles
- Preserve PWA installation while minimizing cache footprint
- Add complete splash screen configuration for all iOS devices
- Support iPhone 17 Pro Max through iPhone 6 series
- Support all iPad models with landscape/portrait orientations
- Use proper media queries and generated splash images
- Fix iOS splash screen caching and display issues
- Updated connection flow between users via QR codes
- Added manual switching option in QR code generator
- Increased number of QR codes for better readability
- Removed session creation and Lightning payment logic
- Refactored security system:
* no more restrictions
* all systems enabled on session creation
- Improved QR code exchange for mobile devices