Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60bf037ef9 | ||
|
|
fcee4216e8 | ||
|
|
6152a77b51 | ||
|
|
b3fcf54670 | ||
|
|
0de8ab2d54 | ||
|
|
c98a01b1d5 | ||
|
|
0e3e3a2974 | ||
|
|
db5d6e481d | ||
|
|
ef2f13d053 | ||
|
|
62567fb5cc | ||
|
|
ae6900d0c7 | ||
|
|
5eaf57c8da | ||
|
|
ab49d7485d | ||
|
|
96fffab765 | ||
|
|
cbf5c5f834 | ||
|
|
b39f9ecd2c | ||
|
|
cb72b9cb1c | ||
|
|
15173a9278 | ||
|
|
cf36656341 | ||
|
|
42be55aaeb | ||
|
|
6f36fce8c6 | ||
|
|
be1d02f1f7 | ||
|
|
9244250835 | ||
|
|
6dac4ce52a | ||
|
|
017a590220 | ||
|
|
d58967c671 | ||
|
|
39aad6ae8b | ||
|
|
7f2ecce57f | ||
|
|
366f080128 | ||
|
|
d11f250257 | ||
|
|
2468cb495e | ||
|
|
1cc873223a | ||
|
|
01cb25f988 | ||
|
|
ad3bee5f2e | ||
|
|
f2a4276b31 | ||
|
|
86a96b0121 | ||
|
|
33f3764ec5 | ||
|
|
a04a70eb97 | ||
|
|
0fbcc240be | ||
|
|
18022c6b68 |
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,6 +1,303 @@
|
||||
# Changelog
|
||||
|
||||
## v4.8.5 — Security hardening release
|
||||
## v5.5.4 — Fix the desktop download buttons
|
||||
|
||||
### Fixed
|
||||
|
||||
- **The download buttons still led to a dead GitHub page.** 5.5.3 updated one of the two places these links live; the platforms menu on the connection screen has its own `DOWNLOADS` table, and it was missed. It still pointed at 0.1.0.
|
||||
- The stale links used `/releases/latest/download/<file>`. GitHub resolves `latest` by redirecting to the newest tag, so once 0.3.0 shipped, a link written for 0.1.0 became `/releases/download/v0.3.0/SecureBit.Chat_0.1.0_x64-setup.exe` — a file that never existed under that tag. The button navigated to GitHub and downloaded nothing, which is why it looked like a working link that simply did nothing.
|
||||
|
||||
### Added
|
||||
|
||||
- `tests/desktop-download-links.test.mjs`, which fails the build if any source drifts: it requires every file to derive its URLs from one `DESKTOP_VERSION` constant, forbids `/releases/latest/download/` and hardcoded versions in filenames, and fetches each generated URL to prove the release asset is really there. Set `SKIP_NETWORK=1` to skip the fetches offline.
|
||||
|
||||
## v5.5.3 — Desktop downloads point at 0.3.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- **The desktop download buttons still offered 0.1.0.** Windows, macOS and Linux now link to the current 0.3.0 release, which is the first with in-app updates, voice-note parity and the verification fixes.
|
||||
- The release version was repeated across three URLs in two different forms (two resolving through `/releases/latest/`, one pinned to a tag), which is how they drifted out of date. It is now a single constant, and the tag is pinned deliberately: filenames carry the version, so a `latest` link breaks as soon as a newer release exists, whereas a pinned tag keeps serving a working installer.
|
||||
|
||||
## v5.5.2 — Fix security level in the header
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Header showed "Secure undefined%".** Moving `getRealSecurityLevel()` onto the connection manager in v5.5.1 made it reachable for the first time, so the header started calling it instead of falling through to `calculateAndReportSecurityLevel()`. It returned only per-feature booleans — no `level`, no `score` — and the header renders those two fields directly. It now runs the same verified scoring as every other consumer and merges the feature flags on top, so all callers see one consistent number. A regression test pins the shape for every branch of the header's fallback chain.
|
||||
|
||||
## v5.5.1 — Security audit fixes
|
||||
|
||||
A security review of the transport and verification layers. Every item below is a
|
||||
fix to how untrusted peer input is handled; no features changed.
|
||||
|
||||
### Security
|
||||
|
||||
- **SAS verification can no longer be bypassed.** `verification_both_confirmed` is an unauthenticated frame that arrives on a channel that is not yet trusted, but it was accepted as proof that both sides had compared their codes — so a peer who completed the signalling exchange could send it right after the data channel opened and drive the other side to a "verified" session while the user never looked at the code. It is now only an *acknowledgement*: it is refused unless this side has already confirmed locally, and `_setVerifiedStatus()` independently rejects any SAS-based transition without a local confirmation. Holding ECDH-derived keys was never sufficient proof of identity — a MITM has those too.
|
||||
- **Unauthenticated frames can no longer be injected into the chat.** A bare `{type:"message"}` JSON frame, a raw non-JSON text frame, and a binary frame were each decoded and rendered in the chat, bypassing decryption, the HMAC check and the verification gate — the injected text was visually indistinguishable from a genuine message. Chat content now reaches the UI only through the authenticated `enhanced_message` path; everything else is dropped and logged.
|
||||
- **A peer can no longer supply the verification code.** `sas_code` announcements were adopted verbatim when no local SAS had been derived yet, which would have shown the user a number chosen by the other end. The announcement may now only corroborate the locally derived code; a missing or mismatching code aborts the session.
|
||||
- **File transfers are gated on verification in both directions.** File control frames (`file_transfer_start`, `file_chunk`, …) are written straight to the data channel by the transfer system, so they never passed through the send path's verification gate on receipt. Sending was already gated; receiving now is too, so an unverified peer cannot open transfers, push chunks or drive transfer state before the SAS has been compared. User consent remains required on top of this.
|
||||
- **Anti-replay is actually enforced.** The sequence-number and AAD validators were defined on the wrong class (`SecureKeyStorage` instead of the connection manager), so every call site silently failed and the sliding replay window never ran. They now live on the manager, the live chat path validates the authenticated sequence number of each message, and a missing or non-numeric sequence number fails closed instead of sailing through the range checks.
|
||||
- **Stale sequence numbers are rejected** rather than logged and decrypted anyway.
|
||||
- **Tighter CSP.** `connect-src` and `img-src` no longer allow arbitrary `https:` hosts (nothing in the app talks to a third party), and `base-uri 'none'` is set. This removes the exfiltration channel an injection would otherwise have.
|
||||
- The SAS is no longer written to logs, and the peer-announced code is compared in constant time on every path.
|
||||
- Fixed `SecureMasterKeyManager.isUnlocked()` testing a field renamed long ago, so it never actually gated anything.
|
||||
|
||||
### Added
|
||||
|
||||
- Regression tests covering the verification gate and inbound frame authentication (`tests/verification-gate.test.mjs`, `tests/inbound-frame-authentication.test.mjs`).
|
||||
|
||||
## 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 15–20% 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 1s–1h).
|
||||
- **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.
|
||||
|
||||
|
||||
@@ -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;"]
|
||||
@@ -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)
|
||||
[](CHANGELOG.md)
|
||||
[](#install-as-an-app)
|
||||
[](#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 |
|
||||
| :---: | :---: |
|
||||
|  |  |
|
||||
|
||||
## 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 15–20% 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
|
||||
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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 200 KiB |
|
After Width: | Height: | Size: 760 KiB |
|
After Width: | Height: | Size: 328 KiB |
@@ -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'
|
||||
}
|
||||
];
|
||||
@@ -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='
|
||||
}
|
||||
];
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,11 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __commonJS = (cb, mod) => function __require() {
|
||||
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") {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 816–817. |
|
||||
| `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` | 13087–13088 | 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 13146–13158.
|
||||
- **`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?
|
||||
@@ -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 15–20% 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 ~1–2 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`).
|
||||
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 4.9 KiB |
@@ -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"
|
||||
@@ -2,20 +2,21 @@
|
||||
<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:;
|
||||
img-src 'self' data: https:;
|
||||
media-src 'none';
|
||||
font-src 'self' data:;
|
||||
connect-src 'self' stun: stuns: turn: turns:;
|
||||
img-src 'self' data:;
|
||||
media-src 'self' blob:;
|
||||
object-src 'none';
|
||||
frame-src 'none';
|
||||
worker-src 'self';
|
||||
manifest-src 'self';
|
||||
form-action 'self';
|
||||
base-uri 'none';
|
||||
upgrade-insecure-requests;">
|
||||
<meta http-equiv="X-Content-Type-Options" content="nosniff">
|
||||
<meta http-equiv="X-XSS-Protection" content="1; mode=block">
|
||||
@@ -23,7 +24,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=1785142410213">
|
||||
|
||||
<!-- PWA Meta Tags -->
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
@@ -89,7 +90,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=1785142410213">
|
||||
<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 +99,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=1785142410213">
|
||||
|
||||
<!-- Microsoft Tiles -->
|
||||
<meta name="msapplication-TileColor" content="#ff6b35">
|
||||
@@ -112,53 +113,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=1785142410213"></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=1785142410213">
|
||||
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1785142410213">
|
||||
<!-- 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=1785142410213">
|
||||
<link rel="stylesheet" href="src/styles/animations.css?v=1785142410213">
|
||||
<link rel="stylesheet" href="src/styles/components.css?v=1785142410213">
|
||||
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
|
||||
<script defer src="src/scripts/load-async-css.js?v=1785142410213"></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=1785142410213"></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=1785142410213"></script>
|
||||
<script type="module" src="src/components/UpdateChecker.jsx?v=1785142410213"></script>
|
||||
<script type="module" src="dist/qr-local.js?v=1785142410213"></script>
|
||||
<script type="module" src="src/components/QRScanner.js?v=1785142410213"></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=1785142410213"></script>
|
||||
<script type="module" src="dist/app.js?v=1785142410213"></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=1785142410213"></script>
|
||||
<script src="./src/pwa/install-prompt.js?v=1785142410213" type="module"></script>
|
||||
<script src="./src/pwa/pwa-manager.js?v=1785142410213" type="module"></script>
|
||||
<script defer src="./src/scripts/pwa-offline-test.js?v=1785142410213"></script>
|
||||
<link rel="stylesheet" href="./src/styles/pwa.css?v=1785142410213">
|
||||
</body>
|
||||
</html>
|
||||
@@ -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}
|
||||
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 38 KiB |
@@ -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 |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 410 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 482 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 178 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 178 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 257 KiB |
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 257 KiB |
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 248 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 248 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 69 KiB |