37 Commits
Author SHA1 Message Date
lockbitchat b3fcf54670 feat(webrtc): end-to-end encrypted voice & video calls with adaptive codecs
CodeQL Analysis / Analyze CodeQL (push) Canceled after 0s
Deploy Application / deploy (push) Canceled after 0s
Mirror to Codeberg / mirror (push) Canceled after 0s
Mirror to PrivacyGuides / mirror (push) Canceled after 0s
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.
2026-07-23 12:56:19 -04:00
lockbitchat 0de8ab2d54 Replace free TURN with self-hosted coturn; fix Safari connectivity; release v5.4.10
CodeQL Analysis / Analyze CodeQL (push) Canceled after 0s
Deploy Application / deploy (push) Canceled after 0s
Mirror to Codeberg / mirror (push) Canceled after 0s
Mirror to PrivacyGuides / mirror (push) Canceled after 0s
- 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).
2026-07-22 18:10:17 -04:00
lockbitchat c98a01b1d5 Add end-to-end encrypted voice messages; release v5.4.5
CodeQL Analysis / Analyze CodeQL (push) Canceled after 0s
Deploy Application / deploy (push) Canceled after 0s
Mirror to Codeberg / mirror (push) Canceled after 0s
Mirror to PrivacyGuides / mirror (push) Canceled after 0s
- 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.
2026-07-22 00:41:58 -04:00
lockbitchat 0e3e3a2974 Add multiple-session support: run several independent encrypted chats at once
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled
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.
2026-06-26 00:00:13 -04:00
lockbitchat db5d6e481d seo: update page title to "SecureBit.chat — Private, Encrypted Messenger"
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled
2026-06-24 16:17:35 -04:00
lockbitchat ef2f13d053 seo: refine social card — SVG mark, more spacing, cache-busting filename
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled
- 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.
2026-06-24 16:13:39 -04:00
lockbitchat 62567fb5cc seo: rework title/description, add structured data, refine social card
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled
- 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.
2026-06-24 16:06:37 -04:00
lockbitchat ae6900d0c7 seo: add social preview image and fix Open Graph / Twitter meta
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled
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).
2026-06-24 16:00:04 -04:00
VolodymyrandGitHub 5eaf57c8da Fix formatting for Privacy by design section in README
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled
fix text
2026-06-24 15:44:54 -04:00
VolodymyrandGitHub ab49d7485d Fix formatting and headings in README.mdfix
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled
fix text
2026-06-24 15:43:16 -04:00
lockbitchat 96fffab765 release: v4.9.1 — redesigned QR scanner, UI fixes, README overhaul
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled
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.
2026-06-24 15:41:15 -04:00
lockbitchat cbf5c5f834 release: v4.9.0 — full redesign + reworked offline mode
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled
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.
2026-06-23 16:52:30 -04:00
lockbitchat b39f9ecd2c release: v4.8.20 secure chat tools — completed, fixed and polished
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled
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.
2026-06-19 02:58:03 -04:00
lockbitchat cb72b9cb1c fix: chat not opening after SAS in v4.8.14 (nowTick scope) — v4.8.15
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled
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.
2026-06-18 21:15:43 -04:00
lockbitchat 15173a9278 release: v4.8.14 secure chat tools (code blocks, view-once, disappearing, unsend, panic)
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled
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.
2026-06-18 20:37:50 -04:00
lockbitchat cf36656341 release: v4.8.13 message integrity & transport hardening
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled
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
2026-06-18 17:08:59 -04:00
lockbitchat 42be55aaeb fix(security): restore outgoing message integrity, add HSTS/Permissions-Policy
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled
- 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.
2026-06-18 16:48:29 -04:00
lockbitchat 6f36fce8c6 release: v4.8.12 chat notification & file-transfer UI fixes
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled
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
2026-06-17 17:51:09 -04:00
lockbitchat be1d02f1f7 release: v4.8.11 file transfer reliability fix
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled
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.
2026-06-16 18:24:29 -04:00
lockbitchat 9244250835 ux(ice-test): clarify result when browser hides reflexive/relay candidates (Safari)
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled
2026-06-16 01:50:40 -04:00
lockbitchat 6dac4ce52a fix(csp): allow stun:/turn: schemes in connect-src
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled
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.
2026-06-16 01:09:02 -04:00
lockbitchat 017a590220 fix(deploy): 404 missing assets instead of HTML fallback; ship public STUN config
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled
- nginx: asset extensions use try_files $uri =404 so a missing file (e.g.
  config/ice-servers.js) no longer serves index.html with the wrong content type
- add config/ice-servers.prod.js (public STUN, no secrets); Dockerfile copies it
  to the git-ignored config/ice-servers.js so the operator-override path exists
2026-06-15 16:30:39 -04:00
lockbitchat d58967c671 build: add Fly.io deployment (nginx static serving)
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled
- Dockerfile: serve the static PWA via nginx:alpine on port 8080
- deploy/nginx.conf: mirror .htaccess (jsx/mjs MIME, no-cache for shell/sw/manifest/meta/dist,
  long-immutable cache for assets, security headers, SPA fallback)
