docs: reorganise documentation; derive header version from package.json; release v5.7.2
No protocol or message-protection changes. The version in the application header was a literal and had fallen behind, showing v5.6.0 while running 5.7.1. It now comes from package.json, and a test fails if a hard-coded one reappears or if meta.json, the README badge, the changelog and the docs disagree about the release. Documentation reorganised so that everything technical lives in doc/ with an index, and the root keeps only what belongs there by convention: README, SECURITY, CHANGELOG and LICENSE. - SECURITY.md rewritten. It listed a supported release line three major versions out of date and made claims the software does not make. It now states what is guaranteed, what is not, and how to report a problem. - SECURITY_DISCLAIMER.md and RESPONSIBLE_USE.md merged into doc/USE-POLICY.md, which says what the software cannot protect against rather than listing generic advice. - doc/SECURITY-ARCHITECTURE.md renamed to doc/ARCHITECTURE.md and rewritten around the session lifecycle, what verification gates, and how recovery works. - doc/CRYPTOGRAPHY.md rewritten: key schedule, the Double Ratchet, framing, and memory handling, with values taken from the source rather than restated. - doc/CONFIGURATION.md rewritten with the real file-type policy, ICE and TURN guidance, and the deployment caching rules that matter. - docs/webrtc-config.md moved to doc/CALLS.md and rewritten; the obsolete docs/webrtc-audit.md, a working document full of stale line numbers, removed along with the docs/ directory. - doc/CONTRIBUTING.md records what the recent regressions taught us about writing tests that can actually fail. - doc/README.md added as an index. Internal security review notes are excluded from the repository via .gitignore. Those describe attack paths against specific releases in enough detail to reproduce them, which is useful privately and harmful in public while users are still updating.
This commit is contained in:
+33
-1
@@ -1,5 +1,37 @@
|
||||
# Changelog
|
||||
|
||||
## v5.7.2 — Documentation, and a version that keeps itself honest
|
||||
|
||||
No changes to the protocol or to how messages are protected.
|
||||
|
||||
### Fixed
|
||||
|
||||
- The version shown in the application header was written as a literal and had
|
||||
fallen behind, displaying v5.6.0 while running 5.7.1. It now comes from
|
||||
`package.json`, so it cannot drift again, and a test fails the build if anyone
|
||||
reintroduces a hard-coded one. The same test checks that `meta.json`, the README
|
||||
badge and the changelog agree with each other before a release goes out.
|
||||
|
||||
### Changed
|
||||
|
||||
- Documentation reorganised. Everything technical now lives in `doc/`, with an
|
||||
index at `doc/README.md`. The root keeps only what belongs there by convention:
|
||||
`README.md`, `SECURITY.md`, `CHANGELOG.md` and `LICENSE`.
|
||||
- `SECURITY.md` rewritten. It described a release line three major versions old
|
||||
and made claims the software does not make. It now states what is guaranteed,
|
||||
what is not, and how to report a problem.
|
||||
- `SECURITY_DISCLAIMER.md` and `RESPONSIBLE_USE.md` merged into
|
||||
`doc/USE-POLICY.md`, which says plainly what the software cannot protect
|
||||
against instead of listing generic advice.
|
||||
- `doc/CRYPTOGRAPHY.md` and `doc/ARCHITECTURE.md` rewritten to describe the
|
||||
current design, including the Double Ratchet, and to quote real values taken
|
||||
from the source rather than restated approximations.
|
||||
- WebRTC call tuning notes moved to `doc/CALLS.md` and rewritten. The obsolete
|
||||
`docs/` directory, which held a working document full of stale line numbers,
|
||||
has been removed.
|
||||
- `doc/CONTRIBUTING.md` now records what recent bugs taught us about writing
|
||||
tests that can actually fail.
|
||||
|
||||
## v5.7.1 — Forward secrecy now engages for both sides of a chat
|
||||
|
||||
The Double Ratchet introduced in 5.7.0 was only taking effect for the peer who
|
||||
@@ -284,7 +316,7 @@ SecureBit now supports **end-to-end encrypted voice and video calls** — the "5
|
||||
- **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).
|
||||
- **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 [`doc/CALLS.md`](doc/CALLS.md).
|
||||
|
||||
### Security
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
No accounts. No servers storing your messages. No installation required.
|
||||
|
||||
[](LICENSE)
|
||||
[](CHANGELOG.md)
|
||||
[](CHANGELOG.md)
|
||||
[](#install-as-an-app)
|
||||
[](#security-model)
|
||||
[](#forward-secrecy)
|
||||
@@ -20,7 +20,7 @@ No accounts. No servers storing your messages. No installation required.
|
||||
|
||||
---
|
||||
|
||||
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.
|
||||
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 account whose metadata can 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.
|
||||
|
||||
@@ -32,41 +32,41 @@ It is designed for people who need a small, auditable, zero-infrastructure way t
|
||||
|
||||
## Features
|
||||
|
||||
** Encryption & verification**
|
||||
**Encryption and verification**
|
||||
- ECDH P-384 key agreement with derived per-session keys, AES-256-GCM payloads, and DTLS-protected transport.
|
||||
- **Double Ratchet forward secrecy** — every message is encrypted with its own key, and the session re-keys itself each time the conversation changes direction. See [Forward secrecy](#forward-secrecy).
|
||||
- Interactive **Short Authentication String (SAS)** verification — you confirm a code out-of-band before the session is trusted, defeating man-in-the-middle attacks.
|
||||
- **Double Ratchet forward secrecy.** Every message is encrypted with its own key, and the session re-keys itself each time the conversation changes direction. See [Forward secrecy](#forward-secrecy).
|
||||
- Interactive **safety code** verification. You confirm a short code out of band before the session is trusted, which is what defeats a man in the middle.
|
||||
- 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.
|
||||
**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.
|
||||
**Encrypted calls**
|
||||
- **One-to-one voice and video calls** over the same verified connection. Media rides the 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 at 15 to 20 percent 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.
|
||||
- **Live connection-quality indicator** (Excellent, Good, Fair, Weak) shown during a call, plus in-call mute and video 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.
|
||||
**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.
|
||||
- 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**
|
||||
**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**
|
||||
**Progressive Web App**
|
||||
- Installable on desktop and mobile, works offline, and ships update notifications.
|
||||
|
||||
## How it works
|
||||
@@ -74,51 +74,56 @@ It is designed for people who need a small, auditable, zero-infrastructure way t
|
||||
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 ════ │
|
||||
Peer A Peer B
|
||||
| |
|
||||
| 1. invitation |
|
||||
|.................................> | carried by QR, link or paste
|
||||
| |
|
||||
| 2. response |
|
||||
| <.................................|
|
||||
| |
|
||||
| 3. both read the same safety code
|
||||
| and compare it out loud |
|
||||
| |
|
||||
| 4. both confirm, session 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.
|
||||
1. **Peer A** creates an invitation, shareable as a QR code, a link or plain text.
|
||||
2. **Peer B** opens it and returns a response the same way.
|
||||
3. Both sides now show the same **safety code**. Compare it over something an attacker cannot impersonate: in person, or a voice you recognise.
|
||||
4. The chat unlocks only after both people confirm the matching code. Three incorrect attempts end the session.
|
||||
|
||||
Step 3 is not a formality. Completing the key exchange proves that someone completed it, not who. Anyone able to intercept and rewrite the invitation can do that with both of you at once, and comparing the code is what catches it.
|
||||
|
||||
## Security model
|
||||
|
||||
| Layer | Mechanism |
|
||||
| --- | --- |
|
||||
| Key agreement | ECDH (P-384), per-session derived keys |
|
||||
| Forward secrecy | Double Ratchet — per-message keys, DH re-key on each reply |
|
||||
| Forward secrecy | Double Ratchet: per-message keys, re-keyed on each reply |
|
||||
| Transport | WebRTC data channel over DTLS |
|
||||
| Message encryption | AES-256-GCM, end-to-end |
|
||||
| Authentication | Interactive SAS bound to both peers' DTLS fingerprints |
|
||||
| Authentication | Interactive safety code bound to both peers' DTLS fingerprints |
|
||||
| Integrity | HMAC + replay protection |
|
||||
| Sanitization | DOMPurify text-only rendering boundary |
|
||||
| Local storage | Encrypted 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. Until it is completed, the session will not act on control messages from the peer.
|
||||
A session is **not** treated as verified until both peers complete the safety code comparison. 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. Until it is completed, the session will not act on control messages from the peer.
|
||||
|
||||
### Forward secrecy
|
||||
|
||||
Message protection does not rest on the keys agreed during the handshake. On top of them SecureBit runs the **Double Ratchet** — the design used by Signal:
|
||||
Message protection does not rest on the keys agreed during the handshake. On top of them SecureBit runs the **Double Ratchet**, the design used by Signal:
|
||||
|
||||
- **Every message gets its own key.** It is derived from a chain key through a one-way function and discarded as soon as the message is encrypted or read, so keys held now cannot reconstruct earlier ones. Recovering the live state of a session does not expose what was said before.
|
||||
- **Each change of direction re-keys the session.** Every reply introduces a fresh ECDH key pair and mixes a new shared secret into the root key, so the conversation continuously moves away from any state an attacker may have captured.
|
||||
- **Out-of-order messages are handled within fixed bounds.** Keys are held for messages that have not arrived yet, capped at 512 per chain and 1024 in total and expiring after five minutes, with a limit on how far ahead a message may claim to be.
|
||||
|
||||
The ratchet is negotiated during the handshake and used when both peers support it. If one side is on an older release, the session falls back to per-session keys and the security panel reports which of the two is actually in use — it shows the state of your connection, not the capabilities of your client.
|
||||
The ratchet is negotiated during the handshake and used when both peers support it. If one side is on an older release, the session falls back to per-session keys and the security panel reports which of the two is actually in use. It shows the state of your connection, not the capabilities of your client.
|
||||
|
||||
> [!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).
|
||||
> 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' addresses and traffic timing, though never message contents. See [`doc/USE-POLICY.md`](doc/USE-POLICY.md).
|
||||
|
||||
## Quick start
|
||||
|
||||
@@ -140,7 +145,7 @@ Open the printed local URL in two browser windows or profiles, then:
|
||||
|
||||
### 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.
|
||||
SecureBit is a progressive web app. 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
|
||||
|
||||
@@ -156,7 +161,7 @@ Configure your own STUN/TURN servers under **Advanced network settings**, or at
|
||||
|
||||
### File transfer policy
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
@@ -185,16 +190,23 @@ dist/ built bundles served in production
|
||||
|
||||
## Documentation
|
||||
|
||||
- [`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
|
||||
Everything technical lives in [`doc/`](doc/README.md).
|
||||
|
||||
## Contributing & responsible use
|
||||
| Document | What it covers |
|
||||
| --- | --- |
|
||||
| [`doc/ARCHITECTURE.md`](doc/ARCHITECTURE.md) | How a session is established, verified and torn down |
|
||||
| [`doc/CRYPTOGRAPHY.md`](doc/CRYPTOGRAPHY.md) | Key schedule, the Double Ratchet, verification, memory handling |
|
||||
| [`doc/CONFIGURATION.md`](doc/CONFIGURATION.md) | Deployment, ICE and TURN, privacy modes, file policy |
|
||||
| [`doc/CALLS.md`](doc/CALLS.md) | Voice and video: codecs, adaptation, and why each value was chosen |
|
||||
| [`doc/API.md`](doc/API.md) | Internal interfaces |
|
||||
| [`doc/CONTRIBUTING.md`](doc/CONTRIBUTING.md) | Development workflow |
|
||||
| [`doc/USE-POLICY.md`](doc/USE-POLICY.md) | Terms of use and the limits of what the software protects |
|
||||
| [`SECURITY.md`](SECURITY.md) | Security policy and vulnerability reporting |
|
||||
| [`CHANGELOG.md`](CHANGELOG.md) | Release history |
|
||||
|
||||
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.
|
||||
## Contributing
|
||||
|
||||
Issues and pull requests are welcome. Read [`doc/CONTRIBUTING.md`](doc/CONTRIBUTING.md) for the workflow and [`doc/USE-POLICY.md`](doc/USE-POLICY.md) for what the project is and is not for.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
# Responsible Use Guidelines
|
||||
|
||||
## 🌐 SecureBit.chat: A Tool for Digital Freedom
|
||||
|
||||
### Core Principles
|
||||
|
||||
SecureBit.chat empowers users with military-grade encryption and privacy protection. This power must be exercised responsibly.
|
||||
|
||||
---
|
||||
|
||||
## 📖 User Code of Ethics
|
||||
|
||||
### ✅ Encouraged Applications
|
||||
|
||||
**Protecting Vulnerable Populations:**
|
||||
- Journalists protecting source confidentiality
|
||||
- Human rights activists organizing safely
|
||||
- Whistleblowers exposing corruption or wrongdoing
|
||||
- Citizens in oppressive regimes communicating freely
|
||||
|
||||
**Legitimate Privacy Needs:**
|
||||
- Business communications requiring confidentiality
|
||||
- Personal conversations deserving privacy
|
||||
- Medical or legal consultations
|
||||
- Research collaboration requiring discretion
|
||||
|
||||
**Educational and Research:**
|
||||
- Academic study of cryptographic protocols
|
||||
- Security research and vulnerability testing
|
||||
- Privacy advocacy and digital rights education
|
||||
- Demonstrating importance of encryption
|
||||
|
||||
### ❌ Prohibited Applications
|
||||
|
||||
**Criminal Activities:**
|
||||
- Planning or coordinating illegal activities
|
||||
- Fraud, theft, or financial crimes
|
||||
- Distribution of illegal content
|
||||
- Harassment, threats, or stalking
|
||||
|
||||
**Harmful Conduct:**
|
||||
- Facilitating violence or terrorism
|
||||
- Child exploitation or abuse
|
||||
- Drug trafficking or illegal commerce
|
||||
- Identity theft or impersonation
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ Operational Security (OPSEC) Guidelines
|
||||
|
||||
### Device Security
|
||||
- Keep devices updated with latest security patches
|
||||
- Use strong, unique passwords and 2FA
|
||||
- Avoid shared or compromised devices
|
||||
- Consider dedicated devices for sensitive communications
|
||||
|
||||
### Communication Security
|
||||
- Always verify security codes through separate channels
|
||||
- Use voice calls or in-person meetings for verification
|
||||
- Be aware of who has physical access to your devices
|
||||
- Understand metadata risks despite encryption
|
||||
|
||||
### Environmental Awareness
|
||||
- Consider your physical surroundings when communicating
|
||||
- Be aware of camera/microphone security
|
||||
- Understand local surveillance capabilities
|
||||
- Use appropriate network security (VPN, Tor, etc.)
|
||||
|
||||
---
|
||||
|
||||
## ⚖️ Legal Compliance Framework
|
||||
|
||||
### Know Your Local Laws
|
||||
- Research encryption regulations in your jurisdiction
|
||||
- Understand penalties for violation of communication laws
|
||||
- Be aware of mandatory disclosure requirements
|
||||
- Consider legal implications of cross-border communications
|
||||
|
||||
### Documentation and Records
|
||||
- Keep records of legitimate use cases
|
||||
- Document business justifications for encryption use
|
||||
- Maintain compliance with organizational policies
|
||||
- Consider legal counsel for sensitive applications
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Community Standards
|
||||
|
||||
### Respectful Communication
|
||||
- Treat all users with dignity and respect
|
||||
- Avoid harassment or abusive language
|
||||
- Respect others' privacy and security needs
|
||||
- Report illegal or abusive behavior to authorities
|
||||
|
||||
### Collaborative Security
|
||||
- Share security knowledge responsibly
|
||||
- Report vulnerabilities through proper channels
|
||||
- Contribute to open-source development ethically
|
||||
- Support digital rights advocacy
|
||||
|
||||
---
|
||||
|
||||
## 📞 Incident Reporting
|
||||
|
||||
### Security Concerns
|
||||
- Report vulnerabilities to the development team
|
||||
- Use responsible disclosure practices
|
||||
- Provide detailed, actionable information
|
||||
- Allow reasonable time for fixes before public disclosure
|
||||
|
||||
### Abuse Reports
|
||||
- Report criminal activity to law enforcement
|
||||
- Document evidence appropriately
|
||||
- Cooperate with legitimate legal processes
|
||||
- Protect victim privacy while reporting
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Supporting Digital Rights
|
||||
|
||||
### Advocacy Opportunities
|
||||
- Support organizations fighting for digital rights
|
||||
- Educate others about importance of privacy
|
||||
- Participate in policy discussions
|
||||
- Vote for representatives who support digital freedoms
|
||||
|
||||
### Responsible Innovation
|
||||
- Contribute to open-source privacy tools
|
||||
- Support development of better security practices
|
||||
- Share knowledge through responsible education
|
||||
- Advance the field of secure communications
|
||||
|
||||
---
|
||||
|
||||
**Remember: With the power to communicate privately comes the responsibility to do so ethically and legally.**
|
||||
|
||||
*Your choices determine whether privacy tools serve freedom or harm society.*
|
||||
+80
-53
@@ -1,71 +1,95 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported release line
|
||||
|
||||
| Release | Status | Protocol |
|
||||
| --- | --- | --- |
|
||||
| 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.
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
Please report security issues privately before public disclosure.
|
||||
Report privately, before public disclosure.
|
||||
|
||||
- Email: `SecureBitChat@proton.me`
|
||||
- Include: affected version, reproduction steps, impact, and any proof-of-concept material
|
||||
- Avoid publishing exploit details before a coordinated fix is available
|
||||
- Include the affected version, steps to reproduce, the impact you see, and any
|
||||
proof-of-concept material
|
||||
- Please allow time for a fix to reach users before publishing details
|
||||
|
||||
## Current security behavior
|
||||
There is no server to patch centrally. Every user has to load a new build, so
|
||||
early publication exposes exactly the people who have not updated yet. That is
|
||||
the only reason for the delay, and it is not indefinite: if you do not get a
|
||||
response within a reasonable time, say so and set your own timeline.
|
||||
|
||||
### Peer verification
|
||||
Reports about a specific deployment (someone else's hosted instance, a TURN
|
||||
server) should go to whoever operates it.
|
||||
|
||||
- SAS verification is mandatory and interactive.
|
||||
- SAS values are derived deterministically from shared session material.
|
||||
- Users must compare the code out of band and enter the matching code manually.
|
||||
- A session becomes verified only after both local and remote confirmations succeed.
|
||||
- Three failed local SAS entries terminate the session.
|
||||
- Protocol version `4.1` rejects incompatible peers instead of silently falling back to older verification behavior.
|
||||
## Supported versions
|
||||
|
||||
### Message handling
|
||||
| Release | Status |
|
||||
| --- | --- |
|
||||
| 5.7.x | Supported |
|
||||
| 5.6.x | Superseded, update recommended |
|
||||
| 5.5.x and earlier | Unsupported |
|
||||
|
||||
- Chat payloads remain encrypted in transit.
|
||||
- Decrypted incoming chat text is sanitized before it reaches React state or the UI.
|
||||
- Encrypted payload validation remains separate from display sanitization.
|
||||
Because the application is served fresh from the network on each load, most users
|
||||
are on the current release automatically. Installed progressive web app instances
|
||||
update on the next launch after a new build is deployed.
|
||||
|
||||
### File transfer
|
||||
Forward secrecy applies only when both peers are on 5.7.0 or later. The feature
|
||||
is negotiated, and a session with an older peer falls back to the previous scheme.
|
||||
The security panel in the application shows which is in force for the current
|
||||
connection.
|
||||
|
||||
- Incoming transfer metadata is validated before presentation to the user.
|
||||
- Every incoming file requires explicit Accept or Reject consent.
|
||||
- Receive buffers are not allocated before consent.
|
||||
- File names are normalized for display and dangerous names are rejected.
|
||||
- Allowed file types are explicit and validated using both MIME type and extension.
|
||||
- High-risk executable or scriptable types are blocked.
|
||||
- Repeated incoming transfer offers are rate-limited and bounded.
|
||||
## What the software guarantees
|
||||
|
||||
### Local storage
|
||||
Message content between two peers who have compared their safety code, against an
|
||||
attacker on the network between them.
|
||||
|
||||
- Sensitive IndexedDB metadata is encrypted, including timestamps and session-related fields where feasible.
|
||||
- Only minimum lookup keys remain in plaintext when required.
|
||||
- Legacy plaintext metadata is migrated lazily on read.
|
||||
- Corrupted encrypted metadata fails closed.
|
||||
That guarantee has a precondition, and it is not optional. Completing the key
|
||||
exchange proves that someone completed it, not who. The safety code comparison is
|
||||
what identifies the peer, and it has to happen over a channel an attacker cannot
|
||||
impersonate.
|
||||
|
||||
### Network privacy
|
||||
## What it does not guarantee
|
||||
|
||||
- Default mode preserves standard WebRTC connectivity.
|
||||
- Relay-only privacy mode uses TURN by setting `iceTransportPolicy: "relay"`.
|
||||
- STUN-only configurations do not provide IP protection.
|
||||
- If TURN is absent, the UI warns that direct WebRTC may expose IP addresses.
|
||||
- A compromised endpoint. Malware, a hostile extension or physical access to an
|
||||
unlocked device sees the plaintext.
|
||||
- Behaviour of the person you are talking to. View-once and disappearing messages
|
||||
are cooperative, not enforced against a determined recipient.
|
||||
- Network privacy by default. A direct connection reveals your IP address to the
|
||||
peer. Relay-only mode with your own TURN server prevents that, at the cost of
|
||||
the relay operator seeing both addresses and traffic timing.
|
||||
- The fact that you are using the software. That is visible to anyone watching
|
||||
your network.
|
||||
|
||||
### Lifecycle cleanup
|
||||
[doc/USE-POLICY.md](doc/USE-POLICY.md) goes into more detail.
|
||||
|
||||
- Disconnect cleanup closes data channels and peer connections, clears verification state, and wipes session crypto state.
|
||||
- Timers, deferred retries, decoy traffic, pending transfers, and React file-transfer callbacks are cleaned up on shutdown.
|
||||
- Received file buffers are retained only within a bounded window and expired handles fail gracefully.
|
||||
## Current behaviour
|
||||
|
||||
## Security verification commands
|
||||
**Verification.** The safety code is derived deterministically from shared
|
||||
session material and both DTLS fingerprints, and is compared manually out of
|
||||
band. A session becomes verified only after both peers confirm. Three incorrect
|
||||
entries end it. Until verification completes, the session refuses control
|
||||
messages from the peer: reconnection signalling, call setup, message deletion and
|
||||
delivery receipts.
|
||||
|
||||
**Message protection.** Chat content is encrypted with a per-message key from the
|
||||
Double Ratchet when both peers support it, and with per-session keys otherwise.
|
||||
Ratchet message keys are destroyed after a single use, and each change of
|
||||
direction re-keys the session root. Content reaches the interface through one
|
||||
authenticated path; unauthenticated frames are dropped rather than displayed.
|
||||
Decrypted text is sanitized before rendering.
|
||||
|
||||
**File transfer.** Metadata is validated before the user is prompted, every
|
||||
transfer requires explicit consent, and no receive buffer is allocated before
|
||||
consent. Accepted types are an explicit allowlist checked by extension, with MIME
|
||||
type as a secondary signal. Executable and scriptable formats are blocked. Voice
|
||||
notes are the one exception to the prompt, and qualify only if the receiver
|
||||
confirms they are genuine audio within size and per-session budgets.
|
||||
|
||||
**Local storage.** Sensitive IndexedDB metadata is stored encrypted. Legacy
|
||||
plaintext records migrate on read. Corrupted encrypted metadata fails closed. No
|
||||
message history is written to disk.
|
||||
|
||||
**Cleanup.** Disconnecting closes the channels, clears verification state, and
|
||||
overwrites the key material that can be overwritten. Timers, retries, cover
|
||||
traffic, pending transfers and interface callbacks are all torn down.
|
||||
|
||||
## Verifying a build
|
||||
|
||||
```bash
|
||||
npm audit
|
||||
@@ -73,9 +97,12 @@ npm test
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Limitations
|
||||
The bundles in `dist/` are committed and served directly. They are not currently
|
||||
verified against the sources by automation, so if you are reviewing this project
|
||||
seriously, build from source and compare rather than reading `src/` alone.
|
||||
|
||||
- A compromised endpoint can still expose plaintext.
|
||||
- WebRTC privacy depends on deployment configuration; TURN must be supplied by the operator.
|
||||
- Users must perform the out-of-band SAS comparison correctly.
|
||||
- Browser security and operating-system security remain part of the threat model.
|
||||
## Limitations of this policy
|
||||
|
||||
This is a small project. There has been no independent cryptographic audit. The
|
||||
documentation in [`doc/`](doc/) describes the design in enough detail to review
|
||||
it, and that is the intended substitute until an audit happens.
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
# Security Disclaimer and Terms of Use
|
||||
|
||||
SecureBit.chat is provided as open-source software for lawful private communication, research, and education. It is supplied **as is**, without warranties of any kind.
|
||||
|
||||
## User responsibilities
|
||||
|
||||
By using SecureBit.chat, you are responsible for:
|
||||
|
||||
- complying with applicable laws and organizational policies
|
||||
- securing your devices and browser environment
|
||||
- verifying SAS codes through an out-of-band channel
|
||||
- understanding that endpoint compromise can defeat application-layer protections
|
||||
- configuring TURN correctly when relay-only privacy mode is required
|
||||
|
||||
## Security limitations
|
||||
|
||||
No communication system can guarantee absolute security. SecureBit.chat reduces risk through encrypted transport, mandatory peer verification, explicit file-transfer consent, local metadata protection, and lifecycle cleanup, but it cannot protect against compromised devices, malicious users with physical access, or incorrect operational practices.
|
||||
|
||||
## Intended use
|
||||
|
||||
SecureBit.chat is intended for legitimate private communication, journalism, research, education, business confidentiality, and personal privacy. It is not intended to facilitate unlawful activity, abuse, harassment, or harm.
|
||||
|
||||
## Current release
|
||||
|
||||
- Product release: `v4.8.21`
|
||||
- Protocol version: `4.1`
|
||||
- Last updated: May 17, 2026
|
||||
Vendored
+22
-18
@@ -3469,7 +3469,7 @@ var EnhancedSecureCryptoUtils = class _EnhancedSecureCryptoUtils {
|
||||
if (!signedPackage || typeof signedPackage !== "object") {
|
||||
throw new Error("Invalid signed package format");
|
||||
}
|
||||
const { keyType, keyData, timestamp, version, signature } = signedPackage;
|
||||
const { keyType, keyData, timestamp, version: version2, signature } = signedPackage;
|
||||
if (!keyType || !keyData || !timestamp || !signature) {
|
||||
throw new Error("Missing required fields in signed package");
|
||||
}
|
||||
@@ -3481,7 +3481,7 @@ var EnhancedSecureCryptoUtils = class _EnhancedSecureCryptoUtils {
|
||||
throw new Error("Signed key package is too old");
|
||||
}
|
||||
await _EnhancedSecureCryptoUtils.validateKeyStructure(keyData, keyType);
|
||||
const packageCopy = { keyType, keyData, timestamp, version };
|
||||
const packageCopy = { keyType, keyData, timestamp, version: version2 };
|
||||
const packageString = JSON.stringify(packageCopy);
|
||||
const isValidSignature = await _EnhancedSecureCryptoUtils.verifySignature(verifyingKey, signature, packageString);
|
||||
if (!isValidSignature) {
|
||||
@@ -11050,7 +11050,7 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
|
||||
oldKeysCount: this.oldKeys.size,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
for (const [version, keySet] of this.oldKeys.entries()) {
|
||||
for (const [version2, keySet] of this.oldKeys.entries()) {
|
||||
if (keySet.encryptionKey) {
|
||||
this._secureWipeMemory(keySet.encryptionKey, "pfs_key_wipe");
|
||||
}
|
||||
@@ -13651,7 +13651,7 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
|
||||
const now = Date.now();
|
||||
const maxKeyAge = _EnhancedSecureWebRTCManager.LIMITS.MAX_KEY_AGE;
|
||||
let wipedKeysCount = 0;
|
||||
for (const [version, keySet] of this.oldKeys.entries()) {
|
||||
for (const [version2, keySet] of this.oldKeys.entries()) {
|
||||
if (now - keySet.timestamp > maxKeyAge) {
|
||||
if (keySet.encryptionKey) {
|
||||
this._secureWipeMemory(keySet.encryptionKey, "pfs_cleanup_wipe");
|
||||
@@ -13666,10 +13666,10 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
|
||||
keySet.macKey = null;
|
||||
keySet.metadataKey = null;
|
||||
keySet.keyFingerprint = null;
|
||||
this.oldKeys.delete(version);
|
||||
this.oldKeys.delete(version2);
|
||||
wipedKeysCount++;
|
||||
this._secureLog("info", "\u{1F9F9} Old PFS keys hard wiped and cleaned up", {
|
||||
version,
|
||||
version: version2,
|
||||
age: Math.round((now - keySet.timestamp) / 1e3) + "s",
|
||||
timestamp: Date.now()
|
||||
});
|
||||
@@ -13682,8 +13682,8 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
|
||||
}
|
||||
}
|
||||
// PFS: Get keys for specific version (for decryption)
|
||||
getKeysForVersion(version) {
|
||||
const oldKeySet = this.oldKeys.get(version);
|
||||
getKeysForVersion(version2) {
|
||||
const oldKeySet = this.oldKeys.get(version2);
|
||||
if (oldKeySet && oldKeySet.encryptionKey && oldKeySet.macKey && oldKeySet.metadataKey) {
|
||||
return {
|
||||
encryptionKey: oldKeySet.encryptionKey,
|
||||
@@ -13691,7 +13691,7 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
|
||||
metadataKey: oldKeySet.metadataKey
|
||||
};
|
||||
}
|
||||
if (version === this.currentKeyVersion) {
|
||||
if (version2 === this.currentKeyVersion) {
|
||||
if (this.encryptionKey && this.macKey && this.metadataKey) {
|
||||
return {
|
||||
encryptionKey: this.encryptionKey,
|
||||
@@ -13701,7 +13701,7 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
|
||||
}
|
||||
}
|
||||
window.EnhancedSecureCryptoUtils.secureLog.log("error", "No valid keys found for version", {
|
||||
requestedVersion: version,
|
||||
requestedVersion: version2,
|
||||
currentVersion: this.currentKeyVersion,
|
||||
availableVersions: Array.from(this.oldKeys.keys())
|
||||
});
|
||||
@@ -15826,8 +15826,8 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
|
||||
throw new Error("Connection rate limit exceeded. Please wait before trying again.");
|
||||
}
|
||||
const timestamp = offerData.ts || offerData.timestamp;
|
||||
const version = offerData.v || offerData.version;
|
||||
if (!timestamp || !version) {
|
||||
const version2 = offerData.v || offerData.version;
|
||||
if (!timestamp || !version2) {
|
||||
throw new Error("Missing required security fields in offer data \u2013 possible MITM attack");
|
||||
}
|
||||
const offerAge = Date.now() - timestamp;
|
||||
@@ -15844,7 +15844,7 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
|
||||
}
|
||||
throw new Error("Offer data is too old \u2013 possible replay attack");
|
||||
}
|
||||
const protocolVersion = version;
|
||||
const protocolVersion = version2;
|
||||
if (protocolVersion !== _EnhancedSecureWebRTCManager.PROTOCOL_VERSION) {
|
||||
this._secureLog("warn", "Protocol version mismatch detected", {
|
||||
operationId,
|
||||
@@ -16473,8 +16473,8 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
|
||||
throw new Error("CRITICAL SECURITY FAILURE: ECDSA key missing keyData or signature");
|
||||
}
|
||||
const timestamp = answerData.ts || answerData.timestamp;
|
||||
const version = answerData.v || answerData.version;
|
||||
if (!timestamp || !version) {
|
||||
const version2 = answerData.v || answerData.version;
|
||||
if (!timestamp || !version2) {
|
||||
throw new Error("Missing required fields in response data \u2013 possible MITM attack");
|
||||
}
|
||||
if (answerData.sessionId && this.sessionId && answerData.sessionId !== this.sessionId) {
|
||||
@@ -19236,9 +19236,9 @@ var SecureKeyStorage = class {
|
||||
}
|
||||
};
|
||||
var SecureIndexedDBWrapper = class {
|
||||
constructor(dbName = "SecureKeyStorage", version = 1) {
|
||||
constructor(dbName = "SecureKeyStorage", version2 = 1) {
|
||||
this.dbName = dbName;
|
||||
this.version = version;
|
||||
this.version = version2;
|
||||
this.db = null;
|
||||
this.KEYS_STORE = "encrypted_keys";
|
||||
this.METADATA_STORE = "key_metadata";
|
||||
@@ -19983,7 +19983,11 @@ var SecureMasterKeyManager = class {
|
||||
// src/scripts/app-boot.js
|
||||
var import_NotificationIntegration = __toESM(require_NotificationIntegration());
|
||||
|
||||
// package.json
|
||||
var version = "5.7.2";
|
||||
|
||||
// src/components/ui/Header.jsx
|
||||
var APP_VERSION = `v${version}`;
|
||||
var EnhancedMinimalHeader = ({
|
||||
status,
|
||||
fingerprint,
|
||||
@@ -20424,7 +20428,7 @@ Right-click or Ctrl+click to disconnect`,
|
||||
React.createElement("div", { key: "txt", style: { lineHeight: 1.2, minWidth: 0 } }, [
|
||||
React.createElement("div", { key: "r1", style: { display: "flex", alignItems: "baseline", gap: "7px" } }, [
|
||||
React.createElement("span", { key: "n", style: { fontSize: "16px", fontWeight: 800, letterSpacing: "-0.3px", color: "#e8e8eb" } }, "SecureBit"),
|
||||
React.createElement("span", { key: "v", style: { fontFamily: MONO, fontSize: "10px", fontWeight: 500, color: "#56565e" } }, "v5.6.0")
|
||||
React.createElement("span", { key: "v", style: { fontFamily: MONO, fontSize: "10px", fontWeight: 500, color: "#56565e" } }, APP_VERSION)
|
||||
]),
|
||||
React.createElement("div", { key: "r2", className: "hidden sm:block", style: { fontSize: "11px", color: "#6b6b73", fontWeight: 500 } }, "End-to-end encrypted")
|
||||
])
|
||||
|
||||
Vendored
+4
-4
File diff suppressed because one or more lines are too long
+2
-2
@@ -20,7 +20,7 @@
|
||||
connection. It is negotiated: both peers advertise `RATCHET_VERSION` in the
|
||||
offer and answer, and a peer that does not falls back to per-session keys.
|
||||
- `_ratchet.canEncrypt` is false on the joining peer until the inviting peer's
|
||||
first message arrives — the sending chain does not exist until then. Callers
|
||||
first message arrives, because the sending chain does not exist until then. Callers
|
||||
must check it rather than assume; the send path falls back to session keys for
|
||||
those first frames.
|
||||
- `_ratchet.getState()` returns counters and the number of retained keys for
|
||||
@@ -52,7 +52,7 @@
|
||||
- The `isVoice` a callback receives is the **receiver's** verdict, not the
|
||||
sender's claim: `validateIncomingMetadata` clears it unless the transfer
|
||||
declares a recognised audio MIME type and fits the per-note and per-session
|
||||
size budgets. A transfer that fails those checks is not rejected — it simply
|
||||
size budgets. A transfer that fails those checks is not rejected; it simply
|
||||
loses the consent-free shortcut and is offered as a normal file.
|
||||
|
||||
## EnhancedSecureFileTransfer
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
# Architecture
|
||||
|
||||
SecureBit.chat is a browser application with no backend. Two browsers negotiate a
|
||||
direct WebRTC connection, derive keys from an ECDH exchange, and confirm each
|
||||
other's identity by comparing a short code out of band. Everything after that
|
||||
runs between the two endpoints.
|
||||
|
||||
There is no server in the message path, and there is no signalling service. The
|
||||
offer and the answer are moved between the two people by whatever channel they
|
||||
already have (a QR code, a pasted block of text, a link). That choice shapes the
|
||||
rest of the design: the out-of-band channel is untrusted, so the protocol assumes
|
||||
an attacker can read and rewrite anything travelling over it, and the safety code
|
||||
comparison is what closes that gap.
|
||||
|
||||
## Session lifecycle
|
||||
|
||||
```text
|
||||
1. Invitation Peer A generates an ECDH key pair, an SDP offer and a session
|
||||
salt, and exports them as a single invitation.
|
||||
|
||||
2. Response Peer B validates the invitation, derives the shared secret,
|
||||
and returns its own keys and SDP as a response.
|
||||
|
||||
3. Transport up DTLS completes and the data channel opens. At this point both
|
||||
sides hold session keys, but neither knows who the other is.
|
||||
|
||||
4. Verification Both sides display the same safety code. The users compare it
|
||||
over a channel an attacker cannot impersonate and enter it.
|
||||
|
||||
5. Verified Only now does the session accept traffic that changes state,
|
||||
and only now does the chat open.
|
||||
```
|
||||
|
||||
Step 3 is the one worth dwelling on. Completing the handshake proves that someone
|
||||
performed a key exchange. It does not prove who. Anyone positioned on the
|
||||
out-of-band channel can substitute their own keys and complete step 3 with both
|
||||
people at once. Step 4 is the only step that distinguishes the intended peer, so
|
||||
everything that could be useful to an impostor waits for it.
|
||||
|
||||
## What verification gates
|
||||
|
||||
Verification is enforced, not merely displayed. Until both sides confirm:
|
||||
|
||||
- reconnection signalling is refused
|
||||
- call setup is refused
|
||||
- message deletion and delivery receipts are refused
|
||||
- incoming file transfers are refused
|
||||
|
||||
The verification exchange itself and liveness probes run earlier, because they
|
||||
have to. That set is an allowlist in the code
|
||||
(`POST_VERIFICATION_CONTROL_TYPES`), and anything not on it is rejected by
|
||||
default rather than passed through.
|
||||
|
||||
The verified state is set in one place, which refuses the transition unless the
|
||||
local user has actually confirmed the code. Three incorrect entries end the
|
||||
session.
|
||||
|
||||
## Message protection layers
|
||||
|
||||
```text
|
||||
ECDH P-384 exchange
|
||||
|
|
||||
HKDF key schedule
|
||||
|
|
||||
.........................................................
|
||||
| | | | |
|
||||
message MAC metadata fingerprint ratchet root
|
||||
key key key (safety code) |
|
||||
Double Ratchet
|
||||
per-message keys
|
||||
```
|
||||
|
||||
Chat content is encrypted with a ratchet-derived key when both peers support the
|
||||
ratchet, and with the session message key otherwise. Either way it reaches the
|
||||
interface through a single authenticated path. Frames that fail authentication
|
||||
are dropped rather than displayed, so nothing appears in a conversation that has
|
||||
not been verified as coming from the peer holding the session keys.
|
||||
|
||||
## Forward secrecy
|
||||
|
||||
Per-message keys come from a chain key through a one-way function and are
|
||||
destroyed after a single use, so a key held now cannot reconstruct an earlier
|
||||
one. Each change of direction in the conversation introduces a fresh ECDH key
|
||||
pair, which re-keys the session root and moves it away from any state an attacker
|
||||
may have captured.
|
||||
|
||||
Out-of-order delivery is supported within fixed bounds: 512 skipped keys per
|
||||
chain, 1024 retained in total, expiring after five minutes. These are a resource
|
||||
control rather than a tuning parameter, because the message number is supplied by
|
||||
the peer.
|
||||
|
||||
Incoming frames are authenticated before any ratchet state is committed. A frame
|
||||
that fails leaves the ratchet untouched, so a malformed or forged frame cannot
|
||||
desynchronise an established session.
|
||||
|
||||
[CRYPTOGRAPHY.md](CRYPTOGRAPHY.md) has the key schedule and the frame format.
|
||||
|
||||
## Session recovery
|
||||
|
||||
A network path can break without anything closing: switching from Wi-Fi to a
|
||||
mobile network, a NAT rebind, a tunnel. The data channel keeps reporting itself
|
||||
as open while packets stop arriving.
|
||||
|
||||
Recovery renegotiates only the transport path, using an ICE restart carried over
|
||||
the existing encrypted channel. The DTLS session, the session keys, the ratchet
|
||||
state and the message history all sit above ICE and survive it, so a repaired
|
||||
connection is the same session and needs no new verification.
|
||||
|
||||
Silence alone is not treated as a dead peer. A backgrounded tab is frozen by the
|
||||
browser and answers nothing, while ICE consent checks continue in the browser's
|
||||
network stack. A connected ICE state therefore means a silent peer is asleep, not
|
||||
gone. Only an unanswered probe on a degraded path starts recovery.
|
||||
|
||||
Recovery gives up when it cannot succeed: when nothing has arrived from the peer
|
||||
since the break (no route exists for the renegotiation), or when the ICE agent
|
||||
produces no candidate pairs at all (restarting cannot rebind it). A session that
|
||||
cannot be recovered is closed and its data wiped rather than left half alive.
|
||||
|
||||
## File transfer
|
||||
|
||||
1. The sender emits metadata.
|
||||
2. The receiver validates name, size, type and abuse limits.
|
||||
3. The receiver is shown an Accept or Reject prompt.
|
||||
4. No receive buffers are allocated before acceptance.
|
||||
5. Chunks are transmitted only after acceptance.
|
||||
6. Completed buffers are retained within a bounded window.
|
||||
|
||||
Voice notes reuse this pipeline and inherit its per-file AES-GCM session key,
|
||||
chunking and SHA-256 integrity check. They differ in three ways: the audio is
|
||||
recorded in the browser, the duration and waveform travel as unsigned
|
||||
presentation metadata (the audio bytes stay covered by the signed hash), and the
|
||||
receiver accepts them without a prompt so they can play inline.
|
||||
|
||||
That last point is why the receiver decides what counts as a voice note. The
|
||||
sender's claim is not enough: a transfer qualifies only if it declares a
|
||||
recognised audio MIME type, stays under 4 MB, and fits a per-session budget of
|
||||
64 MB. Anything else is handled as an ordinary file and goes through the normal
|
||||
prompt. This keeps the convenience of voice notes from becoming a channel for
|
||||
unattended transfers.
|
||||
|
||||
## Disconnect
|
||||
|
||||
The disconnect path clears:
|
||||
|
||||
- WebRTC channels and peer connection handles
|
||||
- timers, deferred retries, cover traffic and decoy traffic
|
||||
- pending transfer state and consent waits
|
||||
- verification state and session key material
|
||||
- ratchet state: the root key, both chain keys and every retained message key are
|
||||
overwritten rather than only dereferenced
|
||||
- React file transfer callbacks and stale interface state
|
||||
|
||||
Values that cannot be overwritten in JavaScript are documented as such rather
|
||||
than reported as cleared. See the memory handling section of
|
||||
[CRYPTOGRAPHY.md](CRYPTOGRAPHY.md).
|
||||
|
||||
## Multiple conversations
|
||||
|
||||
Each conversation gets its own manager instance, peer connection, key material
|
||||
and verification state, held in a map keyed by session. Nothing is shared between
|
||||
them, so two conversations cannot mix, and closing one leaves the others
|
||||
connected.
|
||||
|
||||
## Code layout
|
||||
|
||||
| Path | Responsibility |
|
||||
| --- | --- |
|
||||
| `src/network/EnhancedSecureWebRTCManager.js` | Connection lifecycle, verification, session state, message routing |
|
||||
| `src/network/webrtc/` | Call stack: SDP handling, audio and video senders, network adaptation |
|
||||
| `src/crypto/DoubleRatchet.js` | Forward secrecy: root and chain keys, DH ratchet, skipped keys |
|
||||
| `src/crypto/EnhancedSecureCryptoUtils.js` | Key generation, key schedule, message encryption, sanitization |
|
||||
| `src/crypto/cose-qr.js` | Invitation packing for QR transport |
|
||||
| `src/transfer/EnhancedSecureFileTransfer.js` | Chunked encrypted transfers, consent, type policy |
|
||||
| `src/state/sessionsStore.js` | Reducer for the set of open conversations |
|
||||
| `src/app.jsx` | Interface and message rendering |
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
# Voice and video calls
|
||||
|
||||
Calls run over the same peer connection as the chat. Media is added to the
|
||||
existing connection and renegotiated onto it, and the call SDP is exchanged over
|
||||
the encrypted data channel rather than through any signalling service. Media
|
||||
therefore inherits the session's verification: the DTLS-SRTP fingerprints
|
||||
negotiated for the media were themselves carried over an authenticated channel.
|
||||
|
||||
All tunable values live in `src/network/webrtc/config.js`. This document explains
|
||||
where they come from, because the numbers are otherwise indistinguishable from
|
||||
arbitrary choices.
|
||||
|
||||
## Where the settings attach
|
||||
|
||||
A single `RTCRtpSender` cannot express codec ordering or fmtp parameters, so
|
||||
configuring a sender is spread across three WebRTC surfaces, each at the point in
|
||||
the lifecycle where it works:
|
||||
|
||||
| Concern | Surface | When | Implementation |
|
||||
| --- | --- | --- | --- |
|
||||
| Codec ordering (RED before Opus) | `transceiver.setCodecPreferences` | before creating the offer or answer | `applyAudioCodecPreferences` in `audio.js` |
|
||||
| Opus FEC, DTX, bitrate | SDP `a=fmtp` rewriting | after create, before `setLocalDescription` | `applyOpusSettings` in `sdp.js` |
|
||||
| Priority and maximum bitrate | `sender.setParameters` | after `setLocalDescription` | `configureAudioSender` in `audio.js` |
|
||||
|
||||
Both peers apply the same rewriting, so the negotiated session carries the
|
||||
parameters regardless of who called.
|
||||
|
||||
## Audio
|
||||
|
||||
Speech has to stay intelligible on a bad link, and that goal drives every value
|
||||
below.
|
||||
|
||||
### Opus parameters
|
||||
|
||||
| Parameter | Value | Reason |
|
||||
| --- | --- | --- |
|
||||
| `minptime` | 10 | Smaller packetisation interval, lower latency (RFC 7587, section 7) |
|
||||
| `useinbandfec` | 1 | In-band forward error correction reconstructs a lost packet from the next one. This is the main lever for staying intelligible at 15 to 20 percent loss (RFC 6716, section 2.1.7) |
|
||||
| `usedtx` | 1 | Discontinuous transmission stops sending during silence, leaving the transport free for video and FEC (RFC 7587, section 3.1.3) |
|
||||
| `stereo` | 0 | Mono halves the bitrate with no loss for speech |
|
||||
| `maxaveragebitrate` | 32000 | Comfortable wideband speech |
|
||||
| `cbr` | 0 | Variable bitrate spends bits only when needed |
|
||||
|
||||
### Redundancy
|
||||
|
||||
RED (RFC 2198) carries the previous frame's payload alongside each packet, so
|
||||
isolated losses recover without retransmission. It is enabled only when the
|
||||
browser advertises `audio/red` in `RTCRtpSender.getCapabilities('audio')`, which
|
||||
Chromium does and others vary on; when absent it is skipped silently. RED is
|
||||
ordered before Opus in codec preferences.
|
||||
|
||||
### Sender parameters
|
||||
|
||||
| Parameter | Value | Reason |
|
||||
| --- | --- | --- |
|
||||
| `maxBitrate` | 40000 bps | Headroom above the 32 kbps Opus target for RED redundancy |
|
||||
| `priority` | `high` | Audio wins bandwidth arbitration against video within the connection |
|
||||
| `networkPriority` | `high` | DSCP hint so audio is prioritised on the wire |
|
||||
|
||||
Audio is never throttled by the adaptation controller. Under sustained loss the
|
||||
video degrades and speech continues.
|
||||
|
||||
## Video
|
||||
|
||||
Codec preference order is VP9, AV1, H.264, VP8, applied through
|
||||
`setCodecPreferences`. Retransmission and FEC codecs are kept after the media
|
||||
codecs so they still function. VP9 and AV1 provide scalable coding; H.264 and VP8
|
||||
do not.
|
||||
|
||||
### Scalable coding rather than simulcast
|
||||
|
||||
This is a one-to-one connection with a single receiver, so one encoding with SVC
|
||||
is the right tool: a single stream that degrades by spatial or temporal layer. It
|
||||
is applied through `sender.setParameters` and needs no `addTransceiver` or rid
|
||||
configuration, which keeps it away from the media path that actually works.
|
||||
|
||||
| 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 | 1.5 Mbps | `balanced` |
|
||||
|
||||
`networkPriority` is `medium`, below audio. If a browser rejects the scalability
|
||||
mode, which Firefox and Safari do in places, `configureVideoSender` retries with
|
||||
a plain encoding.
|
||||
|
||||
### Why media is attached with addTrack
|
||||
|
||||
An explicit `addTransceiver({ sendEncodings })` path was built and then removed,
|
||||
because it broke media on real devices in two ways. On the answering side,
|
||||
reusing the transceiver created by `setRemoteDescription` rejected the SVC
|
||||
parameters outright. On repeat and role-reversed calls, the reused transceiver
|
||||
directions desynchronised: the call connected and `ontrack` fired, but no media
|
||||
flowed.
|
||||
|
||||
What ships instead attaches media with `addTrack`, reused across calls through
|
||||
`replaceTrack`, and lets the browser manage transceiver direction. This is what
|
||||
keeps audio and video flowing across reversed and repeated calls.
|
||||
|
||||
Multi-rid simulcast primitives (`buildVideoSendEncodings`) are kept and tested for
|
||||
a future group-call path, but are not wired into the one-to-one flow. They need
|
||||
`addTransceiver({ sendEncodings })`, which requires the problems above to be
|
||||
solved first, ideally with a two-connection browser test rig that is not set up.
|
||||
The adaptation controller is already simulcast-aware for when that lands.
|
||||
|
||||
## Transport feedback
|
||||
|
||||
The call m-lines need RTCP feedback and header extensions present. Most browsers
|
||||
emit them already, so this is an idempotent safety net:
|
||||
|
||||
| Media | RTCP feedback | Header extension |
|
||||
| --- | --- | --- |
|
||||
| Video | `transport-cc`, `nack`, `nack pli`, `ccm fir`, `goog-remb` | transport-wide congestion control |
|
||||
| Audio | `transport-cc`, `nack` | transport-wide congestion control |
|
||||
|
||||
These are added only when missing, never duplicated, and applied only to primary
|
||||
codecs. Transport-wide congestion control is what feeds the bandwidth estimator
|
||||
that adaptation reads.
|
||||
|
||||
Rewritten local SDP is applied with progressive fallback: full rewrite, then Opus
|
||||
only, then raw. A browser that rejects an injected line cannot break the call.
|
||||
|
||||
## Adaptation
|
||||
|
||||
`NetworkAdaptationController` samples `pc.getStats()` every second and reacts:
|
||||
|
||||
| Condition | Action |
|
||||
| --- | --- |
|
||||
| Loss above 10 percent, or round trip above 300 ms | Reduce video `maxBitrate` by 20 percent, floor 100 kbps |
|
||||
| Loss below 3 percent and round trip below 150 ms, sustained 5 samples | Raise video `maxBitrate` by 10 percent, up to the ceiling |
|
||||
| `qualityLimitationReason` is `cpu` | Scale resolution down by 1.5, bitrate unchanged |
|
||||
|
||||
Every change goes through `sender.setParameters`. There is no renegotiation and
|
||||
no track restart, so adaptation is invisible to the call. Audio is never touched.
|
||||
|
||||
The decision function and the stats parsing are pure and unit-tested against
|
||||
recorded `getStats` output.
|
||||
|
||||
### Quality indicator
|
||||
|
||||
The same sample produces a coarse label shown in the call interface:
|
||||
|
||||
| Label | Condition |
|
||||
| --- | --- |
|
||||
| Excellent | Loss below 3 percent and round trip below 150 ms |
|
||||
| Good | Loss below 7 percent and round trip below 250 ms |
|
||||
| Fair | Loss below 15 percent and round trip below 400 ms |
|
||||
| Weak | Anything else |
|
||||
|
||||
It appears in the voice overlay, the video top bar, and as compact bars in the
|
||||
minimized widget, and stays hidden until the first sample has data.
|
||||
|
||||
## Verifying a change
|
||||
|
||||
Unit tests cover SDP rewriting, video codec selection and the adaptation
|
||||
decision:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
For anything touching media in practice, open `chrome://webrtc-internals` during
|
||||
a call and check that:
|
||||
|
||||
- outbound audio shows Opus with the fmtp parameters above, appearing as `red`
|
||||
and `opus` on Chromium
|
||||
- throttling the link steps outbound video `targetBitrate` down within a second
|
||||
or two and recovers when the link clears
|
||||
- audio bitrate holds steady while video adapts
|
||||
- the in-call indicator moves through Fair and Weak as the link degrades
|
||||
+122
-34
@@ -1,12 +1,13 @@
|
||||
# Configuration Guide
|
||||
# Configuration
|
||||
|
||||
## Requirements
|
||||
|
||||
- modern browser with WebRTC and Web Crypto support
|
||||
- Node.js 18+ for local development
|
||||
- TURN service only when relay-only privacy mode is required
|
||||
- A browser with WebRTC and Web Crypto support
|
||||
- Node.js 18 or later, for building
|
||||
- A TURN service, if you need relay-only privacy mode or you expect users behind
|
||||
strict NAT
|
||||
|
||||
## Local setup
|
||||
## Building and running
|
||||
|
||||
```bash
|
||||
npm install
|
||||
@@ -14,50 +15,137 @@ npm run build
|
||||
npm run serve
|
||||
```
|
||||
|
||||
## ICE server configuration
|
||||
`npm run build` compiles the CSS, bundles the JavaScript into `dist/`, and
|
||||
regenerates `meta.json` with a build stamp. That stamp is what the update
|
||||
mechanism compares, and it is also written into `sw.js` so the browser reinstalls
|
||||
the service worker on each release. A deployment that skips `post-build` will not
|
||||
notify anyone that an update exists.
|
||||
|
||||
SecureBit.chat keeps existing STUN support for ordinary WebRTC connectivity. Deployments that require relay-only privacy must provide their own TURN service credentials through deployment configuration; public TURN credentials are intentionally not bundled.
|
||||
The application is static. Any web server can host it, and there is no backend to
|
||||
run.
|
||||
|
||||
## ICE and TURN
|
||||
|
||||
WebRTC needs to discover network paths between the two browsers. STUN is enough
|
||||
to learn a public address; TURN is needed when a direct path cannot be
|
||||
established, and is the only way to keep peers from seeing each other's IP
|
||||
addresses.
|
||||
|
||||
Configuration comes from three places, in order of precedence:
|
||||
|
||||
1. User settings under Advanced network settings, stored in IndexedDB per device
|
||||
2. `config/ice-servers.js`, an operator override loaded before the application
|
||||
3. Built-in public STUN defaults
|
||||
|
||||
`config/ice-servers.js` is not committed, because it is where operator TURN
|
||||
credentials would go. Use `config/ice-servers.example.js` as the template. The
|
||||
Docker image copies `config/ice-servers.prod.js` into place at build time.
|
||||
|
||||
```js
|
||||
window.SECUREBIT_ICE_SERVERS = [
|
||||
{ urls: 'stun:stun.example.org:3478' },
|
||||
{
|
||||
urls: [
|
||||
'turn:turn.example.org:3478?transport=udp',
|
||||
'turn:turn.example.org:3478?transport=tcp',
|
||||
'turns:turn.example.org:443?transport=tcp'
|
||||
],
|
||||
username: '...',
|
||||
credential: '...'
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
Offering several transports is worth the extra lines. UDP is the most widely
|
||||
usable, TCP covers networks that block UDP, and TURNS on 443 gets through
|
||||
firewalls that only allow HTTPS. Some browsers also fail to resolve STUN and TURN
|
||||
hostnames inside their WebRTC layer even when ordinary page DNS works, so listing
|
||||
a raw IP alongside the hostname is a reasonable fallback.
|
||||
|
||||
Any TURN credential shipped to a browser is public by definition, because the
|
||||
browser has to be able to read it. Treat it as a shared resource and apply quotas
|
||||
on the TURN server rather than relying on the credential staying secret.
|
||||
|
||||
### User-supplied servers
|
||||
|
||||
Users can paste their own STUN and TURN servers. Input is validated against an
|
||||
allowlist before it reaches `RTCPeerConnection`: only the `stun`, `stuns`, `turn`
|
||||
and `turns` schemes, a hostname or bracketed IPv6 address with an optional port,
|
||||
and an optional `transport=udp` or `transport=tcp` query. At most 10 servers with
|
||||
8 URLs each. Anything else is rejected with a specific reason.
|
||||
|
||||
### Privacy modes
|
||||
|
||||
| Mode | Behavior | IP privacy |
|
||||
| Mode | Behaviour | IP exposure |
|
||||
| --- | --- | --- |
|
||||
| default | standard WebRTC candidate gathering | direct candidates may expose IP addresses |
|
||||
| relay-only | `iceTransportPolicy: "relay"` | requires TURN and avoids direct peer candidates when configured correctly |
|
||||
| Default | Standard candidate gathering | Direct candidates can reveal addresses to the peer |
|
||||
| Relay-only | Sets `iceTransportPolicy: "relay"` | Requires TURN; peers see only the relay |
|
||||
|
||||
### Operational rules
|
||||
STUN is not a substitute for TURN here. It reveals your public address to the
|
||||
peer by design. Relay-only mode without a configured TURN server cannot connect
|
||||
at all, and the interface warns when TURN is missing.
|
||||
|
||||
- STUN is not a privacy substitute for TURN.
|
||||
- Relay-only mode without TURN cannot establish a working relay connection.
|
||||
- The UI warns users when TURN is missing.
|
||||
- Validate TURN deployment with browser WebRTC diagnostics before production rollout.
|
||||
Validate a TURN deployment with `chrome://webrtc-internals` before relying on it.
|
||||
A relay candidate should appear in the gathered set; if none does, the credentials
|
||||
or the ports are wrong.
|
||||
|
||||
## Verification flow
|
||||
### When connections fail
|
||||
|
||||
Protocol `4.1` requires interactive SAS verification:
|
||||
Candidate gathering finishes only when every configured server has replied or
|
||||
timed out. Behind a VPN or a restrictive firewall that may never happen, and the
|
||||
console will show `701` errors for each unreachable server.
|
||||
|
||||
1. both peers derive the same SAS from shared session material
|
||||
2. users compare the code out of band
|
||||
3. each user enters the matching code manually
|
||||
4. the chat unlocks only after both confirmations succeed
|
||||
The application handles this: it proceeds as soon as there are usable candidates
|
||||
and only keeps waiting while there are none, up to 25 seconds. Host candidates
|
||||
alone are often enough on a local network. If nothing at all is gathered, the
|
||||
message names the likely causes, which in practice are a VPN binding the browser
|
||||
to an interface that cannot reach the servers, or a firewall dropping UDP.
|
||||
|
||||
Three failed local attempts disconnect the session.
|
||||
## File transfer policy
|
||||
|
||||
## File-transfer policy
|
||||
Incoming transfers are validated before the consent prompt and require explicit
|
||||
approval.
|
||||
|
||||
Incoming file requests are validated before the consent prompt and require explicit user approval.
|
||||
| Category | Extensions | Size limit |
|
||||
| --- | --- | --- |
|
||||
| Images | `.jpg` `.jpeg` `.png` `.gif` `.webp` `.bmp` `.ico` | 25 MB |
|
||||
| Documents | `.pdf` | 50 MB |
|
||||
| Text | `.txt` | 10 MB |
|
||||
| Archives | `.zip` | 100 MB |
|
||||
| Voice | `.webm` `.ogg` `.oga` `.opus` `.m4a` `.mp4` `.mp3` `.wav` | 20 MB |
|
||||
|
||||
Allowed categories:
|
||||
Overall ceiling is 100 MB per file.
|
||||
|
||||
- common raster images
|
||||
- PDF
|
||||
- plain text
|
||||
- ZIP archives
|
||||
Blocked outright: `.exe` `.bat` `.cmd` `.sh` `.js` `.msi` `.dmg` `.app` `.jar`
|
||||
`.scr` `.ps1` `.vbs` `.html` `.svg`
|
||||
|
||||
Blocked examples:
|
||||
The extension list is the security boundary. MIME type is treated as advisory,
|
||||
because it is client-supplied, varies between browsers and operating systems, and
|
||||
is frequently absent. An allowed extension is accepted when the MIME type is
|
||||
absent, generic, or one of the recognised types, but a clearly contradictory MIME
|
||||
type is rejected as a spoofing signal.
|
||||
|
||||
- `.exe`, `.bat`, `.cmd`, `.sh`, `.js`
|
||||
- `.msi`, `.dmg`, `.app`, `.jar`, `.scr`
|
||||
- `.ps1`, `.vbs`, `.html`, `.svg`
|
||||
Voice notes are the one transfer accepted without a prompt, so they are checked
|
||||
more strictly. The receiver decides, not the sender: a genuine audio MIME type,
|
||||
at most 4 MB, and a 64 MB budget for the whole session. A transfer that fails
|
||||
those checks is not rejected, it simply loses the shortcut and appears as a normal
|
||||
file with the usual prompt.
|
||||
|
||||
Both MIME type and extension must be acceptable. Missing or unknown MIME types are treated as unsafe unless explicitly covered by policy.
|
||||
## Deployment notes
|
||||
|
||||
The repository includes an nginx configuration (`deploy/nginx.conf`) and an
|
||||
Apache one (`.htaccess`). Both set the same policy, and the important parts are:
|
||||
|
||||
- `index.html`, `sw.js`, `manifest.json`, `meta.json` and `config/ice-servers.js`
|
||||
must not be cached. A stale `meta.json` breaks update notification, and a stale
|
||||
`sw.js` freezes the service worker.
|
||||
- `dist/` bundles are query-versioned, so `no-cache` with revalidation is enough
|
||||
and avoids re-downloading unchanged bundles.
|
||||
- `CDN-Cache-Control` is set separately, because a CDN reads it independently of
|
||||
the browser directive and will otherwise happily serve a stale app shell.
|
||||
- `frame-ancestors` and HSTS have to be sent as headers. The rest of the content
|
||||
security policy is a meta tag in `index.html`.
|
||||
- `.jsx` and `.mjs` must be served as JavaScript, or module loading fails.
|
||||
|
||||
Asset requests should return 404 when a file is missing rather than falling back
|
||||
to the HTML shell. A missing script served as HTML fails in a confusing way.
|
||||
|
||||
+71
-24
@@ -1,42 +1,89 @@
|
||||
# Contributing
|
||||
|
||||
## Development workflow
|
||||
## Workflow
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm test
|
||||
npm test # 41 suites, plain node:assert, no framework
|
||||
npm audit
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Security-sensitive areas
|
||||
Tests are individual `.mjs` files run in sequence by `npm test`. There is no test
|
||||
runner and no mocking library. A new suite is a new file, added to the `test`
|
||||
script in `package.json`.
|
||||
|
||||
Changes involving any of the following require extra review and focused tests:
|
||||
## Areas that need extra care
|
||||
|
||||
- SAS verification and protocol compatibility
|
||||
- WebRTC ICE/TURN behavior
|
||||
- encrypted payload validation or display sanitization
|
||||
- file-transfer consent and type policy
|
||||
- IndexedDB migration logic
|
||||
- disconnect and resource lifecycle cleanup
|
||||
Changes touching any of these should come with tests that would fail without the
|
||||
change:
|
||||
|
||||
## Documentation expectations
|
||||
- Verification: the safety code, the gate on control frames, protocol
|
||||
compatibility
|
||||
- The Double Ratchet: key derivation, chain advance, skipped-key bounds, the
|
||||
order in which state is committed
|
||||
- The inbound message path: anything that decides what reaches the interface
|
||||
- ICE and TURN behaviour, and the connection recovery cycle
|
||||
- File transfer consent and type policy
|
||||
- IndexedDB migration
|
||||
- Disconnect and resource cleanup
|
||||
|
||||
When behavior changes, update the corresponding release-facing documentation in the same change:
|
||||
## Writing tests that are worth having
|
||||
|
||||
- `README.md`
|
||||
- `SECURITY.md`
|
||||
- `doc/CONFIGURATION.md`
|
||||
- `doc/CRYPTOGRAPHY.md`
|
||||
- `doc/SECURITY-ARCHITECTURE.md`
|
||||
- `CHANGELOG.md`
|
||||
Two bugs reached production during recent work, and both had the same cause: the
|
||||
test built its own input instead of using what the application actually produces.
|
||||
|
||||
One test generated its own ECDH key pairs with usages the real generator did not
|
||||
grant, and passed against a build that could not establish a session at all.
|
||||
Another passed a locally generated public key where the application always
|
||||
supplies an imported, non-extractable one, and missed a failure that disabled
|
||||
forward secrecy for one side of every conversation. Locally generated public keys
|
||||
are always extractable in WebCrypto regardless of the flag you pass, so that
|
||||
difference is invisible unless you look for it.
|
||||
|
||||
The lesson is worth stating plainly: use the real factory functions, and where a
|
||||
value crosses a boundary in the application, make the test cross the same
|
||||
boundary. A test that constructs its inputs verifies the algorithm. Only a test
|
||||
that uses the shipped path verifies the code.
|
||||
|
||||
Before relying on a new test, confirm it fails when the fix is removed. A test
|
||||
that cannot fail is worse than no test, because it is read as coverage.
|
||||
|
||||
## Documentation
|
||||
|
||||
When behaviour changes, update the documentation in the same commit:
|
||||
|
||||
| Change | Documents |
|
||||
| --- | --- |
|
||||
| Anything user-visible | `README.md`, `CHANGELOG.md` |
|
||||
| Verification, keys, the ratchet | `doc/CRYPTOGRAPHY.md`, `doc/ARCHITECTURE.md` |
|
||||
| Deployment, ICE, file policy | `doc/CONFIGURATION.md` |
|
||||
| Calls, codecs, adaptation | `doc/CALLS.md` |
|
||||
| Internal interfaces | `doc/API.md` |
|
||||
| Anything security relevant | `SECURITY.md` |
|
||||
|
||||
Values in the documentation (limits, timeouts, algorithm parameters) are taken
|
||||
from the source. If you change one in code, change it in the documentation too,
|
||||
otherwise the next person will trust the wrong number.
|
||||
|
||||
## Release notes and security fixes
|
||||
|
||||
Release notes describe what improved. They do not spell out how a weakness could
|
||||
have been exploited, and neither do source comments. Users who have not updated
|
||||
are the ones exposed by that detail, and with no server there is no way to update
|
||||
everyone at once.
|
||||
|
||||
Comments explaining why a guard exists are valuable and should stay, because they
|
||||
are what stops the guard being removed later. The distinction is between "this
|
||||
check exists because completing the handshake does not prove identity" and a
|
||||
reproduction recipe.
|
||||
|
||||
## Pull requests
|
||||
|
||||
Please include:
|
||||
Include:
|
||||
|
||||
- concise problem statement
|
||||
- implementation summary
|
||||
- tests run
|
||||
- regression risks
|
||||
- screenshots or logs for user-visible changes when relevant
|
||||
- what the problem is
|
||||
- what the change does
|
||||
- which tests you ran, and which new ones you added
|
||||
- what could regress
|
||||
- for user-visible changes, a screenshot or a log
|
||||
|
||||
+181
-42
@@ -1,79 +1,218 @@
|
||||
# Cryptography and Verification
|
||||
# Cryptography
|
||||
|
||||
## Release context
|
||||
Everything here runs in the browser on the Web Crypto API. There are no
|
||||
hand-rolled primitives. What is written by hand is the composition: the key
|
||||
schedule, the ratchet, the verification flow and the framing, and that is what
|
||||
this document describes.
|
||||
|
||||
- Product release: `v5.7.1`
|
||||
- Protocol version: `4.1`
|
||||
- Ratchet wire version: `1`
|
||||
| | |
|
||||
| --- | --- |
|
||||
| Release | v5.7.2 |
|
||||
| Protocol version | 4.1 |
|
||||
| Ratchet wire version | 1 |
|
||||
|
||||
## Primitives
|
||||
|
||||
| Purpose | Algorithm |
|
||||
| --- | --- |
|
||||
| Key agreement | ECDH P-384, falling back to P-256 if the browser refuses P-384 |
|
||||
| Signatures | ECDSA P-384 with SHA-384, falling back to P-256 with SHA-256 |
|
||||
| Key derivation | HKDF-SHA256 |
|
||||
| Message encryption | AES-256-GCM |
|
||||
| Message authentication | HMAC-SHA256, and AES-GCM's own tag on the ratcheted path |
|
||||
| Password derivation | PBKDF2-SHA256, 310,000 iterations, 32-byte salt |
|
||||
|
||||
Session keys are non-extractable `CryptoKey` handles. The exceptions are the
|
||||
values a ratchet has to chain itself, which Web Crypto cannot do behind an opaque
|
||||
handle; those are raw bytes and are overwritten when finished with.
|
||||
|
||||
## Session establishment
|
||||
|
||||
SecureBit.chat uses ECDH-derived session material, DTLS-protected WebRTC transport, and a mandatory Short Authentication String (SAS) verification step.
|
||||
A session begins with one ECDH exchange. The public keys travel inside signed
|
||||
packages, and the receiving side validates the SPKI structure (algorithm OID,
|
||||
curve, point format and length) before importing anything.
|
||||
|
||||
The SAS is deterministic for both peers in the same authenticated session: it is derived with HKDF from the ECDH-derived key fingerprint together with both peers' DTLS fingerprints, canonicalised so each side computes the same value. Users compare the displayed code through an out-of-band channel and enter the matching code manually. Local success alone is insufficient: the session becomes verified only after both peers confirm.
|
||||
From the shared secret, HKDF-SHA256 derives five independent values, each under
|
||||
its own `info` label so that recovering one reveals nothing about the others:
|
||||
|
||||
Verification is the gate for the session, not a label on it. Until both peers have confirmed, the connection does not act on control messages from the other side — reconnection signalling, call setup, message deletion and delivery receipts all wait. The verification exchange itself is the deliberate exception, since it necessarily runs first.
|
||||
|
||||
## Key schedule
|
||||
|
||||
A single ECDH exchange produces the session's root material. From it, HKDF-SHA256 derives four independent keys plus the ratchet root, each under its own `info` label so that compromise of one reveals nothing about the others:
|
||||
|
||||
| Derived key | Purpose |
|
||||
| Label | Use |
|
||||
| --- | --- |
|
||||
| `message-encryption-v4` | AES-256-GCM payload key (static path) |
|
||||
| `message-encryption-v4` | AES-256-GCM payload key on the static path |
|
||||
| `message-authentication-v4` | HMAC-SHA256 message authentication |
|
||||
| `metadata-protection-v4` | AES-256-GCM for message metadata |
|
||||
| `fingerprint-generation-v4` | Key fingerprint shown to the user and fed to the SAS |
|
||||
| `fingerprint-generation-v4` | Key fingerprint shown to the user and fed into the safety code |
|
||||
| `double-ratchet-root-v1` | Root key for the Double Ratchet |
|
||||
|
||||
The raw ECDH output is derived with `deriveBits`, used as HKDF input material, and the buffer holding it is overwritten as soon as the derivation completes. Session keys themselves are non-extractable `CryptoKey` handles.
|
||||
The raw ECDH output is produced with `deriveBits`, used as HKDF input material,
|
||||
and the buffer holding it is overwritten as soon as derivation completes. It is
|
||||
never exported through an extractable key.
|
||||
|
||||
## Forward secrecy — the Double Ratchet
|
||||
The 64-byte session salt is generated by the inviting peer and travels in the
|
||||
invitation, so both sides derive the same schedule.
|
||||
|
||||
Message protection does not rest on the keys agreed during the handshake. On top of them the client runs the Double Ratchet (Signal's design), implemented in `src/crypto/DoubleRatchet.js`.
|
||||
## Verification
|
||||
|
||||
**Symmetric ratchet.** Each message key is derived from a chain key with `KDF_CK` (HMAC-SHA256 over the chain key with distinct constants for the message key and the next chain key), then discarded after a single use. The construction is one-way, so possession of the current chain key does not yield any earlier message key.
|
||||
Both peers compute the same safety code with HKDF, from the ECDH-derived key
|
||||
fingerprint together with both DTLS fingerprints. The fingerprints are
|
||||
canonicalised and sorted so that each side reaches the same value regardless of
|
||||
role.
|
||||
|
||||
**DH ratchet.** Each time the conversation changes direction, the replying peer introduces a fresh ECDH key pair and both sides mix a new shared secret into the root key with `KDF_RK` (HKDF-SHA256, root key as salt). A session therefore re-keys continuously as messages go back and forth.
|
||||
Users compare the code through a channel an attacker cannot impersonate and enter
|
||||
it manually. Local success is not sufficient: the session becomes verified only
|
||||
after both peers confirm. Three incorrect entries end the session.
|
||||
|
||||
**Initialisation.** No additional handshake data is exchanged. Both peers already hold each other's authenticated ECDH public key — the same keys the SAS covers — so the inviting peer begins with a fresh ratchet key against the peer's handshake key, and the joining peer begins with its own handshake key pair. The first DH step converges on the same secret from both directions.
|
||||
This is the step that makes the rest meaningful. Completing the key exchange
|
||||
proves only that someone completed it; anyone able to rewrite the invitation can
|
||||
do that with both people at once. The safety code covers the keys actually in
|
||||
use, so a substitution changes the code the users read to each other.
|
||||
|
||||
The joining peer has no sending chain until the inviting peer's first message arrives; this is inherent to the ratchet, since both sides derive it from the same exchange. Frames sent before that point use the session keys.
|
||||
Verification is also a gate rather than a label. Before it completes, the session
|
||||
declines to act on control messages from the peer: reconnection signalling, call
|
||||
setup, message deletion and delivery receipts. The verification exchange itself is
|
||||
the deliberate exception, since it necessarily runs first.
|
||||
|
||||
**Message framing.** Each ratcheted frame carries a header — the sender's current ratchet public key, the length of the previous sending chain and the message number in the current one. The header is transmitted in the clear, because the receiver needs it before it can derive a key, and is passed to AES-GCM as additional authenticated data. Any modification to it causes decryption to fail rather than redirecting the ratchet.
|
||||
## Forward secrecy
|
||||
|
||||
**Out-of-order messages.** Keys for messages that have not yet arrived are retained so they can still be read, within fixed bounds:
|
||||
The session keys above would last the whole conversation on their own. The Double
|
||||
Ratchet, implemented in `src/crypto/DoubleRatchet.js`, replaces them for message
|
||||
traffic so that protection does not rest on a single set of keys.
|
||||
|
||||
### Symmetric ratchet
|
||||
|
||||
Each message key comes from the current chain key through `KDF_CK`, which is
|
||||
HMAC-SHA256 over the chain key with one constant for the message key and another
|
||||
for the next chain key. The message key is used once and destroyed. Because the
|
||||
construction is one-way, holding the current chain key yields no earlier message
|
||||
key.
|
||||
|
||||
### DH ratchet
|
||||
|
||||
Each time the conversation changes direction, the replying peer generates a fresh
|
||||
ECDH key pair, and both sides mix the new shared secret into the root key with
|
||||
`KDF_RK` (HKDF-SHA256, root key as salt, producing the next root and a new chain
|
||||
key). A session therefore re-keys continuously as messages go back and forth, and
|
||||
an attacker who captured the full state is excluded again after one message in
|
||||
each direction.
|
||||
|
||||
### Initialisation
|
||||
|
||||
No extra handshake data is exchanged. Both peers already hold each other's
|
||||
authenticated ECDH public key, which is exactly what the safety code covers.
|
||||
|
||||
The inviting peer starts with a fresh ratchet key pair against the peer's
|
||||
handshake key and steps the root once, so even its first message has left the
|
||||
handshake key behind. The joining peer keeps its handshake key pair as its
|
||||
current ratchet pair, which is what the inviting peer derived against, and takes
|
||||
no chain until the first message arrives.
|
||||
|
||||
That asymmetry is inherent to the ratchet, not an implementation shortcut: both
|
||||
sides must derive the first chain from the same exchange. The consequence is that
|
||||
the joining peer has no sending chain until it receives something. The
|
||||
application sends a presence update from both sides as soon as verification
|
||||
completes, so those first frames use the session keys, and everything after them
|
||||
is ratcheted.
|
||||
|
||||
### Frame format
|
||||
|
||||
A ratcheted message is `{ type: "ratchet_message", h, c }`, where `h` is a header
|
||||
string and `c` is the base64 body.
|
||||
|
||||
The header carries the sender's current ratchet public key, the length of the
|
||||
previous sending chain, and the message number in the current one. It travels in
|
||||
the clear because the receiver needs it before it can derive a key, and it is
|
||||
passed to AES-GCM as additional authenticated data. Modifying any field causes
|
||||
decryption to fail rather than redirecting the ratchet.
|
||||
|
||||
The header must be handed back to the decrypt call exactly as received. It is the
|
||||
authenticated data itself, so re-serialising it can change a byte and fail
|
||||
authentication for no reason.
|
||||
|
||||
### Out-of-order messages
|
||||
|
||||
Keys for messages that have not yet arrived are retained so they can still be
|
||||
read, within fixed bounds:
|
||||
|
||||
| Bound | Value |
|
||||
| --- | --- |
|
||||
| Maximum skip within one chain | 512 |
|
||||
| Total retained keys | 1024 (oldest evicted first) |
|
||||
| Total retained keys | 1024, oldest evicted first |
|
||||
| Retention period | 5 minutes |
|
||||
|
||||
These are a resource control, not a tuning parameter: the message number is supplied by the peer, so the jump a single frame may claim has to be limited.
|
||||
The message number comes off the wire, so the distance a single frame may claim
|
||||
has to be limited. Without a cap, one frame claiming a number in the millions
|
||||
would force the receiver to derive and hold that many keys.
|
||||
|
||||
**State changes are applied only after authentication.** Receiving stages the chain advance and any DH step, attempts decryption, and commits only on success. A frame that fails authentication leaves the ratchet untouched, so a malformed or forged frame cannot desynchronise an established session.
|
||||
Replay protection is intrinsic here. A message key is destroyed on use, so a
|
||||
number behind the current chain has no key left to open it.
|
||||
|
||||
**Negotiation.** Support is advertised in the invitation and in the response, and the ratchet is used only when both sides advertise it. A peer on an earlier release negotiates it away and the session runs on the per-session keys described above. The security panel reports which of the two is in force for the current connection.
|
||||
### State is committed only after authentication
|
||||
|
||||
## Message protection
|
||||
Receiving stages the chain advance and any DH step, attempts decryption, and
|
||||
commits only on success. A frame that fails authentication leaves the ratchet
|
||||
exactly as it was.
|
||||
|
||||
- encrypted payloads are validated before decryption
|
||||
- chat content reaches the interface through one authenticated path only; unauthenticated frames are rejected rather than rendered
|
||||
- decrypted chat text is sanitized before entering React state or the UI
|
||||
- replay and ordering controls remain part of the session layer; on the ratcheted path replay protection is intrinsic, since a message key is destroyed on use
|
||||
- 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
|
||||
This matters because the header is reachable by anyone on the channel. Advancing
|
||||
the chains before verifying would let a single bad frame push the receiver past
|
||||
the sender and break the session permanently, which would be a remote denial of
|
||||
service against an established conversation.
|
||||
|
||||
## Local key metadata
|
||||
### Negotiation
|
||||
|
||||
Sensitive IndexedDB metadata is stored in encrypted envelopes. Legacy plaintext metadata remains readable through a migration path and is re-written in encrypted form when accessed. Corrupted encrypted metadata fails closed.
|
||||
Support is advertised in the invitation and in the response, and the ratchet runs
|
||||
only when both sides advertise it. A peer on an earlier release negotiates it
|
||||
away and the session uses the per-session keys described above.
|
||||
|
||||
The fallback is deliberate. With no server there is no way to update both ends at
|
||||
once, and a one-sided ratchet decrypts nothing. The security panel reports which
|
||||
of the two is in force for the current connection rather than what the client is
|
||||
capable of.
|
||||
|
||||
## Message protection on the static path
|
||||
|
||||
Messages encrypted with the session keys carry their metadata (identifier,
|
||||
timestamp, sequence number, original length) encrypted separately under the
|
||||
metadata key, and the whole payload is covered by an HMAC. Sequence numbers are
|
||||
checked against a sliding window: a number behind the expected one is rejected as
|
||||
a replay, and a gap beyond the window is rejected as well.
|
||||
|
||||
Payloads are padded to a 16-byte boundary with random bytes, with the true length
|
||||
carried in the encrypted metadata.
|
||||
|
||||
## Rendering
|
||||
|
||||
Decrypted text is sanitized with DOMPurify configured to allow no tags and no
|
||||
attributes at all, then rendered through React text nodes. Fenced code blocks are
|
||||
tokenised by Prism, which escapes its input before highlighting and never
|
||||
evaluates it. The content security policy permits no inline or remote scripts.
|
||||
|
||||
Chat content reaches the interface through one authenticated path. Frames that
|
||||
are not authenticated are rejected rather than displayed, so nothing can appear
|
||||
in a conversation that did not come from the peer holding the session keys.
|
||||
|
||||
## Local storage
|
||||
|
||||
Sensitive IndexedDB metadata is stored in encrypted envelopes. Legacy plaintext
|
||||
records remain readable through a migration path and are rewritten encrypted when
|
||||
next accessed. Corrupted encrypted metadata fails closed.
|
||||
|
||||
The master key for persistent storage is derived from a password with PBKDF2 and
|
||||
is non-extractable. The application supplies the password interface; there is no
|
||||
browser dialog fallback.
|
||||
|
||||
## Memory handling
|
||||
|
||||
Values that can be overwritten are overwritten: the ECDH output, HKDF intermediates, ratchet root and chain keys, and retained message keys are all zeroed when no longer needed. Values that cannot be overwritten in JavaScript — immutable strings, and non-extractable `CryptoKey` handles whose material lives outside the JS heap — are documented as such rather than reported as cleared; for those, non-extractability is the protection.
|
||||
Values that can be overwritten are overwritten: the ECDH output, HKDF
|
||||
intermediates, the ratchet root and chain keys, and retained message keys.
|
||||
|
||||
## Scope note
|
||||
Values that cannot be overwritten are documented rather than reported as cleared.
|
||||
JavaScript strings are immutable, so a secret held as a string can only be
|
||||
dereferenced. A non-extractable `CryptoKey` has no bytes visible to JavaScript at
|
||||
all, so dropping the handle is the only available action and non-extractability is
|
||||
what protects it. Functions that cannot wipe say so in their logs instead of
|
||||
reporting success, because a cleanup path that reports work it did not do is
|
||||
worse than one that reports nothing.
|
||||
|
||||
This document describes the current browser implementation behavior relevant to the v5.7.1 release. It does not replace independent cryptographic review.
|
||||
## Scope
|
||||
|
||||
This describes the browser implementation as it stands in v5.7.2. It is not a
|
||||
substitute for independent cryptographic review.
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Documentation
|
||||
|
||||
Technical documentation for SecureBit.chat. Start with the project [README](../README.md)
|
||||
if you are looking for an overview or a quick start.
|
||||
|
||||
| Document | What it covers |
|
||||
| --- | --- |
|
||||
| [ARCHITECTURE.md](ARCHITECTURE.md) | How a session is established, verified and torn down, and where each guarantee comes from |
|
||||
| [CRYPTOGRAPHY.md](CRYPTOGRAPHY.md) | Key schedule, the Double Ratchet, SAS verification, memory handling |
|
||||
| [CONFIGURATION.md](CONFIGURATION.md) | Deployment, ICE and TURN setup, privacy modes, file transfer policy |
|
||||
| [CALLS.md](CALLS.md) | Voice and video: codec choices, adaptation, and why each value was picked |
|
||||
| [API.md](API.md) | Internal interfaces of the WebRTC manager and file transfer system |
|
||||
| [CONTRIBUTING.md](CONTRIBUTING.md) | Development workflow and what needs extra review |
|
||||
| [USE-POLICY.md](USE-POLICY.md) | Terms of use, intended use, and the limits of what the software can protect |
|
||||
|
||||
Security policy and vulnerability reporting live in [SECURITY.md](../SECURITY.md) at
|
||||
the repository root, where GitHub expects to find them.
|
||||
|
||||
## Keeping this accurate
|
||||
|
||||
These documents describe the current release and are expected to change with it.
|
||||
Every value quoted here (limits, timeouts, algorithm parameters) is taken from the
|
||||
source rather than restated from memory, so if you change one in code, change it
|
||||
here in the same commit. [CONTRIBUTING.md](CONTRIBUTING.md) lists which documents
|
||||
are affected by which areas of the code.
|
||||
@@ -1,99 +0,0 @@
|
||||
# Security Architecture
|
||||
|
||||
## Current baseline
|
||||
|
||||
| Area | Current behavior |
|
||||
| --- | --- |
|
||||
| Protocol | `4.1` with mismatch rejection |
|
||||
| Peer verification | mandatory manual SAS entry; control messages gated on it |
|
||||
| Forward secrecy | Double Ratchet (wire version `1`), negotiated per session |
|
||||
| Transport | WebRTC over DTLS |
|
||||
| Privacy mode | optional TURN relay-only mode |
|
||||
| Message UI safety | one authenticated inbound path; decrypted text sanitized before display |
|
||||
| File transfer | validated metadata, explicit consent, allowlist policy |
|
||||
| Voice messages | same chunked AES-GCM transfer as files; auto-accepted within audio-type and size limits |
|
||||
| Local metadata | encrypted IndexedDB envelopes with migration |
|
||||
| Lifecycle | unified disconnect cleanup and bounded resource retention |
|
||||
|
||||
## Verification state machine
|
||||
|
||||
```text
|
||||
connection established
|
||||
↓
|
||||
shared keys derived
|
||||
↓
|
||||
deterministic SAS displayed
|
||||
↓
|
||||
manual out-of-band comparison
|
||||
↓
|
||||
local input validated
|
||||
↓
|
||||
peer confirmation received
|
||||
↓
|
||||
verified session
|
||||
```
|
||||
|
||||
The verified state is reached only when both local and remote confirmation flags are true, and it is set in a single place so the transition cannot be reached by another route.
|
||||
|
||||
Verification is enforced, not merely displayed. Before it completes, the session declines to act on control messages from the peer — reconnection signalling, call setup, message deletion and delivery receipts. Only the verification exchange itself and liveness probes run earlier, because they have to.
|
||||
|
||||
## Message protection layers
|
||||
|
||||
```text
|
||||
ECDH (P-384)
|
||||
↓
|
||||
HKDF key schedule
|
||||
┌─────────┬───────┬──────────┬─────────────┐
|
||||
↓ ↓ ↓ ↓ ↓
|
||||
message MAC metadata fingerprint ratchet root
|
||||
key key key (→ SAS) ↓
|
||||
Double Ratchet
|
||||
per-message keys
|
||||
```
|
||||
|
||||
Chat content is encrypted under a ratchet-derived key when both peers support it, and under the session message key otherwise. Either way it reaches the interface through one authenticated path; frames that fail authentication are dropped rather than displayed.
|
||||
|
||||
## Forward secrecy
|
||||
|
||||
Per-message keys are derived from a chain key by a one-way function and destroyed after use, and each change of direction introduces a fresh ECDH key pair that re-keys the session root. Out-of-order delivery is supported within fixed bounds (512 skipped keys per chain, 1024 retained in total, five-minute expiry), which limits how much state a peer can cause to be held. Incoming frames are authenticated before any ratchet state is committed, so a bad frame cannot desynchronise a live session.
|
||||
|
||||
See [`CRYPTOGRAPHY.md`](CRYPTOGRAPHY.md) for the key schedule and framing details.
|
||||
|
||||
## File-transfer architecture
|
||||
|
||||
1. sender emits metadata
|
||||
2. receiver validates name, size, type, and abuse limits
|
||||
3. receiver sees Accept / Reject prompt
|
||||
4. no receive buffers are allocated before acceptance
|
||||
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
|
||||
|
||||
The auto-accept decision belongs to the receiver, not the sender. A transfer
|
||||
qualifies only if it declares a recognised audio MIME type and stays within a
|
||||
4 MB per-note limit and a per-session budget; anything else is handled as an
|
||||
ordinary file and goes through the normal consent prompt. This keeps the
|
||||
convenience of voice notes from becoming an unattended transfer channel.
|
||||
|
||||
## Disconnect cleanup
|
||||
|
||||
The canonical disconnect path clears:
|
||||
|
||||
- WebRTC channels and peer connection handles
|
||||
- timers, deferred retries, fake traffic, and decoy traffic
|
||||
- pending transfer state and consent waits
|
||||
- verification state and crypto/PFS state
|
||||
- ratchet state: root key, both chain keys and every retained message key are
|
||||
overwritten, not merely dereferenced
|
||||
- React file-transfer callbacks and stale UI transfer state
|
||||
@@ -0,0 +1,86 @@
|
||||
# Use policy and limitations
|
||||
|
||||
SecureBit.chat is open-source software for private communication, research and
|
||||
education. It is provided as is, without warranty of any kind. This document sets
|
||||
out what the software can and cannot do for you, and what is expected of you when
|
||||
you use it.
|
||||
|
||||
## What it protects
|
||||
|
||||
Message content between two verified peers, against anyone observing or
|
||||
manipulating the network between them. That is a real guarantee and it is what
|
||||
the design is built around.
|
||||
|
||||
## What it does not protect
|
||||
|
||||
Be clear about these before relying on the software for anything that matters.
|
||||
|
||||
**A compromised device.** Malware, a hostile browser extension, a keylogger or
|
||||
someone with access to an unlocked machine sees your messages as you do. No
|
||||
transport encryption helps. This is the most common way private communication is
|
||||
actually broken.
|
||||
|
||||
**The person you are talking to.** They can screenshot, photograph the screen,
|
||||
copy the text, or simply repeat what you said. View-once and disappearing
|
||||
messages are cooperative features that a normal client honours; they are not a
|
||||
technical restriction on a determined recipient.
|
||||
|
||||
**Verification you skipped.** If you do not compare the safety code, or you
|
||||
compare it over a channel the attacker controls, the software cannot tell that
|
||||
someone is in the middle. The comparison must happen over something an attacker
|
||||
cannot impersonate: in person, or a voice you recognise.
|
||||
|
||||
**Metadata, depending on your setup.** A direct connection reveals your IP
|
||||
address to the peer. Relay-only mode with your own TURN server prevents that, but
|
||||
the relay operator can then see both addresses and the timing of traffic, though
|
||||
never the content. Choose according to who you are protecting against.
|
||||
|
||||
**The fact that you are using it.** Someone watching your network can see a
|
||||
WebRTC connection and can see you loading this application. The software does not
|
||||
hide its own use.
|
||||
|
||||
## Your responsibilities
|
||||
|
||||
- Comply with the law where you are and with any policies that apply to you
|
||||
- Keep your device and browser current and under your control
|
||||
- Compare the safety code out of band, every time, on every new session
|
||||
- Configure TURN correctly if you need relay-only mode, and verify it works
|
||||
- Understand that endpoint compromise defeats everything above the endpoint
|
||||
|
||||
## Intended use
|
||||
|
||||
The software is meant for legitimate private communication: journalism and source
|
||||
protection, human rights work, business confidentiality, medical and legal
|
||||
matters, research and education, and ordinary personal conversation that is
|
||||
nobody else's business.
|
||||
|
||||
It is not meant to facilitate unlawful activity, abuse, harassment, exploitation
|
||||
or harm, and being able to communicate privately does not make any of those
|
||||
acceptable.
|
||||
|
||||
If you become aware of the software being used to harm someone, report it to the
|
||||
appropriate authorities. Vulnerabilities in the software itself go to the
|
||||
maintainers first: see [SECURITY.md](../SECURITY.md).
|
||||
|
||||
## Operational notes
|
||||
|
||||
If your threat model is serious, the software is only one part of it.
|
||||
|
||||
Use a device you control and keep it patched. Consider a separate device for
|
||||
sensitive conversations. Be aware of who can see your screen and who can hear
|
||||
you. Understand that a camera and microphone are attached to the machine you are
|
||||
typing on. Consider what your network operator can observe, and whether a VPN or
|
||||
Tor changes that in your favour or simply moves the observation point.
|
||||
|
||||
Know the law where you are. Encryption is regulated differently in different
|
||||
places, and in some jurisdictions there are disclosure requirements attached to
|
||||
it.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome under the same expectations. Report vulnerabilities
|
||||
through the process in [SECURITY.md](../SECURITY.md) rather than publishing them,
|
||||
and give a fix reasonable time to reach users before disclosure. Users who have
|
||||
not updated yet are the ones exposed by early publication.
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow.
|
||||
@@ -1,143 +0,0 @@
|
||||
# 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?
|
||||
@@ -1,160 +0,0 @@
|
||||
# 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`).
|
||||
+22
-22
@@ -24,7 +24,7 @@
|
||||
|
||||
<!-- PWA Manifest -->
|
||||
<link rel="manifest" href="./manifest.json">
|
||||
<link rel="icon" type="image/x-icon" href="./logo/favicon.ico?v=1785985047695">
|
||||
<link rel="icon" type="image/x-icon" href="./logo/favicon.ico?v=1785988424571">
|
||||
|
||||
<!-- PWA Meta Tags -->
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
@@ -90,7 +90,7 @@
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/8.3__iPad_Mini_portrait.png">
|
||||
|
||||
<!-- Apple Touch Icons -->
|
||||
<link rel="apple-touch-icon" href="./logo/icon-180x180.png?v=1785985047695">
|
||||
<link rel="apple-touch-icon" href="./logo/icon-180x180.png?v=1785988424571">
|
||||
<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">
|
||||
@@ -99,7 +99,7 @@
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="./logo/icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="./logo/icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="./logo/icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="./logo/icon-180x180.png?v=1785985047695">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="./logo/icon-180x180.png?v=1785988424571">
|
||||
|
||||
<!-- Microsoft Tiles -->
|
||||
<meta name="msapplication-TileColor" content="#ff6b35">
|
||||
@@ -183,7 +183,7 @@
|
||||
<!-- 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=1785985047695"></script>
|
||||
<script defer src="config/ice-servers.js?v=1785988424571"></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 —
|
||||
@@ -191,8 +191,8 @@
|
||||
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=1785985047695">
|
||||
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1785985047695">
|
||||
<link rel="stylesheet" href="assets/tailwind.css?v=1785988424571">
|
||||
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1785988424571">
|
||||
<!-- 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. -->
|
||||
@@ -200,31 +200,31 @@
|
||||
<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?v=1785985047695">
|
||||
<link rel="stylesheet" href="src/styles/animations.css?v=1785985047695">
|
||||
<link rel="stylesheet" href="src/styles/components.css?v=1785985047695">
|
||||
<link rel="stylesheet" href="src/styles/main.css?v=1785988424571">
|
||||
<link rel="stylesheet" href="src/styles/animations.css?v=1785988424571">
|
||||
<link rel="stylesheet" href="src/styles/components.css?v=1785988424571">
|
||||
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
|
||||
<script defer src="src/scripts/load-async-css.js?v=1785985047695"></script>
|
||||
<script defer src="src/scripts/load-async-css.js?v=1785988424571"></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=1785985047695"></script>
|
||||
<script defer src="src/scripts/fa-check.js?v=1785988424571"></script>
|
||||
<!-- Update Manager - система принудительного обновления -->
|
||||
<script defer src="src/utils/updateManager.js?v=1785985047695"></script>
|
||||
<script type="module" src="src/components/UpdateChecker.jsx?v=1785985047695"></script>
|
||||
<script type="module" src="dist/qr-local.js?v=1785985047695"></script>
|
||||
<script type="module" src="src/components/QRScanner.js?v=1785985047695"></script>
|
||||
<script defer src="src/utils/updateManager.js?v=1785988424571"></script>
|
||||
<script type="module" src="src/components/UpdateChecker.jsx?v=1785988424571"></script>
|
||||
<script type="module" src="dist/qr-local.js?v=1785988424571"></script>
|
||||
<script type="module" src="src/components/QRScanner.js?v=1785988424571"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="dist/app-boot.js?v=1785985047695"></script>
|
||||
<script type="module" src="dist/app.js?v=1785985047695"></script>
|
||||
<script type="module" src="dist/app-boot.js?v=1785988424571"></script>
|
||||
<script type="module" src="dist/app.js?v=1785988424571"></script>
|
||||
|
||||
<script defer src="src/scripts/pwa-register.js?v=1785985047695"></script>
|
||||
<script src="./src/pwa/install-prompt.js?v=1785985047695" type="module"></script>
|
||||
<script src="./src/pwa/pwa-manager.js?v=1785985047695" type="module"></script>
|
||||
<script defer src="./src/scripts/pwa-offline-test.js?v=1785985047695"></script>
|
||||
<link rel="stylesheet" href="./src/styles/pwa.css?v=1785985047695">
|
||||
<script defer src="src/scripts/pwa-register.js?v=1785988424571"></script>
|
||||
<script src="./src/pwa/install-prompt.js?v=1785988424571" type="module"></script>
|
||||
<script src="./src/pwa/pwa-manager.js?v=1785988424571" type="module"></script>
|
||||
<script defer src="./src/scripts/pwa-offline-test.js?v=1785988424571"></script>
|
||||
<link rel="stylesheet" href="./src/styles/pwa.css?v=1785988424571">
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"version": "1785985047695",
|
||||
"buildVersion": "1785985047695",
|
||||
"appVersion": "5.7.1",
|
||||
"buildTime": "2026-08-06T02:57:27.737Z",
|
||||
"buildId": "1785985047695-2a7142c",
|
||||
"gitHash": "2a7142c",
|
||||
"version": "1785988424571",
|
||||
"buildVersion": "1785988424571",
|
||||
"appVersion": "5.7.2",
|
||||
"buildTime": "2026-08-06T03:53:44.611Z",
|
||||
"buildId": "1785988424571-27279ae",
|
||||
"gitHash": "27279ae",
|
||||
"generated": true,
|
||||
"generatedAt": "2026-08-06T02:57:27.739Z"
|
||||
"generatedAt": "2026-08-06T03:53:44.612Z"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "securebit-chat",
|
||||
"version": "5.7.1",
|
||||
"version": "5.7.2",
|
||||
"description": "Secure P2P Communication Application with End-to-End Encryption",
|
||||
"main": "index.html",
|
||||
"scripts": {
|
||||
@@ -11,7 +11,7 @@
|
||||
"dev": "npm run build && python -m http.server 8000",
|
||||
"watch": "npx tailwindcss -i src/styles/tw-input.css -o assets/tailwind.css --watch",
|
||||
"serve": "npx http-server -p 8000",
|
||||
"test": "node tests/sas-verification.test.mjs && node tests/verification-gate.test.mjs && node tests/inbound-frame-authentication.test.mjs && node tests/control-frame-authorization.test.mjs && node tests/security-level-shape.test.mjs && node tests/desktop-download-links.test.mjs && node tests/file-transfer-consent.test.mjs && node tests/incoming-message-sanitization.test.mjs && node tests/outgoing-message-integrity.test.mjs && node tests/secure-chat-features.test.mjs && node tests/notification-meta-forwarding.test.mjs && node tests/notification-ephemeral-privacy.test.mjs && node tests/key-derivation-compat.test.mjs && node tests/key-exchange-e2e.test.mjs && node tests/file-type-allowlist.test.mjs && node tests/voice-auto-accept.test.mjs && node tests/legacy-offer-purge.test.mjs && node tests/webrtc-privacy-mode.test.mjs && node tests/indexeddb-metadata-encryption.test.mjs && node tests/disconnect-cleanup.test.mjs && node tests/timer-lifecycle.test.mjs && node tests/file-transfer-cleanup.test.mjs && node tests/file-transfer-ui-cleanup.test.mjs && node tests/file-transfer-callback-propagation.test.mjs && node tests/debug-window-hooks.test.mjs && node tests/inbound-message-rate-limit.test.mjs && node tests/file-transfer-chunk-rate-limit.test.mjs && node tests/ice-servers-validation.test.mjs && node tests/sessions-reducer.test.mjs && node tests/webrtc-sdp.test.mjs && node tests/webrtc-video.test.mjs && node tests/webrtc-adaptation.test.mjs && node tests/session-recovery.test.mjs && node tests/qr-zip-bomb.test.mjs && node tests/ice-gathering-patience.test.mjs && node tests/double-ratchet.test.mjs && node tests/ratchet-integration.test.mjs"
|
||||
"test": "node tests/sas-verification.test.mjs && node tests/verification-gate.test.mjs && node tests/inbound-frame-authentication.test.mjs && node tests/control-frame-authorization.test.mjs && node tests/security-level-shape.test.mjs && node tests/desktop-download-links.test.mjs && node tests/file-transfer-consent.test.mjs && node tests/incoming-message-sanitization.test.mjs && node tests/outgoing-message-integrity.test.mjs && node tests/secure-chat-features.test.mjs && node tests/notification-meta-forwarding.test.mjs && node tests/notification-ephemeral-privacy.test.mjs && node tests/key-derivation-compat.test.mjs && node tests/key-exchange-e2e.test.mjs && node tests/file-type-allowlist.test.mjs && node tests/voice-auto-accept.test.mjs && node tests/legacy-offer-purge.test.mjs && node tests/webrtc-privacy-mode.test.mjs && node tests/indexeddb-metadata-encryption.test.mjs && node tests/disconnect-cleanup.test.mjs && node tests/timer-lifecycle.test.mjs && node tests/file-transfer-cleanup.test.mjs && node tests/file-transfer-ui-cleanup.test.mjs && node tests/file-transfer-callback-propagation.test.mjs && node tests/debug-window-hooks.test.mjs && node tests/inbound-message-rate-limit.test.mjs && node tests/file-transfer-chunk-rate-limit.test.mjs && node tests/ice-servers-validation.test.mjs && node tests/sessions-reducer.test.mjs && node tests/webrtc-sdp.test.mjs && node tests/webrtc-video.test.mjs && node tests/webrtc-adaptation.test.mjs && node tests/session-recovery.test.mjs && node tests/qr-zip-bomb.test.mjs && node tests/ice-gathering-patience.test.mjs && node tests/version-consistency.test.mjs && node tests/double-ratchet.test.mjs && node tests/ratchet-integration.test.mjs"
|
||||
},
|
||||
"keywords": [
|
||||
"p2p",
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
const EnhancedMinimalHeader = ({
|
||||
status,
|
||||
fingerprint,
|
||||
verificationCode,
|
||||
onDisconnect,
|
||||
isConnected,
|
||||
securityLevel,
|
||||
webrtcManager
|
||||
// The version shown in the header comes from package.json rather than a literal,
|
||||
// so a release cannot ship a header advertising the previous one. It was
|
||||
// hard-coded and drifted. A named import lets the bundler inline just this field
|
||||
// instead of embedding the whole manifest.
|
||||
import { version as packageVersion } from '../../../package.json';
|
||||
|
||||
const APP_VERSION = `v${packageVersion}`;
|
||||
|
||||
const EnhancedMinimalHeader = ({
|
||||
status,
|
||||
fingerprint,
|
||||
verificationCode,
|
||||
onDisconnect,
|
||||
isConnected,
|
||||
securityLevel,
|
||||
webrtcManager
|
||||
}) => {
|
||||
const [realSecurityLevel, setRealSecurityLevel] = React.useState(null);
|
||||
const [lastSecurityUpdate, setLastSecurityUpdate] = React.useState(0);
|
||||
@@ -559,7 +567,7 @@ const EnhancedMinimalHeader = ({
|
||||
React.createElement('div', { key: 'txt', style: { lineHeight: 1.2, minWidth: 0 } }, [
|
||||
React.createElement('div', { key: 'r1', style: { display: 'flex', alignItems: 'baseline', gap: '7px' } }, [
|
||||
React.createElement('span', { key: 'n', style: { fontSize: '16px', fontWeight: 800, letterSpacing: '-0.3px', color: '#e8e8eb' } }, 'SecureBit'),
|
||||
React.createElement('span', { key: 'v', style: { fontFamily: MONO, fontSize: '10px', fontWeight: 500, color: '#56565e' } }, 'v5.6.0')
|
||||
React.createElement('span', { key: 'v', style: { fontFamily: MONO, fontSize: '10px', fontWeight: 500, color: '#56565e' } }, APP_VERSION)
|
||||
]),
|
||||
React.createElement('div', { key: 'r2', className: 'hidden sm:block', style: { fontSize: '11px', color: '#6b6b73', fontWeight: 500 } }, 'End-to-end encrypted')
|
||||
])
|
||||
|
||||
@@ -11,7 +11,7 @@ let DYNAMIC_CACHE = 'securebit-pwa-dynamic-v4.7.56';
|
||||
// Build stamp — rewritten by scripts/post-build.js on every release so this file's
|
||||
// bytes change each deploy. That is what makes the browser detect a new Service Worker,
|
||||
// reinstall it, drop stale caches and (via controllerchange) prompt the page to update.
|
||||
const SW_BUILD_VERSION = '1785985047695';
|
||||
const SW_BUILD_VERSION = '1785988424571';
|
||||
|
||||
// Load version from meta.json on install
|
||||
async function getAppVersion() {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// The version is stated in several places that a release has to keep in step.
|
||||
// The one in the header was hard-coded and drifted: the application advertised
|
||||
// v5.6.0 while running 5.7.1. Anything a person has to remember at release time
|
||||
// eventually gets forgotten, so these assertions do the remembering.
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const read = (rel) => readFileSync(new URL(`../${rel}`, import.meta.url), 'utf8');
|
||||
|
||||
const pkg = JSON.parse(read('package.json'));
|
||||
const version = pkg.version;
|
||||
assert.match(version, /^\d+\.\d+\.\d+$/, 'package.json must carry a plain semver version');
|
||||
|
||||
// package.json is the single source of truth. The header must derive from it
|
||||
// rather than restate it, so no release step is needed to keep them together.
|
||||
{
|
||||
const header = read('src/components/ui/Header.jsx');
|
||||
assert.match(
|
||||
header,
|
||||
/import\s*\{\s*version[^}]*\}\s*from\s*['"][^'"]*package\.json['"]/,
|
||||
'the header must import the version from package.json'
|
||||
);
|
||||
assert.equal(
|
||||
/['"]v\d+\.\d+\.\d+['"]/.test(header),
|
||||
false,
|
||||
'the header must not contain a hard-coded version literal'
|
||||
);
|
||||
}
|
||||
|
||||
// meta.json drives the update check and is regenerated by the build. If it is
|
||||
// behind, the build was not re-run before committing and clients will not be
|
||||
// told an update exists.
|
||||
{
|
||||
const meta = JSON.parse(read('meta.json'));
|
||||
assert.equal(
|
||||
meta.appVersion, version,
|
||||
'meta.json is stale: run `npm run build` before committing a version bump'
|
||||
);
|
||||
}
|
||||
|
||||
// The README badge and the changelog are read by people deciding whether to
|
||||
// update, so a mismatch there is a mismatch in what we are telling them.
|
||||
{
|
||||
const readme = read('README.md');
|
||||
assert.ok(
|
||||
readme.includes(`version-${version}-`),
|
||||
`the README version badge must show ${version}`
|
||||
);
|
||||
|
||||
const changelog = read('CHANGELOG.md');
|
||||
const firstEntry = changelog.split('\n').find((line) => line.startsWith('## v'));
|
||||
assert.ok(
|
||||
firstEntry && firstEntry.startsWith(`## v${version}`),
|
||||
`the newest changelog entry must be v${version}, found: ${firstEntry}`
|
||||
);
|
||||
}
|
||||
|
||||
// Documentation quotes the release it describes.
|
||||
{
|
||||
const crypto = read('doc/CRYPTOGRAPHY.md');
|
||||
assert.ok(
|
||||
crypto.includes(version),
|
||||
`doc/CRYPTOGRAPHY.md must reference the current release (${version})`
|
||||
);
|
||||
}
|
||||
|
||||
console.log('version-consistency.test.mjs: all assertions passed');
|
||||
Reference in New Issue
Block a user