Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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,258 @@
|
||||
# Changelog
|
||||
|
||||
## v4.8.5 — Security hardening release
|
||||
## v5.5.0 — Encrypted voice & video calls
|
||||
|
||||
SecureBit now supports **end-to-end encrypted voice and video calls** — the "5.5 Secure Voice & Calls" roadmap milestone.
|
||||
|
||||
### Added
|
||||
|
||||
- **Voice & video calls.** Start a call from the chat header (phone / camera buttons). Audio and video tracks ride the **same RTCPeerConnection as the chat**, bundled onto one **DTLS-SRTP** transport — so media is end-to-end encrypted with the very connection that in-person **SAS verification** authenticated. Call setup (SDP offer/answer) is renegotiated **in-band over the verified data channel**, never through a signalling server, so the media's DTLS fingerprints are authenticated end-to-end too. There is no server that can see or MITM a call.
|
||||
- **Verification-gated.** Calls are only permitted once a session is **connected and SAS-verified**; the manager rejects call signalling otherwise. The header call buttons stay disabled until then.
|
||||
- **In-call controls.** Mute / unmute, camera on/off (turning the camera on during a voice call upgrades it to video in-band), front/back camera flip, minimize-to-widget (chat stays usable) and hang up. Incoming calls show an accept / decline prompt.
|
||||
- **Adaptive audio codec.** Opus tuned for real-world links — in-band FEC, DTX and RED redundancy (RED preferred first where the browser advertises it) keep speech intelligible under 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
|
||||
doc/ technical documentation
|
||||
src/network/ WebRTC connection and session lifecycle
|
||||
src/transfer/ secure file-transfer implementation
|
||||
src/crypto/ cryptographic utilities
|
||||
src/components/ React UI components
|
||||
src/styles/ component styles
|
||||
doc/ technical documentation
|
||||
dist/ built bundles served in production
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [`SECURITY.md`](SECURITY.md)
|
||||
- [`doc/CONFIGURATION.md`](doc/CONFIGURATION.md)
|
||||
- [`doc/CRYPTOGRAPHY.md`](doc/CRYPTOGRAPHY.md)
|
||||
- [`doc/SECURITY-ARCHITECTURE.md`](doc/SECURITY-ARCHITECTURE.md)
|
||||
- [`doc/API.md`](doc/API.md)
|
||||
- [`CHANGELOG.md`](CHANGELOG.md)
|
||||
- [`SECURITY.md`](SECURITY.md) — security policy & reporting
|
||||
- [`doc/CONFIGURATION.md`](doc/CONFIGURATION.md) — deployment & ICE configuration
|
||||
- [`doc/CRYPTOGRAPHY.md`](doc/CRYPTOGRAPHY.md) — cryptographic design
|
||||
- [`doc/SECURITY-ARCHITECTURE.md`](doc/SECURITY-ARCHITECTURE.md) — architecture overview
|
||||
- [`doc/API.md`](doc/API.md) — internal APIs
|
||||
- [`CHANGELOG.md`](CHANGELOG.md) — full release history
|
||||
|
||||
## Responsible use
|
||||
## Contributing & responsible use
|
||||
|
||||
SecureBit.chat is intended for lawful, ethical use. See [`RESPONSIBLE_USE.md`](RESPONSIBLE_USE.md) and [`SECURITY_DISCLAIMER.md`](SECURITY_DISCLAIMER.md).
|
||||
Issues and pull requests are welcome. SecureBit.chat is intended for lawful, ethical communication only — please read [`RESPONSIBLE_USE.md`](RESPONSIBLE_USE.md) before using or contributing.
|
||||
|
||||
## License
|
||||
|
||||
MIT License. See [`LICENSE`](LICENSE).
|
||||
Released under the [MIT License](LICENSE).
|
||||
|
||||
<div align="center">
|
||||
<sub>Built with WebRTC and the Web Crypto API · No servers, no accounts, no compromises.</sub>
|
||||
</div>
|
||||
|
||||
@@ -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() {
|
||||
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
||||
try {
|
||||
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
||||
} catch (e) {
|
||||
throw mod = 0, e;
|
||||
}
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
|
||||
@@ -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,15 +2,15 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content">
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self';
|
||||
script-src 'self';
|
||||
style-src 'self' 'unsafe-inline';
|
||||
font-src 'self' https://fonts.gstatic.com data:;
|
||||
connect-src 'self' https: wss: ws:;
|
||||
font-src 'self' data:;
|
||||
connect-src 'self' https: wss: ws: stun: stuns: turn: turns:;
|
||||
img-src 'self' data: https:;
|
||||
media-src 'none';
|
||||
media-src 'self' blob:;
|
||||
object-src 'none';
|
||||
frame-src 'none';
|
||||
worker-src 'self';
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
<!-- PWA Manifest -->
|
||||
<link rel="manifest" href="./manifest.json">
|
||||
<link rel="icon" type="image/x-icon" href="./logo/favicon.ico">
|
||||
<link rel="icon" type="image/x-icon" href="./logo/favicon.ico?v=1784825757783">
|
||||
|
||||
<!-- PWA Meta Tags -->
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
@@ -89,7 +89,7 @@
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/8.3__iPad_Mini_portrait.png">
|
||||
|
||||
<!-- Apple Touch Icons -->
|
||||
<link rel="apple-touch-icon" href="./logo/icon-180x180.png">
|
||||
<link rel="apple-touch-icon" href="./logo/icon-180x180.png?v=1784825757783">
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="./logo/icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="./logo/icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="./logo/icon-72x72.png">
|
||||
@@ -98,7 +98,7 @@
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="./logo/icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="./logo/icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="./logo/icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="./logo/icon-180x180.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="./logo/icon-180x180.png?v=1784825757783">
|
||||
|
||||
<!-- Microsoft Tiles -->
|
||||
<meta name="msapplication-TileColor" content="#ff6b35">
|
||||
@@ -112,53 +112,118 @@
|
||||
<!-- Security Headers for PWA - CSP is already defined above -->
|
||||
|
||||
|
||||
<!-- GitHub Pages SEO -->
|
||||
<meta name="description" content="SecureBit.chat v4.4.18 — P2P messenger with ECDH + DTLS + SAS security and 18-layer military-grade cryptography">
|
||||
<meta name="keywords" content="P2P messenger, ECDH, DTLS, SAS, encryption, WebRTC, privacy, ASN.1 validation, military-grade security, 18-layer defense, MITM protection, PFS">
|
||||
<!-- SEO -->
|
||||
<title>SecureBit.chat — Private, Encrypted Messenger</title>
|
||||
<meta name="description" content="SecureBit.chat is a free, end-to-end encrypted peer-to-peer messenger that runs entirely in your browser. No accounts, no servers storing your messages — ECDH P-384, AES-256-GCM and in-person SAS verification.">
|
||||
<meta name="keywords" content="encrypted messenger, end-to-end encryption, peer-to-peer chat, P2P messenger, ECDH, DTLS, SAS verification, WebRTC, private chat, secure chat, no servers, no accounts, MITM protection, PFS">
|
||||
<meta name="author" content="Volodymyr">
|
||||
<link rel="canonical" href="https://github.com/SecureBitChat/securebit-chat/">
|
||||
|
||||
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1">
|
||||
<link rel="canonical" href="https://securebit.chat/">
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:title" content="SecureBit.chat - Enhanced Security Edition">
|
||||
<meta property="og:description" content="The most secure P2P messenger with military-grade cryptography">
|
||||
<meta property="og:url" content="https://github.com/SecureBitChat/securebit-chat/">
|
||||
<meta property="og:site_name" content="SecureBit.chat">
|
||||
<meta property="og:title" content="SecureBit.chat — End-to-End Encrypted Peer-to-Peer Chat">
|
||||
<meta property="og:description" content="A secure messenger that runs entirely in your browser. No accounts, no servers storing your messages. Bring your own STUN/TURN servers — we don't impose anything.">
|
||||
<meta property="og:url" content="https://securebit.chat/">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:image" content="https://github.com/SecureBitChat/securebit-chat/favicon.ico">
|
||||
|
||||
<meta property="og:locale" content="en_US">
|
||||
<meta property="og:image" content="https://securebit.chat/assets/social-card.png">
|
||||
<meta property="og:image:secure_url" content="https://securebit.chat/assets/social-card.png">
|
||||
<meta property="og:image:type" content="image/png">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
<meta property="og:image:alt" content="SecureBit.chat — end-to-end encrypted, peer-to-peer chat">
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="SecureBit.chat - Enhanced Security Edition">
|
||||
<meta name="twitter:description" content="P2P messenger with military-grade cryptography">
|
||||
<meta name="twitter:title" content="SecureBit.chat — End-to-End Encrypted Peer-to-Peer Chat">
|
||||
<meta name="twitter:description" content="A secure messenger that runs entirely in your browser. No accounts, no servers storing your messages.">
|
||||
<meta name="twitter:image" content="https://securebit.chat/assets/social-card.png">
|
||||
<meta name="twitter:image:alt" content="SecureBit.chat — end-to-end encrypted, peer-to-peer chat">
|
||||
|
||||
<title>SecureBit.chat - Enhanced Security Edition</title>
|
||||
<script src="libs/react/react.production.min.js"></script>
|
||||
<script src="libs/react-dom/react-dom.production.min.js"></script>
|
||||
<link rel="stylesheet" href="assets/tailwind.css">
|
||||
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico">
|
||||
<link rel="stylesheet" href="/assets/fontawesome/css/all.min.css">
|
||||
<!-- Structured data (schema.org) -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@graph": [
|
||||
{
|
||||
"@type": "WebSite",
|
||||
"@id": "https://securebit.chat/#website",
|
||||
"name": "SecureBit.chat",
|
||||
"url": "https://securebit.chat/",
|
||||
"description": "End-to-end encrypted, peer-to-peer chat that runs entirely in your browser.",
|
||||
"inLanguage": "en"
|
||||
},
|
||||
{
|
||||
"@type": "WebApplication",
|
||||
"name": "SecureBit.chat",
|
||||
"url": "https://securebit.chat/",
|
||||
"applicationCategory": "CommunicationApplication",
|
||||
"operatingSystem": "Any (modern web browser)",
|
||||
"browserRequirements": "Requires WebRTC and the Web Crypto API",
|
||||
"description": "A free, end-to-end encrypted peer-to-peer messenger. No accounts, no servers storing messages — ECDH P-384, AES-256-GCM and in-person SAS verification.",
|
||||
"image": "https://securebit.chat/assets/social-card.png",
|
||||
"license": "https://opensource.org/licenses/MIT",
|
||||
"isAccessibleForFree": true,
|
||||
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" },
|
||||
"featureList": [
|
||||
"End-to-end encryption (ECDH P-384, AES-256-GCM)",
|
||||
"Peer-to-peer WebRTC connection with no message servers",
|
||||
"In-person SAS verification against man-in-the-middle attacks",
|
||||
"Custom STUN/TURN servers and relay-only privacy mode",
|
||||
"Encrypted file transfer",
|
||||
"Installable Progressive Web App with offline support"
|
||||
],
|
||||
"sameAs": [ "https://github.com/SecureBitChat/securebit-chat" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
<!-- Render-blocking JS is deferred: classic deferred scripts and module scripts
|
||||
both execute in document order after parsing, so React still runs before the
|
||||
app modules below, but the parser / first paint is no longer blocked. -->
|
||||
<script defer src="config/ice-servers.js?v=1784825757783"></script>
|
||||
<script defer src="libs/react/react.production.min.js"></script>
|
||||
<script defer src="libs/react-dom/react-dom.production.min.js"></script>
|
||||
<!-- Prism syntax highlighting (vendored, offline). Tokenizes code as TEXT only —
|
||||
it never executes the snippet. Loaded in manual mode (no auto-highlight).
|
||||
Its CSS is loaded async via load-async-css.js (not paint-critical). -->
|
||||
<script defer src="libs/prism/prism.js"></script>
|
||||
<!-- Critical, paint-defining CSS stays render-blocking (avoids FOUC / layout shift). -->
|
||||
<link rel="stylesheet" href="assets/tailwind.css?v=1784825757783">
|
||||
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1784825757783">
|
||||
<!-- Preload only the fonts needed for first paint. fa-solid covers the bulk of UI
|
||||
icons; fa-regular/fa-brands are loaded on demand by their CSS (rarely on the
|
||||
first screen). Inter latin 400/700 cover body text and headings/buttons. -->
|
||||
<link rel="preload" href="/assets/fontawesome/webfonts/fa-solid-900.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="/assets/fontawesome/webfonts/fa-regular-400.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="/assets/fontawesome/webfonts/fa-brands-400.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="/assets/fonts/inter/files/inter-latin-400.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="/assets/fonts/inter/files/inter-latin-700.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="stylesheet" href="/assets/fonts/inter/inter.css">
|
||||
<link rel="stylesheet" href="src/styles/main.css">
|
||||
<link rel="stylesheet" href="src/styles/animations.css">
|
||||
<link rel="stylesheet" href="src/styles/components.css">
|
||||
<script src="src/scripts/fa-check.js"></script>
|
||||
<link rel="stylesheet" href="src/styles/main.css?v=1784825757783">
|
||||
<link rel="stylesheet" href="src/styles/animations.css?v=1784825757783">
|
||||
<link rel="stylesheet" href="src/styles/components.css?v=1784825757783">
|
||||
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
|
||||
<script defer src="src/scripts/load-async-css.js?v=1784825757783"></script>
|
||||
<noscript>
|
||||
<link rel="stylesheet" href="/assets/fontawesome/css/all.min.css">
|
||||
<link rel="stylesheet" href="libs/prism/prism.css">
|
||||
</noscript>
|
||||
<script defer src="src/scripts/fa-check.js?v=1784825757783"></script>
|
||||
<!-- Update Manager - система принудительного обновления -->
|
||||
<script src="src/utils/updateManager.js"></script>
|
||||
<script type="module" src="src/components/UpdateChecker.jsx"></script>
|
||||
<script type="module" src="dist/qr-local.js?v=1779043608721"></script>
|
||||
<script type="module" src="src/components/QRScanner.js?v=1779043608721"></script>
|
||||
<script defer src="src/utils/updateManager.js?v=1784825757783"></script>
|
||||
<script type="module" src="src/components/UpdateChecker.jsx?v=1784825757783"></script>
|
||||
<script type="module" src="dist/qr-local.js?v=1784825757783"></script>
|
||||
<script type="module" src="src/components/QRScanner.js?v=1784825757783"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="dist/app-boot.js?v=1779043608721"></script>
|
||||
<script type="module" src="dist/app.js?v=1779043608721"></script>
|
||||
<script type="module" src="dist/app-boot.js?v=1784825757783"></script>
|
||||
<script type="module" src="dist/app.js?v=1784825757783"></script>
|
||||
|
||||
<script src="src/scripts/pwa-register.js"></script>
|
||||
<script src="./src/pwa/install-prompt.js" type="module"></script>
|
||||
<script src="./src/pwa/pwa-manager.js" type="module"></script>
|
||||
<script src="./src/scripts/pwa-offline-test.js"></script>
|
||||
<link rel="stylesheet" href="./src/styles/pwa.css">
|
||||
<script defer src="src/scripts/pwa-register.js?v=1784825757783"></script>
|
||||
<script src="./src/pwa/install-prompt.js?v=1784825757783" type="module"></script>
|
||||
<script src="./src/pwa/pwa-manager.js?v=1784825757783" type="module"></script>
|
||||
<script defer src="./src/scripts/pwa-offline-test.js?v=1784825757783"></script>
|
||||
<link rel="stylesheet" href="./src/styles/pwa.css?v=1784825757783">
|
||||
</body>
|
||||
</html>
|
||||
@@ -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 |