- fly.toml: internal_port 8080, force_https, scale-to-zero
- .dockerignore: exclude .git/node_modules/tests from the image
2026-06-15 16:23:14 -04:00
lockbitchat 39aad6ae8b release: v4.8.10 user-configurable STUN/TURN servers
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled
- 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
2026-06-15 16:05:31 -04:00
lockbitchat 7f2ecce57f feat: user-configurable STUN/TURN servers (advanced network settings)
- 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
2026-06-15 15:39:13 -04:00
lockbitchat 366f080128 release: v4.8.9 security hardening patch
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled
- upgrade DOMPurify to patched release (fixes high-severity XSS GHSA-87xg-pxx2-7hvx)
- upgrade esbuild build dependency; npm audit now reports 0 vulnerabilities
- stop tracking config/ice-servers.js (TURN credentials); add example template
- production logger no longer prints error context or info/debug output
- bump version to 4.8.9 across header, manifest, README, init message
- update SECURITY.md supported-release table to v4.8.x
2026-06-15 15:08:03 -04:00
lockbitchat d11f250257 release: v4.8.8 file transfer consent fix
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled
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.
2026-05-26 22:55:52 -04:00
lockbitchat 2468cb495e release: v4.8.7 WebRTC join reliability patch
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled
2026-05-19 09:49:22 -04:00
lockbitchat 1cc873223a fix: stabilize manual WebRTC join flow
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled
2026-05-18 19:49:57 -04:00
lockbitchat 01cb25f988 release: v4.8.6 security hardening patch
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled
2026-05-17 23:30:41 -04:00
lockbitchat ad3bee5f2e fix: enforce service worker cache allowlist
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled
2026-05-17 23:22:46 -04:00
lockbitchat f2a4276b31 fix: remove untracked disconnect timer
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled
2026-05-17 23:16:14 -04:00
lockbitchat 86a96b0121 fix: harden service worker cache policy 2026-05-17 23:13:06 -04:00
lockbitchat 33f3764ec5 fix: synchronize WebRTC privacy mode state 2026-05-17 23:09:45 -04:00
lockbitchat a04a70eb97 fix: throttle inbound file chunks 2026-05-17 23:05:43 -04:00
lockbitchat 0fbcc240be fix: add inbound message rate limiting 2026-05-17 23:01:58 -04:00
lockbitchat 18022c6b68 fix: gate debug window hooks behind explicit flag 2026-05-17 22:58:21 -04:00
168 changed files with 20130 additions and 6401 deletions
+16
View File
@@ -0,0 +1,16 @@
.git
.github
node_modules
tools
tests
Dockerfile
.dockerignore
fly.toml
*.log
.DS_Store
**/.DS_Store
# Source maps are debug-only and never needed at runtime; excluding them keeps the
# build context small (the .map files are several MB) and the deploy upload fast.
**/*.map
# Marketing screenshots are referenced only from the README, not served by the app.
assets/screenshots
+6
View File
@@ -4,3 +4,9 @@ node_modules/
# Local environment noise
.npm/
npm-debug.log*
.DS_Store
**/.DS_Store
# Operator ICE override holds TURN credentials — never commit it.
# Use config/ice-servers.example.js as the template.
config/ice-servers.js
+4
View File
@@ -143,6 +143,10 @@
Header set X-Content-Type-Options "nosniff"
Header set Referrer-Policy "strict-origin-when-cross-origin"
Header set X-Frame-Options "DENY"
# Force HTTPS (2 years + preload) to close the first-visit SSL-strip window.
Header set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
# Restrict powerful features; camera + microphone kept for QR scanning and calls.
Header set Permissions-Policy "camera=(self), microphone=(self), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()"
</IfModule>
# Content Security Policy (frame-ancestors and report-uri only work in HTTP headers, not meta tags)
+253 -1
View File
@@ -1,6 +1,258 @@
# Changelog
## v4.8.5 — Security hardening release
## v5.5.0 — Encrypted voice & video calls
SecureBit now supports **end-to-end encrypted voice and video calls** — the "5.5 Secure Voice & Calls" roadmap milestone.
### Added
- **Voice & video calls.** Start a call from the chat header (phone / camera buttons). Audio and video tracks ride the **same RTCPeerConnection as the chat**, bundled onto one **DTLS-SRTP** transport — so media is end-to-end encrypted with the very connection that in-person **SAS verification** authenticated. Call setup (SDP offer/answer) is renegotiated **in-band over the verified data channel**, never through a signalling server, so the media's DTLS fingerprints are authenticated end-to-end too. There is no server that can see or MITM a call.
- **Verification-gated.** Calls are only permitted once a session is **connected and SAS-verified**; the manager rejects call signalling otherwise. The header call buttons stay disabled until then.
- **In-call controls.** Mute / unmute, camera on/off (turning the camera on during a voice call upgrades it to video in-band), front/back camera flip, minimize-to-widget (chat stays usable) and hang up. Incoming calls show an accept / decline prompt.
- **Adaptive audio codec.** Opus tuned for real-world links — in-band FEC, DTX and RED redundancy (RED preferred first where the browser advertises it) keep speech intelligible under 1520% packet loss. Audio is bandwidth-prioritised and is **never** throttled by the network controller.
- **Adaptive video codec.** VP9 / AV1 single-encoding **SVC** (with H.264 / VP8 fallback), so video degrades gracefully by spatial/temporal layer on a weak link. A runtime `NetworkAdaptationController` reads `getStats()` every second and trims video bitrate on loss/RTT, recovering as the link clears — no renegotiation, no track restart.
- **Live connection-quality indicator.** Excellent → Good → Fair → Weak, surfaced in the voice overlay, video top bar and minimized widget. Codec tunables and their rationale are documented in [`docs/webrtc-config.md`](docs/webrtc-config.md).
### Security
- Media inherits the session's DTLS-SRTP encryption; SDP is exchanged only over the ECDH + SAS-authenticated data channel. No new ICE or signalling server is introduced — calls reuse the existing verified transport.
## v5.4.5 — Encrypted voice messages
SecureBit now supports **end-to-end encrypted voice messages**, sent over the same secure transfer channel as files.
### Added
- **Voice messages.** Record a note in the browser and send it over the existing chunked, AES-GCM-encrypted file-transfer pipeline, so it inherits the same per-file session key and SHA-256 integrity check. Audio is captured as raw PCM and encoded to **WAV**, so it plays back on every platform — including iOS/Safari. Duration and a downsampled waveform travel as unsigned presentation metadata; the audio bytes stay integrity-protected by the signed file hash.
- Voice notes are **auto-accepted** on the receiver (no consent prompt) and played **inline** from an in-memory blob — nothing is written to disk. The bubble shows an upload/download progress ring, a seekable waveform, play/pause and duration, and the usual Encrypted/Decrypted status.
- **Composer.** A mic button records a note with a live waveform and timer plus discard / send controls. On desktop the mic and send buttons sit side by side; on mobile the mic swaps to a send button as soon as you type text.
### Changed
- Content Security Policy `media-src` now allows `blob:` so recorded and received audio can be played back.
- Version scheme moved to the 5.x line (Desktop Edition = **5.0**). The roadmap gains a **5.5 "Secure Voice & Calls"** milestone (encrypted voice messages, audio calls, video calls); later milestones shift accordingly.
## v4.9.0 — Full redesign + reworked offline mode
A ground-up visual redesign of the whole application surface — landing page, "Why unique" / partners / roadmap / community sections, connection setup, in-chat header, real-time security verification report, file transfer, and the PWA install / update / offline / install-guide dialogs.
Offline experience reworked with store-and-forward over the live P2P channel:
- Messages sent while offline are queued (single ✓) and transmitted on reconnect, preserving their original send time.
- Messages to an offline peer stay at one check until that peer returns; the offline client holds them back and surfaces them on reconnect with a notice.
- WhatsApp-style per-message delivery status (sending → sent → delivered, plus a "not sent" state) via an authenticated delivery-receipt control message.
- Browser offline state no longer leaks into the P2P connection indicator.
Resilient file transfer: per-chunk segmented progress, receiver-driven retransmission of missing chunks with auto-resume after a connection blip, corrected receive rate limits, and automatic save on completion.
## v4.8.21 — Redesigned chat surface
A full visual refresh of the connected chat experience, ported from the SecureBit Chat design. No protocol, crypto or message-handling changes — only the presentation layer of the chat screen.
### Changed
- **Message bubbles** redesigned: tighter dark surface (`#0f0f11` canvas, `#26262b` sent / `#161618` received), asymmetric corner radii, monospace timestamps, and a compact per-message status row showing **Encrypted** / **Decrypted** with a lock glyph.
- **View-once** now uses a Telegram-style blurred cover with an SVG grain overlay and a centered "View once · tap to reveal" prompt; after reveal it shows a "Viewed once" tag and still burns after the sender-chosen window.
- **Disappearing timers** render a live `mm:ss` countdown in the message meta in brand orange.
- **Composer** rebuilt: inline `Send files` / `Code` / `View once` / `Timer` chips with active states, inline time-picker rows (view-once: Off/5s/10s/30s/1m, timer: Off/5s/30s/1m/1h/24h), an auto-growing message field, an "Encrypted on your device" affordance, a live character counter, and an orange send button.
- **Handshake summary** card at the top of a verified chat (collapsible): transport / cipher / key-exchange / integrity facts plus the safety number (key fingerprint).
- Fonts are mapped to the self-hosted **Inter** + system monospace stack rather than loading Google Fonts, preserving the look without an external request from a privacy-focused client.
## v4.8.20 — Secure chat tools: completed, fixed and polished
Completes the messaging controls introduced in v4.8.14 and fixes the bug that made them appear broken for recipients. All per-message options travel inside the encrypted message envelope (never in the sanitized text), so message content cannot spoof or corrupt them.
### Fixed
- **Per-message metadata was silently dropped for recipients.** `NotificationIntegration` wrapped both `webrtcManager.onMessage` and `webrtcManager.deliverMessageToUI` with two-argument shims that called the originals without the third argument (`meta`). With notifications enabled, every received message lost its `meta`, so view-once, disappearing timers and unsend all failed on the recipient side. Both wrappers now forward all arguments (`...rest`). Added `tests/notification-meta-forwarding.test.mjs`.
- **Chat would not open after SAS** (regression from the initial wiring): the composer props were threaded into the wrong component (`EnhancedConnectionSetup` instead of `EnhancedChatInterface`), throwing `ReferenceError: nowTick` on the verified-state re-render. Props are now on the chat component.
### Changed
- **Code blocks** now include lightweight, dependency-free syntax highlighting (comments, strings, numbers, keywords) rendered via React nodes — no `innerHTML`, no remote scripts. Enabling code mode expands the input (monospace, 8 rows) for comfortable entry. Copying a block auto-clears the clipboard after ~30s.
- **View-once** is now configurable: the sender picks how long the message stays visible after the peer opens it (5s / 15s / 30s / 1m) via `meta.onceTtl` (clamped 1s1h).
- **Disappearing timer** uses a duration picker (Off / 30s / 5m / 1h) instead of click-cycling.
- **Composer toolbar** moved next to the "Send files" control; borderless buttons with the brand-orange (`accent-orange`) active state; time pickers open upward and are sized for mobile readability.
- Sender bubble background lightened to `rgba(249, 115, 22, 0.05)`.
### Removed
- **Panic wipe** button. Disconnecting already wipes keys and clears session state, so a separate panic control was redundant.
## v4.8.15 — Fix: chat would not open after SAS in v4.8.14
### Fixed
- The secure chat failed to open after both peers confirmed the SAS code: the message list and composer (in `EnhancedChatInterface`) referenced `nowTick`, `onUnsendMessage` and the new composer props, but those were threaded into the sibling `EnhancedConnectionSetup` component by mistake. At runtime this threw `ReferenceError: Can't find variable: nowTick` during the verified-state re-render, so the chat never rendered. The new props are now destructured and passed on `EnhancedChatInterface`, where the chat UI actually lives. No behavioural change to the v4.8.14 features otherwise.
## v4.8.14 — Secure chat tools: code blocks, view-once, disappearing, unsend, panic
Adds privacy-focused messaging controls. Per-message metadata (id, view-once, timer) travels **inside the encrypted message envelope**, never in the sanitized text, so message content cannot spoof or corrupt these controls. The unsend/delete signal travels over the authenticated DTLS control channel like other system messages.
### Added
- **Code blocks.** A composer button wraps the message in a fenced block; both peers render it as a monospace code window with a copy button. The marker travels as ordinary text, and the window is built from already-sanitized text via React nodes only (no `dangerouslySetInnerHTML`), so there is no new XSS surface.
- **Clipboard auto-clear.** Copying a code block clears the clipboard after ~30s — only when it can confirm the clipboard still holds the copied value, or cannot read it back, so a later copy is never clobbered.
- **View-once messages.** The recipient sees a blurred bubble that reveals on tap and is then wiped. Honestly cooperative (a malicious client or a screenshot can still capture it) — this is hygiene, not a guarantee.
- **Disappearing messages.** An optional sticky timer (30s / 5m / 1h) auto-deletes a message on both sides, with a live countdown. The incoming timer value is clamped to [5s, 24h].
- **Unsend (delete for everyone).** Removes your message locally and asks the peer to drop it via a `message_delete` control message (`MESSAGE_TYPES.MESSAGE_DELETE`).
- **Panic wipe.** One button clears the conversation, wipes keys (`_secureWipeKeys`) and tears down the session, behind a confirm prompt.
### Security
- New per-message metadata is whitelisted and bounded by `_sanitizeMessageMeta` on both send and receive; unknown fields, wrong types and out-of-range timers are dropped.
- AAD/replay protection, the SAS verification gate and receive-side DOMPurify sanitization are unchanged.
### Tests
- Added `tests/secure-chat-features.test.mjs` covering metadata sanitization, meta delivery to the UI, and the unsend control path. Full suite: 17 files, all passing.
## v4.8.13 — Message integrity & transport hardening
Security review follow-up. The end-to-end cryptography (ECDH, AES-GCM, PBKDF2, SAS bound to DTLS fingerprints, anti-replay) was verified sound; these changes fix availability/integrity defects on the send path and tighten transport headers and logging.
### Fixed
- Outgoing messages were silently rejected by an over-broad keyword blocklist in `_validateInputData`. Plain words such as "constructor", "global", "document.", "prototype", or the literal text "javascript:" caused `sendSecureMessage` to throw, so legitimate messages never reached the peer. The blocklist provided no real protection: XSS is enforced at the rendering boundary by the receive-side DOMPurify pass and by `sanitizeMessage()` before encryption. The send-path blocklist was removed.
- `_sanitizeInputString` collapsed all whitespace (`/\s+/g` to a single space), destroying multi-line messages and code snippets (`"a\nb\nc"` became `"a b c"`). Newlines, tabs and indentation are now preserved; only control characters are stripped and runs of 3+ blank lines are collapsed to two.
- AAD validation failures logged the raw AAD string, which carried `sessionId` and `keyFingerprint`. Both the message and file-message validators now log only the AAD length.
### Security
- Added `Strict-Transport-Security` (`max-age=63072000; includeSubDomains; preload`) to `deploy/nginx.conf` and `.htaccess`, closing the first-visit SSL-strip window that `upgrade-insecure-requests` alone does not cover.
- Added a restrictive `Permissions-Policy` (`camera=(self)` for in-page QR scanning; microphone, geolocation, payment, usb and sensors denied).
### Tests
- Added `tests/outgoing-message-integrity.test.mjs` covering keyword acceptance, multi-line/indentation preservation, control-character stripping, blank-line collapsing, and the size limit.
## v4.8.12 — Chat notification & file-transfer UI fixes
Fixes duplicated chat output and a layout overflow in the message list.
### Fixed
- A received file was announced many times in the chat instead of once. The per-transfer lock used a single `if` check, so when 3+ chunk operations queued on the same file they ran concurrently and broke assembly atomicity. The lock now serializes correctly, and file assembly is idempotent, so `File received` is shown exactly once per file.
- System messages were duplicated during connection setup (e.g. "Both parties confirmed!" and "Secure connection successfully established"). `handleVerificationBothConfirmed` now bails out if both confirmations were already recorded, so the message and the verified transition fire only once.
- The DTLS fingerprint (a long unbroken string) overflowed the chat bubble. The message text container now uses `min-w-0` so the fingerprint wraps within the bubble.
- Site header, init banner, and manifest now report the current version.
## v4.8.11 — File transfer reliability fix
Fixes file transfers that silently failed to reach the peer, and relaxes the overly strict file-type check that rejected legitimate files.
### Fixed
- File chunks are now sized so the on-the-wire message stays under the 64 KB SCTP message-size limit enforced by WebRTC. Previously each 64 KB chunk became a ~87 KB encrypted+Base64 message that exceeded this limit, so the consent handshake succeeded but no data was ever delivered — most visibly on Safari and cross-browser connections whose SDP omits `a=max-message-size`. The send chunk size is now 16 KB (~22 KB on the wire); inbound chunks up to 64 KB are still accepted for backward compatibility.
### Changed
- File-type validation is now driven by the extension allow-list, with the (client-supplied, easily spoofed) MIME type treated as an advisory signal. Files with a missing MIME type or a cross-OS MIME variant (e.g. `application/x-zip-compressed` for `.zip`, `image/jpg` for `.jpg`) are no longer rejected. Blocked executable/script extensions, a blatantly foreign MIME on a safe extension, and per-type size limits are still enforced.
## v4.8.10 — User-configurable STUN/TURN servers
Adds optional, advanced control over WebRTC connectivity for power and privacy-focused users. Public servers remain the zero-config default.
### Added
- "Advanced network settings" panel (header gear icon and the connection-creation screen) where users can supply their own STUN/TURN servers instead of the bundled public defaults.
- Allowlist-based validation of user input: only `stun:`/`stuns:`/`turn:`/`turns:` URLs with valid hosts are accepted; `javascript:`, `data:`, `http(s):`, `ws(s):`, control characters, and oversized input are rejected before anything reaches `RTCPeerConnection`.
- Optional on-device persistence, encrypted at rest with a non-extractable AES-GCM device key in IndexedDB, with an explicit save prompt and a "Forget saved" action.
- "Test servers" button that gathers ICE candidates against the entered configuration and reports STUN/TURN reachability.
- Privacy guidance in the panel: a TURN relay sees peer IPs and traffic timing (never message contents), so only a trusted/self-hosted relay improves privacy.
### Changed
- Relay-only privacy mode now lives in the advanced settings panel. The standalone relay-only toggle on the start screen was removed to declutter the initial view.
- Server selection priority: user custom servers > operator override (`config/ice-servers.js`) > built-in public defaults.
## v4.8.9 — Security hardening patch
This release closes a vulnerable dependency, removes committed TURN credentials, and tightens production logging.
### Security
- Upgraded DOMPurify from 3.4.4 to a patched release, resolving a high-severity XSS advisory (GHSA-87xg-pxx2-7hvx) in the incoming-message sanitizer.
- Upgraded the `esbuild` build dependency to clear a high-severity advisory in the toolchain. `npm audit` now reports zero vulnerabilities.
- Stopped tracking `config/ice-servers.js` (operator TURN credentials) in Git and added `config/ice-servers.example.js` as a template. Operators must rotate any previously committed credentials.
- Removed temporary debug branches from the production logger so it no longer prints error context or info/debug payloads — only an opaque error code.
### Documentation
- Updated the supported-release table in `SECURITY.md` to the v4.8.x line.
- Synchronized the version string across the header, manifest, README, and in-app initialization message.
## v4.8.8 — File transfer consent fix
This patch completes the mandatory receiver-consent gate for incoming file transfers and resolves a callback ownership conflict that caused every incoming file request to be silently auto-rejected.
### Fixed
- Wired up the missing fourth `onIncomingFileRequest` callback in the main `setFileTransferCallbacks` call. Without it, `handleFileTransferStart` always saw `null` for the consent handler and auto-rejected every incoming file silently.
- Removed independent callback registration from `FileTransferComponent`. The component was overwriting the application-level callbacks on mount and nulling all four on unmount, which destroyed the progress, received, and error handlers whenever the panel was hidden.
- Centralized incoming-consent state (`pendingIncomingFiles`) in the root application component so consent prompts appear regardless of whether the file-transfer panel is currently visible.
- Auto-opens the file-transfer panel when an incoming request arrives so the user sees the Accept / Reject prompt immediately.
- Added `getReceivedFileObjectURL` / `revokeReceivedFileObjectURL` helpers to `EnhancedSecureWebRTCManager` so the panel can offer a download button for completed transfers without relying on captured callback closures.
- Updated `file-transfer-ui-cleanup` regression test to match the new single-owner callback architecture.
### Security
No change to the cryptographic or transport-level security model. Sender chunks are still gated behind an explicit `file_transfer_response` from the receiver before any data is transmitted.
### Verification
- `npm test` — all 14 tests pass.
- `npm run build` — clean production build.
## v4.8.7 — WebRTC manual join reliability patch
This patch improves manual WebRTC setup across separate devices and restrictive local networks.
### Fixed
- Stabilized the manual offer/answer join flow so verification waits for real transport readiness.
- Preserved generated response data during manual exchange instead of resetting the joiner screen prematurely.
- Preserved pending creator-side offer context so responses can be applied after transient ICE failures without false session-salt hijacking errors.
- Added operator ICE override support through `config/ice-servers.js`.
- Added ExpressTURN TURN/STUN configuration for relay fallback in environments where mDNS host candidates cannot connect.
- Added user-visible warning when a remote peer provides only mDNS host candidates and no `srflx` or `relay` route.
- Added safer ICE diagnostics that report candidate classes without exposing full IP addresses or TURN credentials.
### Verification
- `npm test`
- `npm run build`
## v4.8.7 — Security hardening patch release
This patch release strengthens SecureBit.chat across verification, sanitization, privacy, transport abuse resistance, cache safety, and repository hygiene.
### Security hardening
- Bound SAS verification to the actual DTLS fingerprint strings of both peers.
- Replaced regex-based chat sanitization with DOMPurify-backed sanitization.
- Made WebRTC privacy mode explicit and kept relay-only state synchronized at runtime.
- Removed production exposure of internal debug/control hooks.
- Added receiver-side rate limiting for inbound chat messages.
- Added receiver-side throttling for inbound file chunks.
### Runtime and privacy safety
- Hardened service-worker caching so only explicitly allowlisted safe assets are cached.
- Removed an untracked disconnect timer so teardown no longer leaves delayed callbacks behind.
- Preserved relay-only TURN behavior while making privacy implications clearer when relay-only mode is disabled or TURN is unavailable.
### Repository hygiene
- Stopped tracking `node_modules` in Git so platform-specific dependency binaries no longer pollute the repository or break cross-platform builds.
### Validation
- Full regression suite passes.
- Clean install succeeds with `npm ci`.
- Production build succeeds with `npm run build`.
## v4.8.7 — Security hardening release
This release consolidates several months of security, privacy, and lifecycle hardening work by the SecureBit.chat team.
+21
View File
@@ -0,0 +1,21 @@
# SecureBit.chat is a static PWA (no backend). The committed build artifacts in
# dist/ are served as-is by nginx — matching the project's release workflow,
# where dist/ is rebuilt and committed for every release.
FROM nginx:1.27-alpine
# Replace the default nginx config with our static-serving config.
COPY deploy/nginx.conf /etc/nginx/nginx.conf
# Serve the repository (src/, assets/, libs/, dist/, config/, logo/, sw.js, ...).
COPY . /usr/share/nginx/html
# config/ice-servers.js is git-ignored (it can hold operator TURN credentials),
# so it is absent from the build context. Provide the public-STUN production
# override so the operator-override path is populated and nothing 404s.
RUN cp /usr/share/nginx/html/config/ice-servers.prod.js \
/usr/share/nginx/html/config/ice-servers.js
# Fly.io health checks and routing target this port.
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]
+143 -73
View File
@@ -1,34 +1,111 @@
# SecureBit.chat v4.8.5
<div align="center">
SecureBit.chat is a browser-based peer-to-peer chat application built on WebRTC and Web Crypto APIs. It is designed for direct encrypted communication, explicit peer verification, and a small operational footprint without account registration or server-side message storage.
<img src="logo/securebit-logo.png" width="120" alt="SecureBit.chat" />
# SecureBit.chat
**End-to-end encrypted, peer-to-peer chat that runs entirely in your browser.**
No accounts. No servers storing your messages. No installation required.
[![License: MIT](https://img.shields.io/badge/License-MIT-f0892a.svg)](LICENSE)
[![Version](https://img.shields.io/badge/version-5.5.0-3ecf8e.svg)](CHANGELOG.md)
[![PWA](https://img.shields.io/badge/PWA-installable-3ecf8e.svg)](#install-as-an-app)
[![Encryption](https://img.shields.io/badge/crypto-ECDH%20P--384%20%C2%B7%20AES--256--GCM-blue.svg)](#security-model)
[Features](#features) · [How it works](#how-it-works) · [Security](#security-model) · [Quick start](#quick-start) · [Documentation](#documentation)
</div>
---
SecureBit.chat is a browser-based, peer-to-peer messenger built on **WebRTC** and the **Web Crypto API**. Two people establish a direct, end-to-end encrypted channel and verify each other in person — there is no registration, no central server relaying or storing messages, and no metadata account to leak. Everything cryptographic happens locally in the two browsers.
It is designed for people who need a small, auditable, zero-infrastructure way to talk privately: journalists and sources, security researchers, or anyone who simply wants a conversation that leaves nothing behind.
## Screenshots
| Open a secure channel | Encrypted conversation |
| :---: | :---: |
| ![Open a channel](assets/screenshots/login.png) | ![Encrypted chat](assets/screenshots/chat.png) |
## Features
** Encryption & verification**
- ECDH P-384 key agreement with derived per-session keys, AES-256-GCM payloads, and DTLS-protected transport.
- Interactive **Short Authentication String (SAS)** verification — you confirm a code out-of-band before the session is trusted, defeating man-in-the-middle attacks.
- Replay protection, message integrity (HMAC), and a live security report you can open at any time during a call.
** Privacy by design**
- Direct peer-to-peer connection — messages never touch a SecureBit server.
- No accounts, no phone numbers, no message history on disk.
- Optional **relay-only mode** routes traffic through your own TURN server so your IP is never exposed to the peer.
- Local key metadata is stored encrypted in IndexedDB; disconnecting cleans up session state.
** Encrypted calls**
- **1:1 voice and video calls** over the same verified peer-to-peer connection — media rides the SAS-verified DTLS-SRTP transport, so calls inherit the session's end-to-end encryption and never traverse a SecureBit server.
- **Adaptive audio**: Opus with in-band FEC, DTX and RED redundancy for intelligible speech under 1520% packet loss; audio is prioritised and never throttled by the network controller.
- **Adaptive video**: VP9/AV1 single-encoding SVC (H.264/VP8 fallback) that degrades by spatial/temporal layer, with a runtime controller that trims video bitrate on loss/RTT and recovers as the link clears.
- **Live connection-quality indicator** (Excellent → Good → Fair → Weak) shown in the call UI, plus in-call mute and video-upgrade controls.
** Messaging**
- **Encrypted voice messages** — record in the browser and send over the same end-to-end encrypted transfer channel as files. Audio is captured as PCM/WAV, integrity-protected by a signed hash, and played back inline on the recipient's device without ever touching disk.
- Code blocks with syntax highlighting and an auto-clearing copy button.
- View-once and disappearing messages with countdown timers.
- Unsend (delete for everyone) over the authenticated control channel.
- WhatsApp-style delivery status (sending → sent → delivered) with offline store-and-forward.
**Multiple conversations**
- Run several independent chats at the same time. Every conversation gets its own encrypted session, keys and verification, so two chats can never mix.
- A side panel lists your open chats with unread badges. Switching is instant, and starting a new chat leaves the others connected.
- Set your availability (Available, Away, Busy or Invisible) and connected peers can see it. You can also give each chat a private label that is stored only on your device and is never sent to the other side.
** File transfer**
- Consent-gated, end-to-end encrypted transfers with resumable, per-chunk progress.
- Strict file-type allowlist; executable and scriptable formats are rejected.
** Progressive Web App**
- Installable on desktop and mobile, works offline, and ships update notifications.
## How it works
SecureBit never sees your conversation. A session is built directly between the two browsers:
```
Peer A Peer B
│ 1. create encrypted offer │
│ ────────────────────────────► │ (shared out-of-band: QR / link / paste)
│ │
│ 2. return encrypted answer │
│ ◄──────────────────────────── │
│ │
│ 3. compare SAS code aloud │
│ ✓ both confirm → verified │
│ │
│ ═══ end-to-end encrypted ════ │
```
1. **Peer A** creates an offer (sharable as a QR code, link, or text).
2. **Peer B** opens it and returns an answer the same way.
3. Both sides see a **SAS code** and compare it over a trusted channel (in person, a call you recognize, etc.).
4. Only after both peers confirm the matching code does the chat unlock. Three failed attempts terminate the session.
## Security model
SecureBit.chat uses:
| Layer | Mechanism |
| --- | --- |
| Key agreement | ECDH (P-384), per-session derived keys |
| Transport | WebRTC data channel over DTLS |
| Message encryption | AES-256-GCM, end-to-end |
| Authentication | Interactive SAS bound to both peers' DTLS fingerprints |
| Integrity | HMAC + replay protection |
| Sanitization | DOMPurify text-only rendering boundary |
| Local storage | Encrypted key metadata in IndexedDB |
- ECDH key agreement with derived session keys
- DTLS-protected WebRTC transport
- deterministic Short Authentication String (SAS) verification
- end-to-end encrypted chat payloads
- replay protection and session-state cleanup
- encrypted local key metadata in IndexedDB
A session is **not** treated as verified until both peers complete the SAS flow. This is the step that protects you against a man-in-the-middle: the code must be compared through a channel an attacker cannot impersonate.
A session is not treated as verified until both peers complete the interactive SAS flow. Each user must compare the displayed code with the peer through an out-of-band channel and enter the matching code manually. Three failed SAS attempts terminate the session.
## Highlights in v4.8.5
This release consolidates several months of security hardening work by the project team:
- mandatory interactive SAS verification instead of passive click-through confirmation
- deterministic SAS computation from shared session material
- protocol version `4.1` negotiation with mismatch rejection
- optional TURN relay-only privacy mode with clear warnings when TURN is unavailable
- encrypted IndexedDB metadata with lazy migration from legacy plaintext records
- explicit file-transfer consent before any receive buffers are allocated
- strict file-type allowlist using both MIME type and extension checks
- incoming decrypted message sanitization before UI delivery
- improved disconnect, timer, file-transfer, and React UI cleanup behavior
- pinned dependency versions and a clean `npm audit` baseline
> [!WARNING]
> SecureBit.chat is privacy software, not a guarantee. View-once and disappearing messages are cooperative (not screenshot-proof), and a TURN relay can observe both peers' IPs and traffic timing — though never message contents. See [`SECURITY_DISCLAIMER.md`](SECURITY_DISCLAIMER.md).
## Quick start
@@ -40,82 +117,75 @@ npm run build
npm run serve
```
Then open the local server URL in two browser windows or profiles.
Open the printed local URL in two browser windows or profiles, then:
### Establish a session
1. Create an offer in the first window.
2. Transfer it to the second and create an answer.
3. Return the answer to the first window.
4. Compare the SAS code out-of-band and enter it on both sides.
5. Start chatting once both peers are verified.
1. Create an offer in the first browser.
2. Transfer the offer to the peer and create an answer.
3. Return the answer to the first browser.
4. Compare the SAS code out of band.
5. Enter the matching SAS code on both sides.
6. Begin chatting only after both peers are verified.
### Install as an app
SecureBit is a PWA — open it in a supported browser and choose **Install** (or *Add to Home Screen* on mobile) to run it as a standalone, offline-capable app.
## Configuration
### TURN / privacy mode
Direct WebRTC connections may expose IP addresses to peers. SecureBit.chat supports a relay-only privacy mode:
Direct WebRTC connections can reveal IP addresses to the peer. SecureBit supports a relay-only privacy mode:
- default mode keeps normal WebRTC behavior and existing STUN support
- relay-only mode sets `iceTransportPolicy: "relay"`
- relay-only mode requires a configured TURN server
- STUN alone does not hide IP addresses
- public TURN credentials are not bundled or hardcoded
- **Default** keeps standard WebRTC behavior with public STUN.
- **Relay-only** sets `iceTransportPolicy: "relay"` and requires a configured TURN server.
- STUN alone does not hide IP addresses; public TURN credentials are never bundled.
Configure ICE servers at deployment time and enable relay-only mode only when a TURN service is available. See [`doc/CONFIGURATION.md`](doc/CONFIGURATION.md).
Configure your own STUN/TURN servers under **Advanced network settings**, or at deployment time. See [`doc/CONFIGURATION.md`](doc/CONFIGURATION.md).
### File transfer policy
Incoming file transfers require explicit user consent. Before the consent prompt appears, metadata is validated and dangerous names are rejected. Safe accepted categories are:
- common raster images
- PDF
- plain text
- ZIP archives
Executable, scriptable, and high-risk formats are rejected, including `.exe`, `.bat`, `.cmd`, `.sh`, `.js`, `.msi`, `.dmg`, `.app`, `.jar`, `.scr`, `.ps1`, `.vbs`, `.html`, and `.svg`. MIME type and filename extension must agree.
Incoming transfers require explicit consent. Metadata is validated and dangerous names rejected before the prompt appears. Accepted: common raster images, PDF, plain text, and ZIP. Executable/scriptable formats (`.exe`, `.bat`, `.sh`, `.js`, `.msi`, `.dmg`, `.jar`, `.ps1`, `.vbs`, `.html`, `.svg`, …) are blocked, and MIME type must agree with the file extension.
## Development
### Requirements
- Node.js 18+
- npm
### Commands
**Requirements:** Node.js 18+ and npm.
```bash
npm install
npm test
npm audit
npm run build
npm run dev
npm test # run the test suite
npm audit # check dependencies
npm run build # build CSS + JS bundles and refresh meta.json
npm run dev # build and serve locally
```
### Project layout
### Project structure
```text
src/network/ WebRTC connection and session lifecycle
src/transfer/ secure file-transfer implementation
src/crypto/ cryptographic utilities
src/components React UI components
doc/ technical documentation
src/network/ WebRTC connection and session lifecycle
src/transfer/ secure file-transfer implementation
src/crypto/ cryptographic utilities
src/components/ React UI components
src/styles/ component styles
doc/ technical documentation
dist/ built bundles served in production
```
## Documentation
- [`SECURITY.md`](SECURITY.md)
- [`doc/CONFIGURATION.md`](doc/CONFIGURATION.md)
- [`doc/CRYPTOGRAPHY.md`](doc/CRYPTOGRAPHY.md)
- [`doc/SECURITY-ARCHITECTURE.md`](doc/SECURITY-ARCHITECTURE.md)
- [`doc/API.md`](doc/API.md)
- [`CHANGELOG.md`](CHANGELOG.md)
- [`SECURITY.md`](SECURITY.md) — security policy & reporting
- [`doc/CONFIGURATION.md`](doc/CONFIGURATION.md) — deployment & ICE configuration
- [`doc/CRYPTOGRAPHY.md`](doc/CRYPTOGRAPHY.md) — cryptographic design
- [`doc/SECURITY-ARCHITECTURE.md`](doc/SECURITY-ARCHITECTURE.md) — architecture overview
- [`doc/API.md`](doc/API.md) — internal APIs
- [`CHANGELOG.md`](CHANGELOG.md) — full release history
## Responsible use
## Contributing & responsible use
SecureBit.chat is intended for lawful, ethical use. See [`RESPONSIBLE_USE.md`](RESPONSIBLE_USE.md) and [`SECURITY_DISCLAIMER.md`](SECURITY_DISCLAIMER.md).
Issues and pull requests are welcome. SecureBit.chat is intended for lawful, ethical communication only — please read [`RESPONSIBLE_USE.md`](RESPONSIBLE_USE.md) before using or contributing.
## License
MIT License. See [`LICENSE`](LICENSE).
Released under the [MIT License](LICENSE).
<div align="center">
<sub>Built with WebRTC and the Web Crypto API · No servers, no accounts, no compromises.</sub>
</div>
+2 -1
View File
@@ -4,7 +4,8 @@
| Release | Status | Protocol |
| --- | --- | --- |
| v4.1.x | Supported | 4.1 |
| v4.8.x | Supported | 4.1 |
| v4.1.x v4.7.x | Unsupported | 4.1 |
| earlier releases | Unsupported | legacy |
Users should run the current supported release line to receive the latest verification, storage, and file-transfer protections.
+1 -1
View File
@@ -22,6 +22,6 @@ SecureBit.chat is intended for legitimate private communication, journalism, res
## Current release
- Product release: `v4.8.5`
- Product release: `v4.8.21`
- Protocol version: `4.1`
- Last updated: May 17, 2026
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+69 -12
View File
@@ -1,36 +1,93 @@
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v19/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuOKfMZg.ttf) format('truetype');
}
/* Inter — self-hosted (latin + latin-ext subsets, weights 400/500/600/700/800).
Generated from Google Fonts css2 to remove the third-party fonts.gstatic.com
dependency (privacy) and ship compact woff2 instead of full TTF. */
/* latin-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v19/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfMZg.ttf) format('truetype');
src: url(files/inter-latin-ext-400.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(files/inter-latin-400.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v19/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuI6fMZg.ttf) format('truetype');
src: url(files/inter-latin-ext-500.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(files/inter-latin-500.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v19/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuGKYMZg.ttf) format('truetype');
src: url(files/inter-latin-ext-600.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(files/inter-latin-600.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/inter/v19/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuFuYMZg.ttf) format('truetype');
src: url(files/inter-latin-ext-700.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(files/inter-latin-700.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 800;
font-display: swap;
src: url(files/inter-latin-ext-800.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 800;
font-display: swap;
src: url(files/inter-latin-800.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 760 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

+1 -1
View File
File diff suppressed because one or more lines are too long
+21
View File
@@ -0,0 +1,21 @@
// SecureBit.chat operator ICE server override — TEMPLATE.
//
// Copy this file to `config/ice-servers.js` and fill in your own TURN/STUN
// servers. The real `config/ice-servers.js` is git-ignored on purpose:
// TURN credentials are visible to every browser that loads the page, so they
// must never be committed to a public repository. Rotate them from your TURN
// provider dashboard if they are ever exposed.
//
// If this override is absent, the WebRTC manager falls back to the built-in
// public STUN defaults (standard mode only — no relay/IP protection).
window.SECUREBIT_ICE_SERVERS = [
{ urls: 'stun:stun.cloudflare.com:3478' },
{
urls: [
'turn:YOUR_TURN_HOST:3478?transport=udp',
'turn:YOUR_TURN_HOST:3478?transport=tcp'
],
username: 'YOUR_TURN_USERNAME',
credential: 'YOUR_TURN_CREDENTIAL'
}
];
+48
View File
@@ -0,0 +1,48 @@
// Production ICE override baked into the Fly.io image (no secrets — public STUN
// only). The Dockerfile copies this to config/ice-servers.js, which is otherwise
// git-ignored. Users who want a TURN relay can add one via "Advanced network
// settings"; to ship an operator TURN here, add it below (TURN credentials are
// visible to every browser, so rotate them if exposed).
window.SECUREBIT_ICE_SERVERS = [
{ urls: 'stun:stun.cloudflare.com:3478' },
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
// Raw-IP STUN (same coturn box as turn.securebit.chat). Required fallback: some
// clients — notably Safari on certain networks — fail to resolve STUN/TURN
// hostnames inside their WebRTC layer even though normal page DNS works, so they
// gather zero srflx/relay candidates and can't connect. Reaching the server by IP
// bypasses that. Harmless to other browsers.
{ urls: 'stun:144.172.96.126:3478' },
{
// SecureBit self-hosted coturn relay (turn.securebit.chat) — used only when
// direct P2P fails (strict NAT / STUN limits). Media stays E2E-encrypted over
// DTLS; the relay forwards only ciphertext. Multiple transports are offered so
// every browser finds one that works:
// - turn:3478?transport=udp — plain UDP, the most universally compatible
// transport (notably the one Safari reliably gathers relay candidates on).
// - turn:3478?transport=tcp — TCP fallback when UDP is blocked.
// - turns:443?transport=tcp — TURN over TLS on 443, bypasses restrictive
// firewalls that only allow HTTPS.
//
// The credential below is a long-lived token derived from the server's TURN
// REST API secret (static-auth-secret) — username is "<expiry-unix-ts>:label".
// Expiry 2147483647 = 2038-01-19 (max value coturn accepts; it parses the
// timestamp as int32). The master secret itself never leaves the server; only
// this derived username/credential pair is public (by design, like any
// browser-side TURN credential). To revoke/renew, rotate the server's
// static-auth-secret and re-issue with generate_turn_credentials.py.
urls: [
'turn:turn.securebit.chat:3478?transport=udp',
'turn:turn.securebit.chat:3478?transport=tcp',
// Raw-IP TURN relay — the DNS-bypass fallback that lets Safari (and any client
// whose WebRTC layer can't resolve the hostname) obtain a relay candidate.
// Plain turn: (no TLS) so no cert/hostname check is needed; the REST-API
// credential is host-independent, and relayed traffic is already E2E ciphertext.
'turn:144.172.96.126:3478?transport=udp',
'turn:144.172.96.126:3478?transport=tcp',
'turns:turn.securebit.chat:443?transport=tcp'
],
username: '2147483647:securebit',
credential: 'tNrw3h2p/+OE0uLMLVc+ech7T6o='
}
];
+111
View File
@@ -0,0 +1,111 @@
# nginx config for serving SecureBit.chat (static PWA) on Fly.io.
# Mirrors the behavior of the Apache .htaccess: correct JS MIME for ES modules
# (.jsx/.mjs), no-cache for the app shell / service worker / versioning files,
# long-immutable cache for hashed/static assets, security headers, SPA fallback.
worker_processes auto;
events { worker_connections 1024; }
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# ES modules must be served as JavaScript. nginx's default mime.types maps
# .js but not .mjs/.jsx — declare them explicitly (this overrides .js too).
types {
application/javascript js mjs jsx;
text/css css;
application/json json map;
application/manifest+json webmanifest;
font/woff2 woff2;
font/woff woff;
image/svg+xml svg;
}
sendfile on;
tcp_nopush on;
server_tokens off;
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 256;
gzip_types text/plain text/css text/javascript
application/javascript application/json application/manifest+json
application/ld+json application/wasm
image/svg+xml image/x-icon font/woff2;
# Decide Cache-Control from the request path. Keeping all add_header calls at
# one level avoids nginx's header-inheritance reset between blocks.
map $uri $sb_cache {
default "public, max-age=31536000, immutable";
~^/index\.html$ "no-cache, no-store, must-revalidate";
~^/$ "no-cache, no-store, must-revalidate";
~^/sw\.js$ "no-cache, no-store, must-revalidate";
~^/manifest\.json$ "no-cache, no-store, must-revalidate";
~^/meta\.json$ "no-cache, no-store, must-revalidate";
# Operator ICE/TURN config: must never be cached long, or the browser
# locks onto a stale server list (default was max-age=1y immutable).
~^/config/ice-servers\.js$ "no-cache, no-store, must-revalidate";
# dist/ bundles are query-versioned (?v=) in index.html. "no-cache" forces
# revalidation on every load, but dropping "no-store" lets the browser reuse
# the cached copy on a 304 — avoiding a full re-download of the large bundles
# (e.g. qr-local.js ~1.2MB) when nothing changed, while still picking up
# new releases immediately.
~^/dist/ "no-cache, must-revalidate";
}
# CDN-Cache-Control is read by Cloudflare (and other CDNs) *independently* of the
# browser Cache-Control above, and takes precedence at the edge. We force "no-store"
# for the app shell, service worker and version files so a CDN can never serve a
# stale meta.json / index.html / sw.js — which would silently break the in-app
# update notification. The empty default emits no header (nginx skips empty values),
# so immutable hashed assets keep being edge-cached normally for performance.
map $uri $sb_cdn_cache {
default "";
~^/index\.html$ "no-store";
~^/$ "no-store";
~^/sw\.js$ "no-store";
~^/manifest\.json$ "no-store";
~^/meta\.json$ "no-store";
~^/config/ice-servers\.js$ "no-store";
~^/dist/ "no-store";
}
server {
listen 8080;
listen [::]:8080;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Security headers (frame-ancestors complements the in-page CSP meta tag).
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-Frame-Options "DENY" always;
add_header Content-Security-Policy "frame-ancestors 'none';" always;
# Force HTTPS for two years and preload, closing the first-visit SSL-strip
# window that upgrade-insecure-requests alone does not cover.
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# Lock down powerful features. Camera + microphone are allowed for QR
# scanning and encrypted voice/video calls; other sensors are denied.
add_header Permissions-Policy "camera=(self), microphone=(self), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()" always;
add_header Cache-Control $sb_cache always;
# Edge-cache directive for Cloudflare/CDNs (empty value → header is omitted).
add_header CDN-Cache-Control $sb_cdn_cache always;
add_header Service-Worker-Allowed "/" always;
# Real asset files must return 404 when missing — never fall back to the
# HTML shell, which would be served with the wrong content type and break
# module/script loading (e.g. a missing config/ice-servers.js).
location ~* \.(js|mjs|jsx|css|json|map|woff2?|ttf|otf|png|jpe?g|gif|webp|svg|ico|mp3|mp4|webm)$ {
try_files $uri =404;
}
# SPA-style fallback so unknown navigation routes still load the app shell.
location / {
try_files $uri $uri/ /index.html;
}
}
}
+5085 -1475
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
File diff suppressed because one or more lines are too long
Vendored
+3708 -1585
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
File diff suppressed because one or more lines are too long
+5 -1
View File
@@ -5,7 +5,11 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
try {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
} catch (e) {
throw mod = 0, e;
}
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
+2 -2
View File
File diff suppressed because one or more lines are too long
+10
View File
@@ -18,6 +18,16 @@
- `setFileTransferCallbacks(onProgress, onReceived, onError, onIncomingRequest)` updates manager fields and any live `EnhancedSecureFileTransfer` instance.
- Passing `null` values detaches callbacks from the active transfer system.
### Voice messages
- `sendFile(file, options)` accepts an optional `options` object. `options.voice`
(`{ dur, bars }`) marks the transfer as a voice note and rides along as unsigned
metadata; `options.uiId` correlates progress events to a UI bubble before the
`fileId` resolves.
- `onProgress` receives `{ fileId, uiId, direction, progress, isVoice, voice }`.
`onIncomingFileRequest` and `onReceived` include `isVoice` and `voice` so the UI
can auto-accept and render a voice bubble instead of a file card.
## EnhancedSecureFileTransfer
### Incoming transfers
+5 -2
View File
@@ -2,7 +2,7 @@
## Release context
- Product release: `v4.8.5`
- Product release: `v5.4.5`
- Protocol version: `4.1`
## Session establishment
@@ -16,6 +16,9 @@ The SAS is deterministic for both peers in the same authenticated session. Users
- encrypted payloads are validated before decryption
- decrypted chat text is sanitized before entering React state or the UI
- replay and ordering controls remain part of the session layer
- voice messages are transported over the file-transfer channel: each is
encrypted with a per-file AES-GCM session key and integrity-checked with a
signed SHA-256 hash before playback
## Local key metadata
@@ -23,4 +26,4 @@ Sensitive IndexedDB metadata is stored in encrypted envelopes. Legacy plaintext
## Scope note
This document describes the current browser implementation behavior relevant to the v4.8.5 hardening release. It does not replace independent cryptographic review.
This document describes the current browser implementation behavior relevant to the v5.4.5 release. It does not replace independent cryptographic review.
+13
View File
@@ -10,6 +10,7 @@
| Privacy mode | optional TURN relay-only mode |
| Message UI safety | incoming decrypted text sanitized before display |
| File transfer | validated metadata, explicit consent, allowlist policy |
| Voice messages | same chunked AES-GCM transfer as files; auto-accepted and played inline |
| Local metadata | encrypted IndexedDB envelopes with migration |
| Lifecycle | unified disconnect cleanup and bounded resource retention |
@@ -42,6 +43,18 @@ The verified state is reached only when both local and remote confirmation flags
5. sender transmits chunks only after acceptance
6. completed received buffers are retained within a bounded window
## Voice messages
Voice notes reuse the file-transfer pipeline, so they inherit its per-file
AES-GCM session key, chunking, and SHA-256 integrity check. Differences from a
regular file:
1. audio is recorded in-browser and encoded as PCM/WAV before sending
2. duration and a downsampled waveform travel as **unsigned** presentation
metadata; the audio bytes remain integrity-protected by the signed file hash
3. the receiver **auto-accepts** voice transfers (no consent prompt) and plays
them inline from an in-memory blob — nothing is written to disk
## Disconnect cleanup
The canonical disconnect path clears:
+143
View File
@@ -0,0 +1,143 @@
# WebRTC Audit — SecureBit.chat call stack
**Step 1 deliverable. No code changed.** This maps the existing WebRTC/call code so we can plan the adaptive voice/video stack (Opus FEC/DTX/RED, VP9-SVC + fallbacks, TWCC/NACK/PLI, getStats adaptation) *without* creating a parallel branch.
---
## 0. Reality vs. the task spec (read this first)
The task is written against a modular TypeScript layout (`src/webrtc/codecs/*.ts`, `call.ts`, `config.ts`, …). **That layout does not exist and does not match the repo.** Concretely:
| Task assumption | Actual repo |
|---|---|
| TypeScript (`.ts`) | Plain JS / JSX. No `tsconfig`, no `.ts` files, esbuild bundles JS as-is. |
| `src/webrtc/` modular stack | One monolith: `src/network/EnhancedSecureWebRTCManager.js` (~14.8k lines). |
| Fresh `RTCPeerConnection` per call, `addTransceiver` at init | **One long-lived PC** shared with the encrypted **data channel**; calls are **renegotiated onto it**. Media is added with `addTrack`, **never** `addTransceiver`. |
| Standard signalling server / offer at init | **Two separate SDP paths** (see §5). Call SDP is exchanged **in-band over the E2E data channel**. |
| Playwright available | Not installed. Tests are plain `node tests/*.test.mjs`. No `RTCPeerConnection` in Node. |
| `debug('webrtc:adapt')` | No `debug` dependency. Logging is `_secureLog(...)` + the new `window.sbCallLog(...)`. |
**Consequence:** we integrate into the existing manager + a small set of **new plain-JS helper modules** it imports. We do **not** add TypeScript or a `src/webrtc/` TS tree. Proposed JS layout in §7.
---
## 1. Module map
| File | Kind | Exports / globals | Role |
|---|---|---|---|
| `src/network/EnhancedSecureWebRTCManager.js` | ES module | `export { EnhancedSecureWebRTCManager, SecureMasterKeyManager, SecureIndexedDBWrapper, SecurePersistentKeyStorage }` (line 14812). Also `window.EnhancedSecureWebRTCManager`. | **All** transport + crypto + call logic. The class to extend. |
| `src/components/ui/CallUI.jsx` | ES module (side-effect) | `window.CallUIComponent`, `export { CallUIComponent }` | Presentational call overlay (voice/video/minimized/incoming). Pure UI; no media/crypto. |
| `src/scripts/app-boot.js` | bundled entry | imports the manager + UI components; sets `window.*` | Real loader (→ `dist/app-boot.js`). CallUI is imported here. |
| `src/app.jsx` | bundled entry | React app (`h`/createElement) | Header call buttons + mounts `CallUI`. getUserMedia for **voice messages** (unrelated to calls) at 816817. |
| `config/ice-servers*.js` | script | `window.SECUREBIT_ICE_SERVERS` | Operator STUN/TURN override. |
| `src/network/iceServers.js`, `iceSettingsStore.js` | ES modules | ICE validation/persistence | User-supplied ICE servers. |
> `src/scripts/bootstrap-modules.js` exists but is **dead** (not referenced anywhere). Ignore it.
---
## 2. RTCPeerConnection: creation & config
- **`createPeerConnection()`** — `EnhancedSecureWebRTCManager.js:7731`. Single `new RTCPeerConnection(config)` at **7737**. Wires `onconnectionstatechange`, `oniceconnectionstatechange`, `onicecandidateerror`, **`ontrack`** (7802, call media), `ondatachannel`.
- Called from **9983** (initiator/offer path) and **10646** (responder/answer path).
- **`_buildPeerConnectionConfig()`** — `7661`. Returns:
```js
{ iceServers, iceCandidatePoolSize: 10, bundlePolicy: 'balanced' } // + iceTransportPolicy:'relay' in privacy mode
```
No `rtcpMuxPolicy`, no codec/degradation config (nothing to do there; those live on senders/transceivers).
- One PC per session; multi-session keeps a `Map<id, manager>` (each with its own PC).
## 3. SDP formation
No codec munging exists today — only **DTLS fingerprint extraction** (`_extractDTLSFingerprintFromSDP`) and **ICE candidate diagnostics** (`_summarizeIceCandidatesInSDP` 1057, `_logIceCandidateDiagnostics` 1119, `_warnIfRemoteCandidatesNeedRelay` 1145).
| Op | Line(s) | Path |
|---|---|---|
| `createOffer` | 10004 | **Data-channel handshake** (initial connection) |
| `setLocalDescription(offer)` | 10009 | handshake |
| `setRemoteDescription(offer)` | 10682 | handshake |
| `createAnswer` | 10717 | handshake |
| `setLocalDescription(answer)` | 10727 | handshake |
| `setRemoteDescription(answer)` | 11462 | handshake |
| `createOffer` | 13047, 13262 | **Call** (startCall / renegotiate) |
| `setLocalDescription` | 13048, 13263 | call |
| `setRemoteDescription(offer)` | 13081 | call |
| `createAnswer` + `setLocalDescription` | 1308713088 | call |
| `setRemoteDescription(answer)` | 13285 | call |
| `setLocalDescription({type:'rollback'})` | 13176 | call teardown |
**Where codec/RTP munging must hook:** only the **call** offers/answers (13047/13087/13262). The handshake SDP (m=application/data channel only) must stay untouched.
## 4. Media: getUserMedia / addTrack / transceivers
- **getUserMedia (calls):** `_acquireLocalMedia` 13012, `upgradeToVideo` 13221, `switchCamera` 13245.
- Unrelated: voice-message recorder `app.jsx:817`; QR scanner `QRScanner.js:72`.
- **addTrack:** `_acquireLocalMedia` 13017, `upgradeToVideo` 13230. **← the only way media is attached today.**
- **replaceTrack:** upgrade 13228, switchCamera 13252, teardown 13151.
- **removeTrack / getReceivers / getTransceivers / .stop():** teardown 1314613158.
- **`addTransceiver`: NONE.** `sendEncodings`: NONE. `setParameters`/`getParameters`: NONE. `setCodecPreferences`/`getCapabilities`: NONE.
- **`ontrack`** handler: 7802 (accumulates into `this.remoteMediaStream`).
## 5. Signalling model (two SDP paths — important)
1. **Connection handshake** (data channel): offer/answer compressed and exchanged **out-of-band** (QR / copy-paste), authenticated by **SAS**. Establishes the DTLS transport + `securechat` data channel (`createDataChannel` 9986).
2. **Call setup** (media): after the channel is verified, call SDP rides **in-band over that data channel** via `MESSAGE_TYPES.CALL_OFFER/CALL_ANSWER/CALL_ICE/CALL_DECLINE/CALL_END`. Routed in the live `dataChannel.onmessage` (~7996) → `_handleCallSignal` (13274). Media BUNDLEs onto the existing transport, so **no new ICE**.
→ Adaptive stack changes affect **path 2 only**.
## 6. Call subsystem inventory (all in the manager, added recently)
State/getters: `callState` object; `getCallState` 12957, `getRemoteMediaStream` 12961, `getLocalMediaStream` 12965, `_updateCallState` 12969 (fires `onCallStateChanged` + `securebit-call-state` DOM event).
Lifecycle: `_callCanStart` 12983 (gated on connected+verified), `_acquireLocalMedia` 12999, `startCall` 13025, `_onIncomingCallOffer` 13065, `_answerCallOffer` 13080, `acceptCall`/`declineCall`, `endCall` 13120, `_teardownCallMedia` 13132.
Controls: `setMicEnabled` 13186, `toggleMic` 13193, `setCameraEnabled` 13197, `toggleCamera` 13215, `upgradeToVideo` 13218, `switchCamera` 13241, `_renegotiateCall` 13258.
Signalling: `_handleCallSignal` 13274, `_sendCallSignal`, `MESSAGE_TYPES.CALL_*`.
Debug: `window.sbCallLog` / `window.__sbCallLog` (module top of the manager).
UI: `CallUI.jsx` + header buttons (`app.jsx`) + overlay mount in `EnhancedChatInterface`.
## 7. Gaps vs. the target spec
| Requirement | Status | Note |
|---|---|---|
| Opus fmtp (FEC/DTX/RED, maxavgbitrate, cbr=0) | ❌ | No SDP munging. |
| RED via setCodecPreferences | ❌ | No codec prefs. |
| Audio sender priority/networkPriority/maxBitrate | ❌ | No setParameters. |
| VP9 SVC (`L3T3_KEY`) + simulcast fallback | ❌ | No `sendEncodings`; uses `addTrack`. |
| AV1 / H.264 / VP8 fallback order | ❌ | No `getCapabilities` sort. |
| TWCC/NACK/PLI/FIR/REMB on m-lines | ⚠️ | Whatever the browser emits by default; not enforced/verified. |
| `NetworkAdaptationController` (getStats loop) | ❌ | None. |
| Config with sourced constants | ❌ | Only `_config.webrtc` (ICE/privacy). |
## 8. Proposed integration (for confirmation — not yet built)
New **plain-JS** modules under `src/network/webrtc/` (imported by the manager), mapping to the task's requested files:
| Task file | Proposed actual file |
|---|---|
| `src/webrtc/config.ts` | `src/network/webrtc/config.js` — all constants + source comments |
| `src/webrtc/codecs/audio.ts` | `src/network/webrtc/audio.js` — `configureAudioSender` |
| `src/webrtc/codecs/video.ts` | `src/network/webrtc/video.js` — `configureVideoSender` |
| `src/webrtc/codecs/sdp.ts` | `src/network/webrtc/sdp.js` — pure SDP munging utils (testable in Node) |
| `src/webrtc/adaptation/metrics.ts` | `src/network/webrtc/adaptation/metrics.js` — getStats parsing |
| `src/webrtc/adaptation/controller.ts` | `src/network/webrtc/adaptation/controller.js` — `NetworkAdaptationController` |
| `src/webrtc/call.ts` | **integrate into `EnhancedSecureWebRTCManager.js`** call methods |
Hook points in the manager:
- `_acquireLocalMedia` (12999): switch `addTrack` → `addTransceiver('audio'/'video', {direction, sendEncodings})`, then `setCodecPreferences` on the transceivers.
- `startCall`/`_answerCallOffer`/`_renegotiateCall`: after `createOffer/createAnswer`, run `sdp.js` munging before `setLocalDescription`; after `setRemoteDescription`, call `configureAudioSender`/`configureVideoSender` and start the controller.
- `_teardownCallMedia`: stop the controller.
**Key compromise to confirm:** the PC is long-lived and shared with the data channel, so we **cannot** follow Step-6's "addTransceiver at PC init" literally. Transceivers are added/negotiated in-band at call start on the existing PC (the audio/video transceivers can be created once on first call and reused via `replaceTrack` thereafter — this also fixes the transceiver-accumulation issue seen earlier).
## 9. Constraints / risks
- **Browser matrix:** `scalabilityMode`/`setCodecPreferences` support varies. Firefox: SVC limited → simulcast fallback mandatory. Safari: VP9 often absent → H.264 fallback; RED support varies. All feature-detected via `getCapabilities`/try-catch.
- **SDP munging is fragile** across browsers (m-line/payload ordering). Utilities must be defensive and idempotent (spec says "no duplicate lines").
- **Testing:** SDP utils + controller (mock `getStats`) → plain `node .mjs` unit tests (fits current infra). **Integration test needs a real browser** (two `RTCPeerConnection`s + throttling) — **Playwright is not installed**; that item needs either adding Playwright (a dependency — task says avoid unless necessary) or manual `chrome://webrtc-internals` verification.
- **In-band renegotiation** must keep the perfect-negotiation guards already in place to avoid glare.
## 10. Open questions before Step 2
1. Confirm JS (not TS) + `src/network/webrtc/` layout in §8.
2. Confirm the long-lived-PC compromise in §8 (reuse transceivers via `replaceTrack`, negotiate in-band).
3. Playwright: add it for the integration test, or rely on `chrome://webrtc-internals` manual verification for the "adaptation visible under throttling" criterion?
4. Commit-per-step + deploy to Fly after each step (as before), GitHub untouched — same as current workflow?
+160
View File
@@ -0,0 +1,160 @@
# WebRTC call configuration & rationale
All tunables live in `src/network/webrtc/config.js`. This doc explains the values
and where they come from. Built incrementally per step; **all steps shipped**
adaptive audio, SVC video, transport feedback, and runtime bitrate adaptation are
live in the 1:1 call flow.
## How the pieces attach (why not one `configureAudioSender`)
A single `RTCRtpSender` cannot carry codec ordering or fmtp, so the brief's
`configureAudioSender` is split across the three WebRTC surfaces at their correct
lifecycle points (see `EnhancedSecureWebRTCManager` call methods):
| Concern | API surface | When | Code |
|---|---|---|---|
| RED-first / Opus-second ordering | `transceiver.setCodecPreferences` | before `createOffer`/`createAnswer` | `applyAudioCodecPreferences` (audio.js) via `_applyAudioCodecPrefs` |
| Opus FEC/DTX/bitrate | SDP `a=fmtp` munging | after create, before `setLocalDescription` | `applyOpusSettings` (sdp.js) via `_mungeCallSdp` |
| priority / networkPriority / maxBitrate | `sender.setParameters` | after `setLocalDescription` | `configureAudioSender` (audio.js) via `_applyCallSenderParams` |
Both peers run the same munging, so the negotiated session carries the params.
## Step 2 — Audio (`AUDIO_CONFIG`)
### Opus fmtp (`opusFmtp`)
| Param | Value | Why |
|---|---|---|
| `minptime` | 10 | Smaller packetisation time → lower latency. Opus RFC 7587 §7. |
| `useinbandfec` | 1 | In-band FEC reconstructs a lost packet from the next one — the main lever for the 1520% loss target. RFC 6716 §2.1.7. |
| `usedtx` | 1 | Discontinuous transmission: stop sending in silence, freeing the shared transport for video/FEC. RFC 7587 §3.1.3. |
| `stereo` | 0 | Mono voice — halves bitrate, no quality loss for speech. |
| `maxaveragebitrate` | 32000 | Comfortable wideband speech; brief-specified. |
| `cbr` | 0 | Variable bitrate spends bits only when needed — better quality/bit than CBR for speech. |
### RED (`preferRed: true`)
RFC 2198 redundant audio: each packet also carries the previous frame's payload,
so isolated losses recover without retransmission. Enabled only when the browser
advertises `audio/red` in `RTCRtpSender.getCapabilities('audio')` (Chromium yes;
Safari/Firefox vary → silently skipped). Ordered **RED first, Opus second** via
`setCodecPreferences`.
### Sender (`sender`)
| Param | Value | Why |
|---|---|---|
| `maxBitrate` | 40000 bps | Brief value; head-room above 32 kbps Opus for RED redundancy. |
| `priority` | `high` | Bandwidth arbitration within the PC — audio wins over video. |
| `networkPriority` | `high` | DSCP hint so audio packets are prioritised on the wire. |
## Step 3 — Video (`VIDEO_CONFIG`)
### Codec preference (`codecPreferenceOrder`)
`VP9 → AV1 → H.264 → VP8`, applied with `setCodecPreferences` (via
`applyVideoCodecPreferences`). rtx/red/fec codecs are kept after the media codecs
so retransmission/FEC still work. VP9/AV1 give SVC; H.264/VP8 are plain.
### Single-encoding SVC (why not multi-rid simulcast)
This is **1:1 P2P** (one receiver), so a single encoding with **SVC** is the right
tool: one stream that degrades by spatial/temporal layer. It's applied through
`sender.setParameters` (`configureVideoSender`) and needs **no** `addTransceiver`/
rids, so it doesn't disturb the working `addTrack` media path.
| Codec | scalabilityMode | maxBitrate | degradationPreference |
|---|---|---|---|
| VP9 | `L3T3_KEY` (3 spatial × 3 temporal, key-aligned) | 1.5 Mbps | `balanced` |
| AV1 | `L1T3` | 1.2 Mbps | `maintain-framerate` |
| H.264 / VP8 | none (plain) | 1.5 Mbps | `balanced` |
`networkPriority: 'medium'` — below audio's `high`. If a browser rejects the SVC
mode (Firefox/Safari gaps), `configureVideoSender` retries with a plain encoding.
### Initialization (Step 6) + simulcast (Step 3) — what shipped, and why
**Attempted then reverted:** an explicit `addTransceiver({sendEncodings})` path
(single-encoding SVC / multi-rid simulcast) was implemented but **broke media on
real devices** during testing:
- on the **answerer**, reusing the transceiver created by `setRemoteDescription`
rejected the SVC `setParameters` (`"parameters are not valid"`), and
- on **role-reversed / repeat calls**, the reused transceiver **directions
desynced** — the call connected (timer ran, `ontrack` fired) but no audio/video
actually flowed.
**What ships instead:** media is attached with **`addTrack`** (reused across calls
via `replaceTrack`), letting the browser manage transceiver direction implicitly —
this is what keeps audio/video flowing across reversed and repeat calls. Video uses
**single-encoding SVC** (VP9 `L3T3_KEY` / AV1 `L1T3`) applied via
`configureVideoSender``setParameters`, plus `setCodecPreferences` (VP9→AV1→H264→VP8).
For 1:1 P2P a single SVC stream is the right tool anyway — it degrades by
spatial/temporal layer, which is what the "video degrades gracefully" requirement
needs. `configureVideoSender` falls back to a plain encoding if a scalabilityMode
is rejected.
**Multi-rid simulcast** (`buildVideoSendEncodings`, `VIDEO_CONFIG.*.simulcast`) is
kept as **tested, exported primitives** for a future SFU/group-call path, but is
**not wired into the 1:1 call flow** — it needs `addTransceiver({sendEncodings})`,
which requires the answerer-direction / setParameters issues above to be solved
first (ideally with a Playwright two-PC test rig, which isn't set up). The
adaptation controller is already simulcast-aware (`_applyToVideoSender` throttles
only the top layer) for when that lands.
## Step 4 — Transport (`TRANSPORT_CONFIG`)
Ensures the RTCP feedback / header extensions are present on the call m-lines
(most browsers already emit them, so this is an idempotent safety net):
| m-line | rtcp-fb | header ext |
|---|---|---|
| video | `transport-cc`, `nack`, `nack pli`, `ccm fir`, `goog-remb` | TWCC (`…transport-wide-cc…`) |
| audio | `transport-cc`, `nack` | TWCC |
Implemented as pure, idempotent SDP utils (`ensureRtcpFb`, `ensureExtmap`,
`applyTransport`) — added only when missing, never duplicated, applied only to
primary codecs (rtx/red/fec skipped). `transport-cc` (TWCC) is what feeds the
bandwidth estimator the Step-5 controller reads.
**Safety:** munged local SDP is set via `_setLocalMunged` with progressive
fallback — full munge → Opus-only → raw — so a browser rejecting an injected
line can never break the call.
## Step 5 — Adaptation + quality indicator (`ADAPTATION_CONFIG`)
`NetworkAdaptationController` reads `pc.getStats()` every 1 s and reacts:
| Condition | Action | Note |
|---|---|---|
| loss > 10% **or** RTT > 300 ms | video `maxBitrate` 20% (floor 100 kbps) | audio never touched |
| loss < 3% **and** RTT < 150 ms, 5 ticks | video `maxBitrate` +10% (up to ceiling) | gradual recovery |
| `qualityLimitationReason === 'cpu'` | `scaleResolutionDownBy` ×1.5 | bitrate unchanged |
All changes go through `sender.setParameters` — **no renegotiation, no track
restart**. **Audio is never throttled** by the controller, so speech stays intact
regardless of loss (stronger than the brief's "≥25%" guard). The pure decision
(`decideAdaptation`) and stats parsing (`summarizeStats`) are unit-tested with
mock getStats.
### Connection-quality indicator (user-facing)
The same `getStats` sample yields a coarse label (`qualityFromMetrics`) surfaced in
`callState.quality` and rendered in the call UI as signal bars + text:
| Label | Loss / RTT | Colour |
|---|---|---|
| Excellent | <3% and <150 ms | green |
| Good | <7% and <250 ms | green |
| Fair | <15% and <400 ms | yellow |
| Weak | otherwise | red |
Shown in the voice overlay, the video top bar, and (compact bars) the minimized
widget. Hidden until the first sample has data.
## Verification (all steps)
- Unit: `npm test` (SDP, video codecs, adaptation) — all green.
- Manual (`chrome://webrtc-internals`, per criteria): throttle the link (DevTools
Network / OS) → outbound video `targetBitrate` steps down within ~12 s and
recovers when the link clears; audio bitrate holds; the in-call indicator moves
Excellent → Fair → Weak.
- Audio: in a call, open `chrome://webrtc-internals` → audio outbound-rtp should
show Opus with the fmtp above; on Chromium the codec is `red`/`opus`. Under
packet loss, `useinbandfec` keeps audio intelligible.
> The temporary `[Call]` / `window.__sbCallLog` diagnostic logger used during
> bring-up has been removed for production; the manager's `_secureLog` handles
> runtime logging and stays silent in production builds (`DEBUG_MODE = false`).
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

+26
View File
@@ -0,0 +1,26 @@
# Fly.io configuration for SecureBit.chat (static PWA served by nginx).
# Set `app` to your chosen Fly app name and `primary_region` to the closest
# region (e.g. fra=Frankfurt, ams=Amsterdam, waw=Warsaw, iad=US East).
# Run `fly platform regions` to list them.
app = "securebit-chat"
primary_region = "fra"
[build]
dockerfile = "Dockerfile"
[http_service]
internal_port = 8080
force_https = true # matches the app's upgrade-insecure-requests CSP
auto_stop_machines = "stop"
auto_start_machines = true
min_machines_running = 1 # keep one machine warm so cold starts never 503 during testing
[http_service.concurrency]
type = "requests"
soft_limit = 200
hard_limit = 250
[[vm]]
size = "shared-cpu-1x"
memory = "256mb"
+107 -42
View File
@@ -2,15 +2,15 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content">
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
font-src 'self' https://fonts.gstatic.com data:;
connect-src 'self' https: wss: ws:;
font-src 'self' data:;
connect-src 'self' https: wss: ws: stun: stuns: turn: turns:;
img-src 'self' data: https:;
media-src 'none';
media-src 'self' blob:;
object-src 'none';
frame-src 'none';
worker-src 'self';
@@ -23,7 +23,7 @@
<!-- PWA Manifest -->
<link rel="manifest" href="./manifest.json">
<link rel="icon" type="image/x-icon" href="./logo/favicon.ico">
<link rel="icon" type="image/x-icon" href="./logo/favicon.ico?v=1784825757783">
<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
@@ -89,7 +89,7 @@
<link rel="apple-touch-startup-image" media="screen and (device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/8.3__iPad_Mini_portrait.png">
<!-- Apple Touch Icons -->
<link rel="apple-touch-icon" href="./logo/icon-180x180.png">
<link rel="apple-touch-icon" href="./logo/icon-180x180.png?v=1784825757783">
<link rel="apple-touch-icon" sizes="57x57" href="./logo/icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="./logo/icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="./logo/icon-72x72.png">
@@ -98,7 +98,7 @@
<link rel="apple-touch-icon" sizes="120x120" href="./logo/icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="./logo/icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="./logo/icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="./logo/icon-180x180.png">
<link rel="apple-touch-icon" sizes="180x180" href="./logo/icon-180x180.png?v=1784825757783">
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35">
@@ -112,53 +112,118 @@
<!-- Security Headers for PWA - CSP is already defined above -->
<!-- GitHub Pages SEO -->
<meta name="description" content="SecureBit.chat v4.4.18 — P2P messenger with ECDH + DTLS + SAS security and 18-layer military-grade cryptography">
<meta name="keywords" content="P2P messenger, ECDH, DTLS, SAS, encryption, WebRTC, privacy, ASN.1 validation, military-grade security, 18-layer defense, MITM protection, PFS">
<!-- SEO -->
<title>SecureBit.chat — Private, Encrypted Messenger</title>
<meta name="description" content="SecureBit.chat is a free, end-to-end encrypted peer-to-peer messenger that runs entirely in your browser. No accounts, no servers storing your messages — ECDH P-384, AES-256-GCM and in-person SAS verification.">
<meta name="keywords" content="encrypted messenger, end-to-end encryption, peer-to-peer chat, P2P messenger, ECDH, DTLS, SAS verification, WebRTC, private chat, secure chat, no servers, no accounts, MITM protection, PFS">
<meta name="author" content="Volodymyr">
<link rel="canonical" href="https://github.com/SecureBitChat/securebit-chat/">
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1">
<link rel="canonical" href="https://securebit.chat/">
<!-- Open Graph -->
<meta property="og:title" content="SecureBit.chat - Enhanced Security Edition">
<meta property="og:description" content="The most secure P2P messenger with military-grade cryptography">
<meta property="og:url" content="https://github.com/SecureBitChat/securebit-chat/">
<meta property="og:site_name" content="SecureBit.chat">
<meta property="og:title" content="SecureBit.chat — End-to-End Encrypted Peer-to-Peer Chat">
<meta property="og:description" content="A secure messenger that runs entirely in your browser. No accounts, no servers storing your messages. Bring your own STUN/TURN servers — we don't impose anything.">
<meta property="og:url" content="https://securebit.chat/">
<meta property="og:type" content="website">
<meta property="og:image" content="https://github.com/SecureBitChat/securebit-chat/favicon.ico">
<meta property="og:locale" content="en_US">
<meta property="og:image" content="https://securebit.chat/assets/social-card.png">
<meta property="og:image:secure_url" content="https://securebit.chat/assets/social-card.png">
<meta property="og:image:type" content="image/png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="SecureBit.chat — end-to-end encrypted, peer-to-peer chat">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="SecureBit.chat - Enhanced Security Edition">
<meta name="twitter:description" content="P2P messenger with military-grade cryptography">
<meta name="twitter:title" content="SecureBit.chat — End-to-End Encrypted Peer-to-Peer Chat">
<meta name="twitter:description" content="A secure messenger that runs entirely in your browser. No accounts, no servers storing your messages.">
<meta name="twitter:image" content="https://securebit.chat/assets/social-card.png">
<meta name="twitter:image:alt" content="SecureBit.chat — end-to-end encrypted, peer-to-peer chat">
<title>SecureBit.chat - Enhanced Security Edition</title>
<script src="libs/react/react.production.min.js"></script>
<script src="libs/react-dom/react-dom.production.min.js"></script>
<link rel="stylesheet" href="assets/tailwind.css">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico">
<link rel="stylesheet" href="/assets/fontawesome/css/all.min.css">
<!-- Structured data (schema.org) -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "WebSite",
"@id": "https://securebit.chat/#website",
"name": "SecureBit.chat",
"url": "https://securebit.chat/",
"description": "End-to-end encrypted, peer-to-peer chat that runs entirely in your browser.",
"inLanguage": "en"
},
{
"@type": "WebApplication",
"name": "SecureBit.chat",
"url": "https://securebit.chat/",
"applicationCategory": "CommunicationApplication",
"operatingSystem": "Any (modern web browser)",
"browserRequirements": "Requires WebRTC and the Web Crypto API",
"description": "A free, end-to-end encrypted peer-to-peer messenger. No accounts, no servers storing messages — ECDH P-384, AES-256-GCM and in-person SAS verification.",
"image": "https://securebit.chat/assets/social-card.png",
"license": "https://opensource.org/licenses/MIT",
"isAccessibleForFree": true,
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" },
"featureList": [
"End-to-end encryption (ECDH P-384, AES-256-GCM)",
"Peer-to-peer WebRTC connection with no message servers",
"In-person SAS verification against man-in-the-middle attacks",
"Custom STUN/TURN servers and relay-only privacy mode",
"Encrypted file transfer",
"Installable Progressive Web App with offline support"
],
"sameAs": [ "https://github.com/SecureBitChat/securebit-chat" ]
}
]
}
</script>
<!-- Render-blocking JS is deferred: classic deferred scripts and module scripts
both execute in document order after parsing, so React still runs before the
app modules below, but the parser / first paint is no longer blocked. -->
<script defer src="config/ice-servers.js?v=1784825757783"></script>
<script defer src="libs/react/react.production.min.js"></script>
<script defer src="libs/react-dom/react-dom.production.min.js"></script>
<!-- Prism syntax highlighting (vendored, offline). Tokenizes code as TEXT only —
it never executes the snippet. Loaded in manual mode (no auto-highlight).
Its CSS is loaded async via load-async-css.js (not paint-critical). -->
<script defer src="libs/prism/prism.js"></script>
<!-- Critical, paint-defining CSS stays render-blocking (avoids FOUC / layout shift). -->
<link rel="stylesheet" href="assets/tailwind.css?v=1784825757783">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1784825757783">
<!-- Preload only the fonts needed for first paint. fa-solid covers the bulk of UI
icons; fa-regular/fa-brands are loaded on demand by their CSS (rarely on the
first screen). Inter latin 400/700 cover body text and headings/buttons. -->
<link rel="preload" href="/assets/fontawesome/webfonts/fa-solid-900.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fontawesome/webfonts/fa-regular-400.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fontawesome/webfonts/fa-brands-400.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin-400.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin-700.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/assets/fonts/inter/inter.css">
<link rel="stylesheet" href="src/styles/main.css">
<link rel="stylesheet" href="src/styles/animations.css">
<link rel="stylesheet" href="src/styles/components.css">
<script src="src/scripts/fa-check.js"></script>
<link rel="stylesheet" href="src/styles/main.css?v=1784825757783">
<link rel="stylesheet" href="src/styles/animations.css?v=1784825757783">
<link rel="stylesheet" href="src/styles/components.css?v=1784825757783">
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="src/scripts/load-async-css.js?v=1784825757783"></script>
<noscript>
<link rel="stylesheet" href="/assets/fontawesome/css/all.min.css">
<link rel="stylesheet" href="libs/prism/prism.css">
</noscript>
<script defer src="src/scripts/fa-check.js?v=1784825757783"></script>
<!-- Update Manager - система принудительного обновления -->
<script src="src/utils/updateManager.js"></script>
<script type="module" src="src/components/UpdateChecker.jsx"></script>
<script type="module" src="dist/qr-local.js?v=1779043608721"></script>
<script type="module" src="src/components/QRScanner.js?v=1779043608721"></script>
<script defer src="src/utils/updateManager.js?v=1784825757783"></script>
<script type="module" src="src/components/UpdateChecker.jsx?v=1784825757783"></script>
<script type="module" src="dist/qr-local.js?v=1784825757783"></script>
<script type="module" src="src/components/QRScanner.js?v=1784825757783"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="dist/app-boot.js?v=1779043608721"></script>
<script type="module" src="dist/app.js?v=1779043608721"></script>
<script type="module" src="dist/app-boot.js?v=1784825757783"></script>
<script type="module" src="dist/app.js?v=1784825757783"></script>
<script src="src/scripts/pwa-register.js"></script>
<script src="./src/pwa/install-prompt.js" type="module"></script>
<script src="./src/pwa/pwa-manager.js" type="module"></script>
<script src="./src/scripts/pwa-offline-test.js"></script>
<link rel="stylesheet" href="./src/styles/pwa.css">
<script defer src="src/scripts/pwa-register.js?v=1784825757783"></script>
<script src="./src/pwa/install-prompt.js?v=1784825757783" type="module"></script>
<script src="./src/pwa/pwa-manager.js?v=1784825757783" type="module"></script>
<script defer src="./src/scripts/pwa-offline-test.js?v=1784825757783"></script>
<link rel="stylesheet" href="./src/styles/pwa.css?v=1784825757783">
</body>
</html>
+1
View File
@@ -0,0 +1 @@
code[class*=language-],pre[class*=language-]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

+35
View File
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg version="1.1" width="700" height="760" viewBox="276 240 700 760"
xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Brushed-metal silver for the upper "S" hook -->
<linearGradient id="sbSilver" x1="0" y1="0" x2="0.35" y2="1">
<stop offset="0" stop-color="#fdfdff"/>
<stop offset="0.20" stop-color="#e7e7ec"/>
<stop offset="0.46" stop-color="#c4c4cb"/>
<stop offset="0.72" stop-color="#a4a4ac"/>
<stop offset="1" stop-color="#86868d"/>
</linearGradient>
<!-- Glossy orange for the body, tail and dots -->
<linearGradient id="sbOrange" x1="0" y1="0" x2="0.25" y2="1">
<stop offset="0" stop-color="#ffb84d"/>
<stop offset="0.27" stop-color="#ff9a33"/>
<stop offset="0.58" stop-color="#fb7d16"/>
<stop offset="1" stop-color="#db5d04"/>
</linearGradient>
</defs>
<!-- Upper silver hook -->
<path fill="url(#sbSilver)" fill-rule="nonzero"
d="m 835.26446,352.56633 102.39051,-103.90366 -418.64101,1.00877 c 0,0 -171.69323,1.22309 -222.43455,167.96079 -52.34251,171.99925 77.67556,253.20215 77.67556,253.20215 0,0 35.54922,23.82856 79.77792,31.68982 15.73869,2.39372 79.16695,1.09532 79.16695,1.09532 54.47377,-10.08773 41.40629,-81.22528 -10.65516,-77.67557 C 492.06451,630.5166 372.5156,615.45079 386.86464,469.07968 415.02639,353.31661 520.52712,353.57511 520.52712,353.57511 Z"/>
<!-- Orange body + chat tail -->
<path fill="url(#sbOrange)" fill-rule="nonzero"
d="m 289.24744,881.29522 95.22696,-95.94027 369.13823,1.06997 C 873.15229,774.31964 863.51011,647.63259 863.51011,647.63259 846.7608,546.1216 749.51871,545.49427 749.51871,545.49427 l -232.01791,0.25219 c -37.80546,-8.91638 -37.85435,-49.4299 -37.85435,-49.4299 0,0 -1.56131,-38.07813 40.52401,-46.63785 l 260.62023,-0.7745 c 170.83788,24.60922 185.61432,187.63187 185.61432,187.63187 0,0 18.85523,117.07655 -90.63794,200.89054 l -0.62454,154.4184 -144.79052,-110.68137 z"/>
<!-- Two chat dots -->
<path fill="url(#sbOrange)"
d="m 658.38568,658.74237 a 27.462458,27.462458 0 0 1 -27.43073,27.46244 27.462458,27.462458 0 0 1 -27.49412,-27.39898 27.462458,27.462458 0 0 1 27.36719,-27.52575 27.462458,27.462458 0 0 1 27.55736,27.33536 z"/>
<path fill="url(#sbOrange)"
d="m 748.42871,659.07971 a 27.462458,27.462458 0 0 1 -27.43073,27.46244 27.462458,27.462458 0 0 1 -27.49412,-27.39898 27.462458,27.462458 0 0 1 27.36719,-27.52575 27.462458,27.462458 0 0 1 27.55736,27.33537 z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 410 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Some files were not shown because too many files have changed in this diff Show More