v6.7.3: faster loading, and pages search engines can read
The bundles carried all thirteen translations at once and a page fetched them a third time as raw source; each page now loads only its own language. Alongside that: JavaScript is minified, the eight stylesheets are served as one file, the QR scanner is fetched after the app is up instead of on every visit, Inter ships once rather than five copies of the same file, and Font Awesome is subset to the 82 icons this app draws instead of all 2468. 1.85 MB across 43 requests becomes under 700 KB across 33. On mobile the page starts drawing in 1.6 s instead of 6.3 s and is usable in 4.5 s instead of 11 s. Pages also carry their text in the HTML now. Everything was drawn by JavaScript into an empty div, so crawlers saw correct metadata around nothing, and twelve of the thirteen language pages had never been shown to anyone. The documentation is published under /docs/ with a new FAQ, and unknown addresses return a real 404. Separately: the localized shells were served with the year-long immutable cache header meant for static assets, which pinned anyone who opened /de/ or /ru/ to that build. The header is fixed and the service worker refreshes what it cached. Claude-Session: https://claude.ai/code/session_014KjzTXxrhzYoDDWChYQ4u2
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- Generated by scripts/build-docs.js from doc/*.md. Edits here are overwritten;
|
||||
change the Markdown instead. -->
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; upgrade-insecure-requests;">
|
||||
<meta http-equiv="X-Content-Type-Options" content="nosniff">
|
||||
<meta http-equiv="Referrer-Policy" content="strict-origin-when-cross-origin">
|
||||
<title>API Notes - SecureBit.chat</title>
|
||||
<meta name="description" content="API Notes — SecureBit.chat technical documentation.">
|
||||
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1">
|
||||
<link rel="canonical" href="https://securebit.chat/docs/api/">
|
||||
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico">
|
||||
<meta property="og:site_name" content="SecureBit.chat">
|
||||
<meta property="og:title" content="API Notes - SecureBit.chat">
|
||||
<meta property="og:description" content="API Notes — SecureBit.chat technical documentation.">
|
||||
<meta property="og:url" content="https://securebit.chat/docs/api/">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:locale" content="en_US">
|
||||
<meta property="og:image" content="https://securebit.chat/assets/social-card.png">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="API Notes - SecureBit.chat">
|
||||
<meta name="twitter:description" content="API Notes — SecureBit.chat technical documentation.">
|
||||
<meta name="twitter:image" content="https://securebit.chat/assets/social-card.png">
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "TechArticle",
|
||||
"headline": "API Notes",
|
||||
"description": "API Notes — SecureBit.chat technical documentation.",
|
||||
"url": "https://securebit.chat/docs/api/",
|
||||
"inLanguage": "en",
|
||||
"isPartOf": {
|
||||
"@type": "WebSite",
|
||||
"@id": "https://securebit.chat/#website"
|
||||
},
|
||||
"publisher": {
|
||||
"@id": "https://securebit.chat/#organization"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: #0f0f11;
|
||||
color: #d6d6dc;
|
||||
font-family: Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.68;
|
||||
}
|
||||
.wrap { max-width: 46rem; margin: 0 auto; padding: 28px 24px 90px; }
|
||||
.top {
|
||||
display: flex; flex-wrap: wrap; gap: 8px 18px; align-items: baseline;
|
||||
padding-bottom: 16px; margin-bottom: 40px;
|
||||
border-bottom: 1px solid rgba(255,255,255,.08);
|
||||
font-size: 13.5px;
|
||||
}
|
||||
.top a { color: #8a8a92; text-decoration: none; }
|
||||
.top a:hover, .top a:focus-visible { color: #f0892a; }
|
||||
.top .brand { color: #f0892a; font-weight: 700; letter-spacing: .04em; }
|
||||
.top .here { color: #d6d6dc; margin-inline-start: auto; }
|
||||
h1 { font-size: clamp(28px, 5vw, 36px); font-weight: 800; letter-spacing: -1px; line-height: 1.14; color: #f4f4f6; margin: 0 0 24px; }
|
||||
h2 { font-size: 22px; font-weight: 700; letter-spacing: -.4px; color: #f4f4f6; margin: 46px 0 12px; padding-top: 14px; border-top: 1px solid rgba(255,255,255,.07); }
|
||||
h3 { font-size: 17.5px; font-weight: 700; color: #e8e8eb; margin: 30px 0 8px; }
|
||||
h4 { font-size: 15.5px; font-weight: 700; color: #e8e8eb; margin: 22px 0 6px; }
|
||||
p, li { color: #a9a9b3; }
|
||||
p { margin: 0 0 16px; }
|
||||
ul, ol { padding-inline-start: 22px; margin: 0 0 16px; }
|
||||
li { margin: 5px 0; }
|
||||
a { color: #f0892a; text-underline-offset: 2px; }
|
||||
strong { color: #e8e8eb; }
|
||||
code { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: .88em; background: #17171c; border: 1px solid rgba(255,255,255,.07); border-radius: 4px; padding: 1px 5px; color: #e8e8eb; }
|
||||
pre { background: #0b0b0e; border: 1px solid rgba(255,255,255,.08); border-radius: 8px; padding: 14px 16px; overflow-x: auto; margin: 0 0 18px; }
|
||||
pre code { background: none; border: 0; padding: 0; font-size: 13px; line-height: 1.62; color: #c9c9d1; }
|
||||
.tablewrap { overflow-x: auto; margin: 0 0 20px; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 14.5px; min-width: 30rem; }
|
||||
th { text-align: start; color: #8a8a92; font-weight: 600; font-size: 12px; letter-spacing: .08em; text-transform: uppercase; padding: 0 14px 8px 0; border-bottom: 1px solid rgba(255,255,255,.12); }
|
||||
td { padding: 9px 14px 9px 0; border-bottom: 1px solid rgba(255,255,255,.06); vertical-align: top; color: #a9a9b3; }
|
||||
td:first-child, th:first-child { padding-inline-start: 0; }
|
||||
blockquote { margin: 0 0 18px; padding: 2px 0 2px 16px; border-inline-start: 3px solid rgba(240,137,42,.4); color: #8a8a92; }
|
||||
hr { border: 0; border-top: 1px solid rgba(255,255,255,.08); margin: 34px 0; }
|
||||
img { max-width: 100%; height: auto; }
|
||||
a:focus-visible { outline: 2px solid #f0892a; outline-offset: 2px; border-radius: 2px; }
|
||||
.more { margin-top: 64px; padding-top: 22px; border-top: 1px solid rgba(255,255,255,.08); }
|
||||
.more h2 { font-size: 13px; letter-spacing: .12em; text-transform: uppercase; color: #6b6b73; border: 0; margin: 0 0 12px; padding: 0; font-weight: 700; }
|
||||
.more ul { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: 4px 24px; }
|
||||
.more li { margin: 0; padding: 7px 0; border-bottom: 1px solid rgba(255,255,255,.05); font-size: 14.5px; }
|
||||
@media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<nav class="top">
|
||||
<a class="brand" href="/">SecureBit.chat</a>
|
||||
<a href="/docs/">Documentation</a>
|
||||
<a href="https://github.com/SecureBitChat/securebit-chat" rel="noopener">GitHub</a>
|
||||
<span class="here">API Notes</span>
|
||||
</nav>
|
||||
<h1>API Notes</h1>
|
||||
<h2 id="enhancedsecurewebrtcmanager">EnhancedSecureWebRTCManager</h2>
|
||||
<h3 id="verification">Verification</h3>
|
||||
<ul>
|
||||
<li><code>confirmVerification(userCode)</code> validates a manually entered SAS code.</li>
|
||||
<li>Verification succeeds only after both local and remote confirmations are present.</li>
|
||||
<li><code>isVerified</code> is assigned in one place (<code>_setVerifiedStatus</code>), which refuses any
|
||||
SAS-based transition without a recorded local confirmation.</li>
|
||||
<li>Control frames listed in <code>POST_VERIFICATION_CONTROL_TYPES</code> (reconnection
|
||||
signalling, call setup, message deletion, delivery receipts) are only acted on
|
||||
after verification. The set is an allowlist; unrecognised frame types are
|
||||
rejected by the chat channel's default-deny branch.</li>
|
||||
<li>Protocol version <code>4.1</code> is enforced during offer/answer processing.</li>
|
||||
</ul>
|
||||
<h3 id="forward-secrecy">Forward secrecy</h3>
|
||||
<ul>
|
||||
<li><code>isRatchetActive()</code> reports whether the Double Ratchet is running on this
|
||||
connection. It is negotiated: both peers advertise <code>RATCHET_VERSION</code> in the
|
||||
offer and answer, and a peer that does not falls back to per-session keys.</li>
|
||||
<li><code>_ratchet.canEncrypt</code> is false on the joining peer until the inviting peer's
|
||||
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.</li>
|
||||
<li><code>_ratchet.getState()</code> returns counters and the number of retained keys for
|
||||
diagnostics. It exposes no key material.</li>
|
||||
<li>Ratcheted chat arrives as <code>MESSAGE_TYPES.RATCHET_MESSAGE</code> with <code>h</code> (the header
|
||||
string, used verbatim as AES-GCM additional data) and <code>c</code> (base64 body). The
|
||||
header must be passed back to <code>decrypt()</code> exactly as received; re-serialising
|
||||
it can change a byte and fail authentication.</li>
|
||||
</ul>
|
||||
<h3 id="privacy-mode">Privacy mode</h3>
|
||||
<ul>
|
||||
<li>relay-only configuration sets WebRTC <code>iceTransportPolicy</code> to <code>"relay"</code>.</li>
|
||||
<li>TURN availability is checked before claiming IP protection.</li>
|
||||
</ul>
|
||||
<h3 id="file-transfer-callbacks">File transfer callbacks</h3>
|
||||
<ul>
|
||||
<li><code>setFileTransferCallbacks(onProgress, onReceived, onError, onIncomingRequest)</code> updates manager fields and any live <code>EnhancedSecureFileTransfer</code> instance.</li>
|
||||
<li>Passing <code>null</code> values detaches callbacks from the active transfer system.</li>
|
||||
</ul>
|
||||
<h3 id="voice-messages">Voice messages</h3>
|
||||
<ul>
|
||||
<li><code>sendFile(file, options)</code> accepts an optional <code>options</code> object. <code>options.voice</code>
|
||||
(<code>{ dur, bars }</code>) marks the transfer as a voice note and rides along as unsigned
|
||||
metadata; <code>options.uiId</code> correlates progress events to a UI bubble before the
|
||||
<code>fileId</code> resolves.</li>
|
||||
<li><code>onProgress</code> receives <code>{ fileId, uiId, direction, progress, isVoice, voice }</code>.
|
||||
<code>onIncomingFileRequest</code> and <code>onReceived</code> include <code>isVoice</code> and <code>voice</code> so the UI
|
||||
can auto-accept and render a voice bubble instead of a file card.</li>
|
||||
<li>The <code>isVoice</code> a callback receives is the <strong>receiver's</strong> verdict, not the
|
||||
sender's claim: <code>validateIncomingMetadata</code> 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
|
||||
loses the consent-free shortcut and is offered as a normal file.</li>
|
||||
</ul>
|
||||
<h2 id="enhancedsecurefiletransfer">EnhancedSecureFileTransfer</h2>
|
||||
<h3 id="incoming-transfers">Incoming transfers</h3>
|
||||
<ul>
|
||||
<li>metadata is validated before prompting</li>
|
||||
<li>acceptance is explicit</li>
|
||||
<li>receive buffers are allocated only after consent</li>
|
||||
<li>file type acceptance is allowlist-based</li>
|
||||
</ul>
|
||||
<h3 id="cleanup">Cleanup</h3>
|
||||
<ul>
|
||||
<li>pending sender consent promises are rejected on cleanup</li>
|
||||
<li>consent timeouts are cleared immediately</li>
|
||||
<li>retained received buffers are bounded</li>
|
||||
<li>evicted download handles fail with a user-facing availability message</li>
|
||||
</ul>
|
||||
<h2 id="securepersistentkeystorage">SecurePersistentKeyStorage</h2>
|
||||
<ul>
|
||||
<li>metadata is encrypted before storage</li>
|
||||
<li>legacy plaintext records migrate lazily</li>
|
||||
<li>corrupted encrypted metadata is ignored safely</li>
|
||||
</ul>
|
||||
|
||||
<nav class="more">
|
||||
<h2>More documentation</h2>
|
||||
<ul>
|
||||
<li><a href="/docs/">Documentation</a></li>
|
||||
<li><a href="/docs/faq/">Frequently asked questions</a></li>
|
||||
<li><a href="/docs/architecture/">Architecture</a></li>
|
||||
<li><a href="/docs/cryptography/">Cryptography</a></li>
|
||||
<li><a href="/docs/descriptor-sbq2/">SBQ2 — connection descriptor v2</a></li>
|
||||
<li><a href="/docs/configuration/">Configuration</a></li>
|
||||
<li><a href="/docs/calls/">Voice and video calls</a></li>
|
||||
<li><a href="/docs/contributing/">Contributing</a></li>
|
||||
<li><a href="/docs/use-policy/">Use policy and limitations</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,312 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- Generated by scripts/build-docs.js from doc/*.md. Edits here are overwritten;
|
||||
change the Markdown instead. -->
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; upgrade-insecure-requests;">
|
||||
<meta http-equiv="X-Content-Type-Options" content="nosniff">
|
||||
<meta http-equiv="Referrer-Policy" content="strict-origin-when-cross-origin">
|
||||
<title>Architecture - SecureBit.chat</title>
|
||||
<meta name="description" content="SecureBit.chat is a browser application with no backend. Two browsers negotiate a direct WebRTC connection, derive keys from an ECDH exchange, and confirm…">
|
||||
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1">
|
||||
<link rel="canonical" href="https://securebit.chat/docs/architecture/">
|
||||
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico">
|
||||
<meta property="og:site_name" content="SecureBit.chat">
|
||||
<meta property="og:title" content="Architecture - SecureBit.chat">
|
||||
<meta property="og:description" content="SecureBit.chat is a browser application with no backend. Two browsers negotiate a direct WebRTC connection, derive keys from an ECDH exchange, and confirm…">
|
||||
<meta property="og:url" content="https://securebit.chat/docs/architecture/">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:locale" content="en_US">
|
||||
<meta property="og:image" content="https://securebit.chat/assets/social-card.png">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Architecture - SecureBit.chat">
|
||||
<meta name="twitter:description" content="SecureBit.chat is a browser application with no backend. Two browsers negotiate a direct WebRTC connection, derive keys from an ECDH exchange, and confirm…">
|
||||
<meta name="twitter:image" content="https://securebit.chat/assets/social-card.png">
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "TechArticle",
|
||||
"headline": "Architecture",
|
||||
"description": "SecureBit.chat is a browser application with no backend. Two browsers negotiate a direct WebRTC connection, derive keys from an ECDH exchange, and confirm…",
|
||||
"url": "https://securebit.chat/docs/architecture/",
|
||||
"inLanguage": "en",
|
||||
"isPartOf": {
|
||||
"@type": "WebSite",
|
||||
"@id": "https://securebit.chat/#website"
|
||||
},
|
||||
"publisher": {
|
||||
"@id": "https://securebit.chat/#organization"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: #0f0f11;
|
||||
color: #d6d6dc;
|
||||
font-family: Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.68;
|
||||
}
|
||||
.wrap { max-width: 46rem; margin: 0 auto; padding: 28px 24px 90px; }
|
||||
.top {
|
||||
display: flex; flex-wrap: wrap; gap: 8px 18px; align-items: baseline;
|
||||
padding-bottom: 16px; margin-bottom: 40px;
|
||||
border-bottom: 1px solid rgba(255,255,255,.08);
|
||||
font-size: 13.5px;
|
||||
}
|
||||
.top a { color: #8a8a92; text-decoration: none; }
|
||||
.top a:hover, .top a:focus-visible { color: #f0892a; }
|
||||
.top .brand { color: #f0892a; font-weight: 700; letter-spacing: .04em; }
|
||||
.top .here { color: #d6d6dc; margin-inline-start: auto; }
|
||||
h1 { font-size: clamp(28px, 5vw, 36px); font-weight: 800; letter-spacing: -1px; line-height: 1.14; color: #f4f4f6; margin: 0 0 24px; }
|
||||
h2 { font-size: 22px; font-weight: 700; letter-spacing: -.4px; color: #f4f4f6; margin: 46px 0 12px; padding-top: 14px; border-top: 1px solid rgba(255,255,255,.07); }
|
||||
h3 { font-size: 17.5px; font-weight: 700; color: #e8e8eb; margin: 30px 0 8px; }
|
||||
h4 { font-size: 15.5px; font-weight: 700; color: #e8e8eb; margin: 22px 0 6px; }
|
||||
p, li { color: #a9a9b3; }
|
||||
p { margin: 0 0 16px; }
|
||||
ul, ol { padding-inline-start: 22px; margin: 0 0 16px; }
|
||||
li { margin: 5px 0; }
|
||||
a { color: #f0892a; text-underline-offset: 2px; }
|
||||
strong { color: #e8e8eb; }
|
||||
code { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: .88em; background: #17171c; border: 1px solid rgba(255,255,255,.07); border-radius: 4px; padding: 1px 5px; color: #e8e8eb; }
|
||||
pre { background: #0b0b0e; border: 1px solid rgba(255,255,255,.08); border-radius: 8px; padding: 14px 16px; overflow-x: auto; margin: 0 0 18px; }
|
||||
pre code { background: none; border: 0; padding: 0; font-size: 13px; line-height: 1.62; color: #c9c9d1; }
|
||||
.tablewrap { overflow-x: auto; margin: 0 0 20px; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 14.5px; min-width: 30rem; }
|
||||
th { text-align: start; color: #8a8a92; font-weight: 600; font-size: 12px; letter-spacing: .08em; text-transform: uppercase; padding: 0 14px 8px 0; border-bottom: 1px solid rgba(255,255,255,.12); }
|
||||
td { padding: 9px 14px 9px 0; border-bottom: 1px solid rgba(255,255,255,.06); vertical-align: top; color: #a9a9b3; }
|
||||
td:first-child, th:first-child { padding-inline-start: 0; }
|
||||
blockquote { margin: 0 0 18px; padding: 2px 0 2px 16px; border-inline-start: 3px solid rgba(240,137,42,.4); color: #8a8a92; }
|
||||
hr { border: 0; border-top: 1px solid rgba(255,255,255,.08); margin: 34px 0; }
|
||||
img { max-width: 100%; height: auto; }
|
||||
a:focus-visible { outline: 2px solid #f0892a; outline-offset: 2px; border-radius: 2px; }
|
||||
.more { margin-top: 64px; padding-top: 22px; border-top: 1px solid rgba(255,255,255,.08); }
|
||||
.more h2 { font-size: 13px; letter-spacing: .12em; text-transform: uppercase; color: #6b6b73; border: 0; margin: 0 0 12px; padding: 0; font-weight: 700; }
|
||||
.more ul { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: 4px 24px; }
|
||||
.more li { margin: 0; padding: 7px 0; border-bottom: 1px solid rgba(255,255,255,.05); font-size: 14.5px; }
|
||||
@media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<nav class="top">
|
||||
<a class="brand" href="/">SecureBit.chat</a>
|
||||
<a href="/docs/">Documentation</a>
|
||||
<a href="https://github.com/SecureBitChat/securebit-chat" rel="noopener">GitHub</a>
|
||||
<span class="here">Architecture</span>
|
||||
</nav>
|
||||
<h1>Architecture</h1>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<h2 id="session-lifecycle">Session lifecycle</h2>
|
||||
<pre><code class="language-text">1. Invitation Peer A generates its key pairs and an SDP offer, and exports a
|
||||
compact descriptor: ICE candidates, the DTLS certificate
|
||||
fingerprint, an expiry, and a 16-byte commitment to its key
|
||||
material. 110-150 bytes; one QR code. No keys travel in it.
|
||||
|
||||
2. Response Peer B validates the descriptor strictly, answers the SDP, and
|
||||
returns a descriptor of the same shape, tagged so that it can
|
||||
only be an answer to this particular invitation.
|
||||
|
||||
3. Transport up DTLS completes and the data channel opens. Only the peer
|
||||
holding the private key behind the fingerprint in the
|
||||
invitation can reach this point.
|
||||
|
||||
4. Key exchange Each side sends its public keys over the open channel as the
|
||||
first frame. Each verifies the other's blob against the
|
||||
commitment from the invitation BEFORE parsing it, then derives
|
||||
the session from a transcript of both descriptors and both
|
||||
blobs, and signs that transcript to prove it owns its identity
|
||||
key. Any failure closes the connection.
|
||||
|
||||
5. Verification Both sides display the same safety code, derived from that
|
||||
transcript. The users compare it over a channel an attacker
|
||||
cannot impersonate and enter it.
|
||||
|
||||
6. Verified Only now does the session accept traffic that changes state,
|
||||
and only now does the chat open.
|
||||
</code></pre>
|
||||
<p>Steps 3 and 4 are the ones worth dwelling on. The fingerprint in the invitation
|
||||
authenticates the transport to whoever showed you the code, and the commitment
|
||||
means substituted key material is refused automatically rather than noticed by a
|
||||
human. But neither proves <em>who</em> showed you the code. Anyone positioned on the
|
||||
out-of-band channel can rewrite the whole invitation, commitment included, and
|
||||
complete steps 3 and 4 with both people at once. Step 5 is the only step that
|
||||
distinguishes the intended peer, so everything that could be useful to an
|
||||
impostor waits for it — and because the safety code is computed over the full
|
||||
transcript, a rewritten handshake cannot produce matching digits.</p>
|
||||
<p>The invitation format and its decoder rules are in
|
||||
<a href="/docs/descriptor-sbq2/">DESCRIPTOR-SBQ2.md</a>.</p>
|
||||
<h2 id="what-verification-gates">What verification gates</h2>
|
||||
<p>Verification is enforced, not merely displayed. Until both sides confirm:</p>
|
||||
<ul>
|
||||
<li>reconnection signalling is refused</li>
|
||||
<li>call setup is refused</li>
|
||||
<li>message deletion and delivery receipts are refused</li>
|
||||
<li>incoming file transfers are refused</li>
|
||||
</ul>
|
||||
<p>The verification exchange itself and liveness probes run earlier, because they
|
||||
have to. That set is an allowlist in the code
|
||||
(<code>POST_VERIFICATION_CONTROL_TYPES</code>), and anything not on it is rejected by
|
||||
default rather than passed through.</p>
|
||||
<p>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.</p>
|
||||
<h2 id="message-protection-layers">Message protection layers</h2>
|
||||
<pre><code class="language-text"> ECDH P-384 exchange
|
||||
|
|
||||
HKDF key schedule
|
||||
|
|
||||
.........................................................
|
||||
| | | | |
|
||||
message MAC metadata fingerprint ratchet root
|
||||
key key key (safety code) |
|
||||
Double Ratchet
|
||||
per-message keys
|
||||
</code></pre>
|
||||
<p>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.</p>
|
||||
<h2 id="forward-secrecy">Forward secrecy</h2>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<p><a href="/docs/cryptography/">CRYPTOGRAPHY.md</a> has the key schedule and the frame format.</p>
|
||||
<h2 id="session-recovery">Session recovery</h2>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<h2 id="file-transfer">File transfer</h2>
|
||||
<ol>
|
||||
<li>The sender emits metadata.</li>
|
||||
<li>The receiver validates name, size, type and abuse limits.</li>
|
||||
<li>The receiver is shown an Accept or Reject prompt.</li>
|
||||
<li>No receive buffers are allocated before acceptance.</li>
|
||||
<li>Chunks are transmitted only after acceptance.</li>
|
||||
<li>Completed buffers are retained within a bounded window.</li>
|
||||
</ol>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<h2 id="disconnect">Disconnect</h2>
|
||||
<p>The disconnect path clears:</p>
|
||||
<ul>
|
||||
<li>WebRTC channels and peer connection handles</li>
|
||||
<li>timers, deferred retries, cover traffic and decoy traffic</li>
|
||||
<li>pending transfer state and consent waits</li>
|
||||
<li>verification state and session key material</li>
|
||||
<li>ratchet state: the root key, both chain keys and every retained message key are
|
||||
overwritten rather than only dereferenced</li>
|
||||
<li>React file transfer callbacks and stale interface state</li>
|
||||
</ul>
|
||||
<p>Values that cannot be overwritten in JavaScript are documented as such rather
|
||||
than reported as cleared. See the memory handling section of
|
||||
<a href="/docs/cryptography/">CRYPTOGRAPHY.md</a>.</p>
|
||||
<h2 id="multiple-conversations">Multiple conversations</h2>
|
||||
<p>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.</p>
|
||||
<h2 id="code-layout">Code layout</h2>
|
||||
<div class="tablewrap"><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Path</th>
|
||||
<th>Responsibility</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><code>src/network/EnhancedSecureWebRTCManager.js</code></td>
|
||||
<td>Connection lifecycle, verification, session state, message routing</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>src/network/webrtc/</code></td>
|
||||
<td>Call stack: SDP handling, audio and video senders, network adaptation</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>src/crypto/DoubleRatchet.js</code></td>
|
||||
<td>Forward secrecy: root and chain keys, DH ratchet, skipped keys</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>src/crypto/EnhancedSecureCryptoUtils.js</code></td>
|
||||
<td>Key generation, key schedule, message encryption, sanitization</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>src/crypto/cose-qr.js</code></td>
|
||||
<td>Invitation packing for QR transport</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>src/transfer/EnhancedSecureFileTransfer.js</code></td>
|
||||
<td>Chunked encrypted transfers, consent, type policy</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>src/state/sessionsStore.js</code></td>
|
||||
<td>Reducer for the set of open conversations</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>src/app.jsx</code></td>
|
||||
<td>Interface and message rendering</td>
|
||||
</tr>
|
||||
</tbody></table></div>
|
||||
|
||||
<nav class="more">
|
||||
<h2>More documentation</h2>
|
||||
<ul>
|
||||
<li><a href="/docs/">Documentation</a></li>
|
||||
<li><a href="/docs/faq/">Frequently asked questions</a></li>
|
||||
<li><a href="/docs/cryptography/">Cryptography</a></li>
|
||||
<li><a href="/docs/descriptor-sbq2/">SBQ2 — connection descriptor v2</a></li>
|
||||
<li><a href="/docs/configuration/">Configuration</a></li>
|
||||
<li><a href="/docs/calls/">Voice and video calls</a></li>
|
||||
<li><a href="/docs/api/">API Notes</a></li>
|
||||
<li><a href="/docs/contributing/">Contributing</a></li>
|
||||
<li><a href="/docs/use-policy/">Use policy and limitations</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,391 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- Generated by scripts/build-docs.js from doc/*.md. Edits here are overwritten;
|
||||
change the Markdown instead. -->
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; upgrade-insecure-requests;">
|
||||
<meta http-equiv="X-Content-Type-Options" content="nosniff">
|
||||
<meta http-equiv="Referrer-Policy" content="strict-origin-when-cross-origin">
|
||||
<title>Voice and video calls - SecureBit.chat</title>
|
||||
<meta name="description" content="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…">
|
||||
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1">
|
||||
<link rel="canonical" href="https://securebit.chat/docs/calls/">
|
||||
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico">
|
||||
<meta property="og:site_name" content="SecureBit.chat">
|
||||
<meta property="og:title" content="Voice and video calls - SecureBit.chat">
|
||||
<meta property="og:description" content="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…">
|
||||
<meta property="og:url" content="https://securebit.chat/docs/calls/">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:locale" content="en_US">
|
||||
<meta property="og:image" content="https://securebit.chat/assets/social-card.png">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Voice and video calls - SecureBit.chat">
|
||||
<meta name="twitter:description" content="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…">
|
||||
<meta name="twitter:image" content="https://securebit.chat/assets/social-card.png">
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "TechArticle",
|
||||
"headline": "Voice and video calls",
|
||||
"description": "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…",
|
||||
"url": "https://securebit.chat/docs/calls/",
|
||||
"inLanguage": "en",
|
||||
"isPartOf": {
|
||||
"@type": "WebSite",
|
||||
"@id": "https://securebit.chat/#website"
|
||||
},
|
||||
"publisher": {
|
||||
"@id": "https://securebit.chat/#organization"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: #0f0f11;
|
||||
color: #d6d6dc;
|
||||
font-family: Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.68;
|
||||
}
|
||||
.wrap { max-width: 46rem; margin: 0 auto; padding: 28px 24px 90px; }
|
||||
.top {
|
||||
display: flex; flex-wrap: wrap; gap: 8px 18px; align-items: baseline;
|
||||
padding-bottom: 16px; margin-bottom: 40px;
|
||||
border-bottom: 1px solid rgba(255,255,255,.08);
|
||||
font-size: 13.5px;
|
||||
}
|
||||
.top a { color: #8a8a92; text-decoration: none; }
|
||||
.top a:hover, .top a:focus-visible { color: #f0892a; }
|
||||
.top .brand { color: #f0892a; font-weight: 700; letter-spacing: .04em; }
|
||||
.top .here { color: #d6d6dc; margin-inline-start: auto; }
|
||||
h1 { font-size: clamp(28px, 5vw, 36px); font-weight: 800; letter-spacing: -1px; line-height: 1.14; color: #f4f4f6; margin: 0 0 24px; }
|
||||
h2 { font-size: 22px; font-weight: 700; letter-spacing: -.4px; color: #f4f4f6; margin: 46px 0 12px; padding-top: 14px; border-top: 1px solid rgba(255,255,255,.07); }
|
||||
h3 { font-size: 17.5px; font-weight: 700; color: #e8e8eb; margin: 30px 0 8px; }
|
||||
h4 { font-size: 15.5px; font-weight: 700; color: #e8e8eb; margin: 22px 0 6px; }
|
||||
p, li { color: #a9a9b3; }
|
||||
p { margin: 0 0 16px; }
|
||||
ul, ol { padding-inline-start: 22px; margin: 0 0 16px; }
|
||||
li { margin: 5px 0; }
|
||||
a { color: #f0892a; text-underline-offset: 2px; }
|
||||
strong { color: #e8e8eb; }
|
||||
code { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: .88em; background: #17171c; border: 1px solid rgba(255,255,255,.07); border-radius: 4px; padding: 1px 5px; color: #e8e8eb; }
|
||||
pre { background: #0b0b0e; border: 1px solid rgba(255,255,255,.08); border-radius: 8px; padding: 14px 16px; overflow-x: auto; margin: 0 0 18px; }
|
||||
pre code { background: none; border: 0; padding: 0; font-size: 13px; line-height: 1.62; color: #c9c9d1; }
|
||||
.tablewrap { overflow-x: auto; margin: 0 0 20px; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 14.5px; min-width: 30rem; }
|
||||
th { text-align: start; color: #8a8a92; font-weight: 600; font-size: 12px; letter-spacing: .08em; text-transform: uppercase; padding: 0 14px 8px 0; border-bottom: 1px solid rgba(255,255,255,.12); }
|
||||
td { padding: 9px 14px 9px 0; border-bottom: 1px solid rgba(255,255,255,.06); vertical-align: top; color: #a9a9b3; }
|
||||
td:first-child, th:first-child { padding-inline-start: 0; }
|
||||
blockquote { margin: 0 0 18px; padding: 2px 0 2px 16px; border-inline-start: 3px solid rgba(240,137,42,.4); color: #8a8a92; }
|
||||
hr { border: 0; border-top: 1px solid rgba(255,255,255,.08); margin: 34px 0; }
|
||||
img { max-width: 100%; height: auto; }
|
||||
a:focus-visible { outline: 2px solid #f0892a; outline-offset: 2px; border-radius: 2px; }
|
||||
.more { margin-top: 64px; padding-top: 22px; border-top: 1px solid rgba(255,255,255,.08); }
|
||||
.more h2 { font-size: 13px; letter-spacing: .12em; text-transform: uppercase; color: #6b6b73; border: 0; margin: 0 0 12px; padding: 0; font-weight: 700; }
|
||||
.more ul { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: 4px 24px; }
|
||||
.more li { margin: 0; padding: 7px 0; border-bottom: 1px solid rgba(255,255,255,.05); font-size: 14.5px; }
|
||||
@media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<nav class="top">
|
||||
<a class="brand" href="/">SecureBit.chat</a>
|
||||
<a href="/docs/">Documentation</a>
|
||||
<a href="https://github.com/SecureBitChat/securebit-chat" rel="noopener">GitHub</a>
|
||||
<span class="here">Voice and video calls</span>
|
||||
</nav>
|
||||
<h1>Voice and video calls</h1>
|
||||
<p>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.</p>
|
||||
<p>All tunable values live in <code>src/network/webrtc/config.js</code>. This document explains
|
||||
where they come from, because the numbers are otherwise indistinguishable from
|
||||
arbitrary choices.</p>
|
||||
<h2 id="where-the-settings-attach">Where the settings attach</h2>
|
||||
<p>A single <code>RTCRtpSender</code> 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:</p>
|
||||
<div class="tablewrap"><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Concern</th>
|
||||
<th>Surface</th>
|
||||
<th>When</th>
|
||||
<th>Implementation</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td>Codec ordering (RED before Opus)</td>
|
||||
<td><code>transceiver.setCodecPreferences</code></td>
|
||||
<td>before creating the offer or answer</td>
|
||||
<td><code>applyAudioCodecPreferences</code> in <code>audio.js</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Opus FEC, DTX, bitrate</td>
|
||||
<td>SDP <code>a=fmtp</code> rewriting</td>
|
||||
<td>after create, before <code>setLocalDescription</code></td>
|
||||
<td><code>applyOpusSettings</code> in <code>sdp.js</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Priority and maximum bitrate</td>
|
||||
<td><code>sender.setParameters</code></td>
|
||||
<td>after <code>setLocalDescription</code></td>
|
||||
<td><code>configureAudioSender</code> in <code>audio.js</code></td>
|
||||
</tr>
|
||||
</tbody></table></div>
|
||||
<p>Both peers apply the same rewriting, so the negotiated session carries the
|
||||
parameters regardless of who called.</p>
|
||||
<h2 id="audio">Audio</h2>
|
||||
<p>Speech has to stay intelligible on a bad link, and that goal drives every value
|
||||
below.</p>
|
||||
<h3 id="opus-parameters">Opus parameters</h3>
|
||||
<div class="tablewrap"><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Parameter</th>
|
||||
<th>Value</th>
|
||||
<th>Reason</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><code>minptime</code></td>
|
||||
<td>10</td>
|
||||
<td>Smaller packetisation interval, lower latency (RFC 7587, section 7)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>useinbandfec</code></td>
|
||||
<td>1</td>
|
||||
<td>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)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>usedtx</code></td>
|
||||
<td>1</td>
|
||||
<td>Discontinuous transmission stops sending during silence, leaving the transport free for video and FEC (RFC 7587, section 3.1.3)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>stereo</code></td>
|
||||
<td>0</td>
|
||||
<td>Mono halves the bitrate with no loss for speech</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>maxaveragebitrate</code></td>
|
||||
<td>32000</td>
|
||||
<td>Comfortable wideband speech</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>cbr</code></td>
|
||||
<td>0</td>
|
||||
<td>Variable bitrate spends bits only when needed</td>
|
||||
</tr>
|
||||
</tbody></table></div>
|
||||
<h3 id="redundancy">Redundancy</h3>
|
||||
<p>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 <code>audio/red</code> in <code>RTCRtpSender.getCapabilities('audio')</code>, which
|
||||
Chromium does and others vary on; when absent it is skipped silently. RED is
|
||||
ordered before Opus in codec preferences.</p>
|
||||
<h3 id="sender-parameters">Sender parameters</h3>
|
||||
<div class="tablewrap"><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Parameter</th>
|
||||
<th>Value</th>
|
||||
<th>Reason</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><code>maxBitrate</code></td>
|
||||
<td>40000 bps</td>
|
||||
<td>Headroom above the 32 kbps Opus target for RED redundancy</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>priority</code></td>
|
||||
<td><code>high</code></td>
|
||||
<td>Audio wins bandwidth arbitration against video within the connection</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>networkPriority</code></td>
|
||||
<td><code>high</code></td>
|
||||
<td>DSCP hint so audio is prioritised on the wire</td>
|
||||
</tr>
|
||||
</tbody></table></div>
|
||||
<p>Audio is never throttled by the adaptation controller. Under sustained loss the
|
||||
video degrades and speech continues.</p>
|
||||
<h2 id="video">Video</h2>
|
||||
<p>Codec preference order is VP9, AV1, H.264, VP8, applied through
|
||||
<code>setCodecPreferences</code>. 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.</p>
|
||||
<h3 id="scalable-coding-rather-than-simulcast">Scalable coding rather than simulcast</h3>
|
||||
<p>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 <code>sender.setParameters</code> and needs no <code>addTransceiver</code> or rid
|
||||
configuration, which keeps it away from the media path that actually works.</p>
|
||||
<div class="tablewrap"><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Codec</th>
|
||||
<th>scalabilityMode</th>
|
||||
<th>maxBitrate</th>
|
||||
<th>degradationPreference</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td>VP9</td>
|
||||
<td><code>L3T3_KEY</code> (3 spatial, 3 temporal, key-aligned)</td>
|
||||
<td>1.5 Mbps</td>
|
||||
<td><code>balanced</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>AV1</td>
|
||||
<td><code>L1T3</code></td>
|
||||
<td>1.2 Mbps</td>
|
||||
<td><code>maintain-framerate</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>H.264, VP8</td>
|
||||
<td>none</td>
|
||||
<td>1.5 Mbps</td>
|
||||
<td><code>balanced</code></td>
|
||||
</tr>
|
||||
</tbody></table></div>
|
||||
<p><code>networkPriority</code> is <code>medium</code>, below audio. If a browser rejects the scalability
|
||||
mode, which Firefox and Safari do in places, <code>configureVideoSender</code> retries with
|
||||
a plain encoding.</p>
|
||||
<h3 id="why-media-is-attached-with-addtrack">Why media is attached with addTrack</h3>
|
||||
<p>An explicit <code>addTransceiver({ sendEncodings })</code> 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 <code>setRemoteDescription</code> rejected the SVC
|
||||
parameters outright. On repeat and role-reversed calls, the reused transceiver
|
||||
directions desynchronised: the call connected and <code>ontrack</code> fired, but no media
|
||||
flowed.</p>
|
||||
<p>What ships instead attaches media with <code>addTrack</code>, reused across calls through
|
||||
<code>replaceTrack</code>, and lets the browser manage transceiver direction. This is what
|
||||
keeps audio and video flowing across reversed and repeated calls.</p>
|
||||
<p>Multi-rid simulcast primitives (<code>buildVideoSendEncodings</code>) are kept and tested for
|
||||
a future group-call path, but are not wired into the one-to-one flow. They need
|
||||
<code>addTransceiver({ sendEncodings })</code>, 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.</p>
|
||||
<h2 id="transport-feedback">Transport feedback</h2>
|
||||
<p>The call m-lines need RTCP feedback and header extensions present. Most browsers
|
||||
emit them already, so this is an idempotent safety net:</p>
|
||||
<div class="tablewrap"><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Media</th>
|
||||
<th>RTCP feedback</th>
|
||||
<th>Header extension</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td>Video</td>
|
||||
<td><code>transport-cc</code>, <code>nack</code>, <code>nack pli</code>, <code>ccm fir</code>, <code>goog-remb</code></td>
|
||||
<td>transport-wide congestion control</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Audio</td>
|
||||
<td><code>transport-cc</code>, <code>nack</code></td>
|
||||
<td>transport-wide congestion control</td>
|
||||
</tr>
|
||||
</tbody></table></div>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<h2 id="adaptation">Adaptation</h2>
|
||||
<p><code>NetworkAdaptationController</code> samples <code>pc.getStats()</code> every second and reacts:</p>
|
||||
<div class="tablewrap"><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Condition</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td>Loss above 10 percent, or round trip above 300 ms</td>
|
||||
<td>Reduce video <code>maxBitrate</code> by 20 percent, floor 100 kbps</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Loss below 3 percent and round trip below 150 ms, sustained 5 samples</td>
|
||||
<td>Raise video <code>maxBitrate</code> by 10 percent, up to the ceiling</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>qualityLimitationReason</code> is <code>cpu</code></td>
|
||||
<td>Scale resolution down by 1.5, bitrate unchanged</td>
|
||||
</tr>
|
||||
</tbody></table></div>
|
||||
<p>Every change goes through <code>sender.setParameters</code>. There is no renegotiation and
|
||||
no track restart, so adaptation is invisible to the call. Audio is never touched.</p>
|
||||
<p>The decision function and the stats parsing are pure and unit-tested against
|
||||
recorded <code>getStats</code> output.</p>
|
||||
<h3 id="quality-indicator">Quality indicator</h3>
|
||||
<p>The same sample produces a coarse label shown in the call interface:</p>
|
||||
<div class="tablewrap"><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Label</th>
|
||||
<th>Condition</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td>Excellent</td>
|
||||
<td>Loss below 3 percent and round trip below 150 ms</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Good</td>
|
||||
<td>Loss below 7 percent and round trip below 250 ms</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Fair</td>
|
||||
<td>Loss below 15 percent and round trip below 400 ms</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Weak</td>
|
||||
<td>Anything else</td>
|
||||
</tr>
|
||||
</tbody></table></div>
|
||||
<p>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.</p>
|
||||
<h2 id="verifying-a-change">Verifying a change</h2>
|
||||
<p>Unit tests cover SDP rewriting, video codec selection and the adaptation
|
||||
decision:</p>
|
||||
<pre><code class="language-bash">npm test
|
||||
</code></pre>
|
||||
<p>For anything touching media in practice, open <code>chrome://webrtc-internals</code> during
|
||||
a call and check that:</p>
|
||||
<ul>
|
||||
<li>outbound audio shows Opus with the fmtp parameters above, appearing as <code>red</code>
|
||||
and <code>opus</code> on Chromium</li>
|
||||
<li>throttling the link steps outbound video <code>targetBitrate</code> down within a second
|
||||
or two and recovers when the link clears</li>
|
||||
<li>audio bitrate holds steady while video adapts</li>
|
||||
<li>the in-call indicator moves through Fair and Weak as the link degrades</li>
|
||||
</ul>
|
||||
|
||||
<nav class="more">
|
||||
<h2>More documentation</h2>
|
||||
<ul>
|
||||
<li><a href="/docs/">Documentation</a></li>
|
||||
<li><a href="/docs/faq/">Frequently asked questions</a></li>
|
||||
<li><a href="/docs/architecture/">Architecture</a></li>
|
||||
<li><a href="/docs/cryptography/">Cryptography</a></li>
|
||||
<li><a href="/docs/descriptor-sbq2/">SBQ2 — connection descriptor v2</a></li>
|
||||
<li><a href="/docs/configuration/">Configuration</a></li>
|
||||
<li><a href="/docs/api/">API Notes</a></li>
|
||||
<li><a href="/docs/contributing/">Contributing</a></li>
|
||||
<li><a href="/docs/use-policy/">Use policy and limitations</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,283 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- Generated by scripts/build-docs.js from doc/*.md. Edits here are overwritten;
|
||||
change the Markdown instead. -->
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; upgrade-insecure-requests;">
|
||||
<meta http-equiv="X-Content-Type-Options" content="nosniff">
|
||||
<meta http-equiv="Referrer-Policy" content="strict-origin-when-cross-origin">
|
||||
<title>Configuration - SecureBit.chat</title>
|
||||
<meta name="description" content="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…">
|
||||
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1">
|
||||
<link rel="canonical" href="https://securebit.chat/docs/configuration/">
|
||||
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico">
|
||||
<meta property="og:site_name" content="SecureBit.chat">
|
||||
<meta property="og:title" content="Configuration - SecureBit.chat">
|
||||
<meta property="og:description" content="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…">
|
||||
<meta property="og:url" content="https://securebit.chat/docs/configuration/">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:locale" content="en_US">
|
||||
<meta property="og:image" content="https://securebit.chat/assets/social-card.png">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Configuration - SecureBit.chat">
|
||||
<meta name="twitter:description" content="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…">
|
||||
<meta name="twitter:image" content="https://securebit.chat/assets/social-card.png">
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "TechArticle",
|
||||
"headline": "Configuration",
|
||||
"description": "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…",
|
||||
"url": "https://securebit.chat/docs/configuration/",
|
||||
"inLanguage": "en",
|
||||
"isPartOf": {
|
||||
"@type": "WebSite",
|
||||
"@id": "https://securebit.chat/#website"
|
||||
},
|
||||
"publisher": {
|
||||
"@id": "https://securebit.chat/#organization"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: #0f0f11;
|
||||
color: #d6d6dc;
|
||||
font-family: Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.68;
|
||||
}
|
||||
.wrap { max-width: 46rem; margin: 0 auto; padding: 28px 24px 90px; }
|
||||
.top {
|
||||
display: flex; flex-wrap: wrap; gap: 8px 18px; align-items: baseline;
|
||||
padding-bottom: 16px; margin-bottom: 40px;
|
||||
border-bottom: 1px solid rgba(255,255,255,.08);
|
||||
font-size: 13.5px;
|
||||
}
|
||||
.top a { color: #8a8a92; text-decoration: none; }
|
||||
.top a:hover, .top a:focus-visible { color: #f0892a; }
|
||||
.top .brand { color: #f0892a; font-weight: 700; letter-spacing: .04em; }
|
||||
.top .here { color: #d6d6dc; margin-inline-start: auto; }
|
||||
h1 { font-size: clamp(28px, 5vw, 36px); font-weight: 800; letter-spacing: -1px; line-height: 1.14; color: #f4f4f6; margin: 0 0 24px; }
|
||||
h2 { font-size: 22px; font-weight: 700; letter-spacing: -.4px; color: #f4f4f6; margin: 46px 0 12px; padding-top: 14px; border-top: 1px solid rgba(255,255,255,.07); }
|
||||
h3 { font-size: 17.5px; font-weight: 700; color: #e8e8eb; margin: 30px 0 8px; }
|
||||
h4 { font-size: 15.5px; font-weight: 700; color: #e8e8eb; margin: 22px 0 6px; }
|
||||
p, li { color: #a9a9b3; }
|
||||
p { margin: 0 0 16px; }
|
||||
ul, ol { padding-inline-start: 22px; margin: 0 0 16px; }
|
||||
li { margin: 5px 0; }
|
||||
a { color: #f0892a; text-underline-offset: 2px; }
|
||||
strong { color: #e8e8eb; }
|
||||
code { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: .88em; background: #17171c; border: 1px solid rgba(255,255,255,.07); border-radius: 4px; padding: 1px 5px; color: #e8e8eb; }
|
||||
pre { background: #0b0b0e; border: 1px solid rgba(255,255,255,.08); border-radius: 8px; padding: 14px 16px; overflow-x: auto; margin: 0 0 18px; }
|
||||
pre code { background: none; border: 0; padding: 0; font-size: 13px; line-height: 1.62; color: #c9c9d1; }
|
||||
.tablewrap { overflow-x: auto; margin: 0 0 20px; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 14.5px; min-width: 30rem; }
|
||||
th { text-align: start; color: #8a8a92; font-weight: 600; font-size: 12px; letter-spacing: .08em; text-transform: uppercase; padding: 0 14px 8px 0; border-bottom: 1px solid rgba(255,255,255,.12); }
|
||||
td { padding: 9px 14px 9px 0; border-bottom: 1px solid rgba(255,255,255,.06); vertical-align: top; color: #a9a9b3; }
|
||||
td:first-child, th:first-child { padding-inline-start: 0; }
|
||||
blockquote { margin: 0 0 18px; padding: 2px 0 2px 16px; border-inline-start: 3px solid rgba(240,137,42,.4); color: #8a8a92; }
|
||||
hr { border: 0; border-top: 1px solid rgba(255,255,255,.08); margin: 34px 0; }
|
||||
img { max-width: 100%; height: auto; }
|
||||
a:focus-visible { outline: 2px solid #f0892a; outline-offset: 2px; border-radius: 2px; }
|
||||
.more { margin-top: 64px; padding-top: 22px; border-top: 1px solid rgba(255,255,255,.08); }
|
||||
.more h2 { font-size: 13px; letter-spacing: .12em; text-transform: uppercase; color: #6b6b73; border: 0; margin: 0 0 12px; padding: 0; font-weight: 700; }
|
||||
.more ul { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: 4px 24px; }
|
||||
.more li { margin: 0; padding: 7px 0; border-bottom: 1px solid rgba(255,255,255,.05); font-size: 14.5px; }
|
||||
@media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<nav class="top">
|
||||
<a class="brand" href="/">SecureBit.chat</a>
|
||||
<a href="/docs/">Documentation</a>
|
||||
<a href="https://github.com/SecureBitChat/securebit-chat" rel="noopener">GitHub</a>
|
||||
<span class="here">Configuration</span>
|
||||
</nav>
|
||||
<h1>Configuration</h1>
|
||||
<h2 id="requirements">Requirements</h2>
|
||||
<ul>
|
||||
<li>A browser with WebRTC and Web Crypto support</li>
|
||||
<li>Node.js 18 or later, for building</li>
|
||||
<li>A TURN service, if you need relay-only privacy mode or you expect users behind
|
||||
strict NAT</li>
|
||||
</ul>
|
||||
<h2 id="building-and-running">Building and running</h2>
|
||||
<pre><code class="language-bash">npm install
|
||||
npm run build
|
||||
npm run serve
|
||||
</code></pre>
|
||||
<p><code>npm run build</code> compiles the CSS, bundles the JavaScript into <code>dist/</code>, and
|
||||
regenerates <code>meta.json</code> with a build stamp. That stamp is what the update
|
||||
mechanism compares, and it is also written into <code>sw.js</code> so the browser reinstalls
|
||||
the service worker on each release. A deployment that skips <code>post-build</code> will not
|
||||
notify anyone that an update exists.</p>
|
||||
<p>The application is static. Any web server can host it, and there is no backend to
|
||||
run.</p>
|
||||
<h2 id="ice-and-turn">ICE and TURN</h2>
|
||||
<p>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.</p>
|
||||
<p>Configuration comes from three places, in order of precedence:</p>
|
||||
<ol>
|
||||
<li>User settings under Advanced network settings, stored in IndexedDB per device</li>
|
||||
<li><code>config/ice-servers.js</code>, an operator override loaded before the application</li>
|
||||
<li>Built-in public STUN defaults</li>
|
||||
</ol>
|
||||
<p><code>config/ice-servers.js</code> is not committed, because it is where operator TURN
|
||||
credentials would go. Use <code>config/ice-servers.example.js</code> as the template. The
|
||||
Docker image copies <code>config/ice-servers.prod.js</code> into place at build time.</p>
|
||||
<pre><code class="language-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: '...'
|
||||
}
|
||||
];
|
||||
</code></pre>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<h3 id="user-supplied-servers">User-supplied servers</h3>
|
||||
<p>Users can paste their own STUN and TURN servers. Input is validated against an
|
||||
allowlist before it reaches <code>RTCPeerConnection</code>: only the <code>stun</code>, <code>stuns</code>, <code>turn</code>
|
||||
and <code>turns</code> schemes, a hostname or bracketed IPv6 address with an optional port,
|
||||
and an optional <code>transport=udp</code> or <code>transport=tcp</code> query. At most 10 servers with
|
||||
8 URLs each. Anything else is rejected with a specific reason.</p>
|
||||
<h3 id="privacy-modes">Privacy modes</h3>
|
||||
<div class="tablewrap"><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Mode</th>
|
||||
<th>Behaviour</th>
|
||||
<th>IP exposure</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td>Default</td>
|
||||
<td>Standard candidate gathering</td>
|
||||
<td>Direct candidates can reveal addresses to the peer</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Relay-only</td>
|
||||
<td>Sets <code>iceTransportPolicy: "relay"</code></td>
|
||||
<td>Requires TURN; peers see only the relay</td>
|
||||
</tr>
|
||||
</tbody></table></div>
|
||||
<p>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.</p>
|
||||
<p>Validate a TURN deployment with <code>chrome://webrtc-internals</code> before relying on it.
|
||||
A relay candidate should appear in the gathered set; if none does, the credentials
|
||||
or the ports are wrong.</p>
|
||||
<h3 id="when-connections-fail">When connections fail</h3>
|
||||
<p>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 <code>701</code> errors for each unreachable server.</p>
|
||||
<p>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.</p>
|
||||
<h2 id="file-transfer-policy">File transfer policy</h2>
|
||||
<p>Incoming transfers are validated before the consent prompt and require explicit
|
||||
approval.</p>
|
||||
<div class="tablewrap"><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Category</th>
|
||||
<th>Extensions</th>
|
||||
<th>Size limit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td>Images</td>
|
||||
<td><code>.jpg</code> <code>.jpeg</code> <code>.png</code> <code>.gif</code> <code>.webp</code> <code>.bmp</code> <code>.ico</code></td>
|
||||
<td>25 MB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Documents</td>
|
||||
<td><code>.pdf</code></td>
|
||||
<td>50 MB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Text</td>
|
||||
<td><code>.txt</code></td>
|
||||
<td>10 MB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Archives</td>
|
||||
<td><code>.zip</code></td>
|
||||
<td>100 MB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Voice</td>
|
||||
<td><code>.webm</code> <code>.ogg</code> <code>.oga</code> <code>.opus</code> <code>.m4a</code> <code>.mp4</code> <code>.mp3</code> <code>.wav</code></td>
|
||||
<td>20 MB</td>
|
||||
</tr>
|
||||
</tbody></table></div>
|
||||
<p>Overall ceiling is 100 MB per file.</p>
|
||||
<p>Blocked outright: <code>.exe</code> <code>.bat</code> <code>.cmd</code> <code>.sh</code> <code>.js</code> <code>.msi</code> <code>.dmg</code> <code>.app</code> <code>.jar</code>
|
||||
<code>.scr</code> <code>.ps1</code> <code>.vbs</code> <code>.html</code> <code>.svg</code></p>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<h2 id="deployment-notes">Deployment notes</h2>
|
||||
<p>The repository includes an nginx configuration (<code>deploy/nginx.conf</code>) and an
|
||||
Apache one (<code>.htaccess</code>). Both set the same policy, and the important parts are:</p>
|
||||
<ul>
|
||||
<li><code>index.html</code>, <code>sw.js</code>, <code>manifest.json</code>, <code>meta.json</code> and <code>config/ice-servers.js</code>
|
||||
must not be cached. A stale <code>meta.json</code> breaks update notification, and a stale
|
||||
<code>sw.js</code> freezes the service worker.</li>
|
||||
<li><code>dist/</code> bundles are query-versioned, so <code>no-cache</code> with revalidation is enough
|
||||
and avoids re-downloading unchanged bundles.</li>
|
||||
<li><code>CDN-Cache-Control</code> is set separately, because a CDN reads it independently of
|
||||
the browser directive and will otherwise happily serve a stale app shell.</li>
|
||||
<li><code>frame-ancestors</code> and HSTS have to be sent as headers. The rest of the content
|
||||
security policy is a meta tag in <code>index.html</code>.</li>
|
||||
<li><code>.jsx</code> and <code>.mjs</code> must be served as JavaScript, or module loading fails.</li>
|
||||
</ul>
|
||||
<p>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.</p>
|
||||
|
||||
<nav class="more">
|
||||
<h2>More documentation</h2>
|
||||
<ul>
|
||||
<li><a href="/docs/">Documentation</a></li>
|
||||
<li><a href="/docs/faq/">Frequently asked questions</a></li>
|
||||
<li><a href="/docs/architecture/">Architecture</a></li>
|
||||
<li><a href="/docs/cryptography/">Cryptography</a></li>
|
||||
<li><a href="/docs/descriptor-sbq2/">SBQ2 — connection descriptor v2</a></li>
|
||||
<li><a href="/docs/calls/">Voice and video calls</a></li>
|
||||
<li><a href="/docs/api/">API Notes</a></li>
|
||||
<li><a href="/docs/contributing/">Contributing</a></li>
|
||||
<li><a href="/docs/use-policy/">Use policy and limitations</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,219 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- Generated by scripts/build-docs.js from doc/*.md. Edits here are overwritten;
|
||||
change the Markdown instead. -->
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; upgrade-insecure-requests;">
|
||||
<meta http-equiv="X-Content-Type-Options" content="nosniff">
|
||||
<meta http-equiv="Referrer-Policy" content="strict-origin-when-cross-origin">
|
||||
<title>Contributing - SecureBit.chat</title>
|
||||
<meta name="description" content="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…">
|
||||
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1">
|
||||
<link rel="canonical" href="https://securebit.chat/docs/contributing/">
|
||||
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico">
|
||||
<meta property="og:site_name" content="SecureBit.chat">
|
||||
<meta property="og:title" content="Contributing - SecureBit.chat">
|
||||
<meta property="og:description" content="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…">
|
||||
<meta property="og:url" content="https://securebit.chat/docs/contributing/">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:locale" content="en_US">
|
||||
<meta property="og:image" content="https://securebit.chat/assets/social-card.png">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Contributing - SecureBit.chat">
|
||||
<meta name="twitter:description" content="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…">
|
||||
<meta name="twitter:image" content="https://securebit.chat/assets/social-card.png">
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "TechArticle",
|
||||
"headline": "Contributing",
|
||||
"description": "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…",
|
||||
"url": "https://securebit.chat/docs/contributing/",
|
||||
"inLanguage": "en",
|
||||
"isPartOf": {
|
||||
"@type": "WebSite",
|
||||
"@id": "https://securebit.chat/#website"
|
||||
},
|
||||
"publisher": {
|
||||
"@id": "https://securebit.chat/#organization"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: #0f0f11;
|
||||
color: #d6d6dc;
|
||||
font-family: Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.68;
|
||||
}
|
||||
.wrap { max-width: 46rem; margin: 0 auto; padding: 28px 24px 90px; }
|
||||
.top {
|
||||
display: flex; flex-wrap: wrap; gap: 8px 18px; align-items: baseline;
|
||||
padding-bottom: 16px; margin-bottom: 40px;
|
||||
border-bottom: 1px solid rgba(255,255,255,.08);
|
||||
font-size: 13.5px;
|
||||
}
|
||||
.top a { color: #8a8a92; text-decoration: none; }
|
||||
.top a:hover, .top a:focus-visible { color: #f0892a; }
|
||||
.top .brand { color: #f0892a; font-weight: 700; letter-spacing: .04em; }
|
||||
.top .here { color: #d6d6dc; margin-inline-start: auto; }
|
||||
h1 { font-size: clamp(28px, 5vw, 36px); font-weight: 800; letter-spacing: -1px; line-height: 1.14; color: #f4f4f6; margin: 0 0 24px; }
|
||||
h2 { font-size: 22px; font-weight: 700; letter-spacing: -.4px; color: #f4f4f6; margin: 46px 0 12px; padding-top: 14px; border-top: 1px solid rgba(255,255,255,.07); }
|
||||
h3 { font-size: 17.5px; font-weight: 700; color: #e8e8eb; margin: 30px 0 8px; }
|
||||
h4 { font-size: 15.5px; font-weight: 700; color: #e8e8eb; margin: 22px 0 6px; }
|
||||
p, li { color: #a9a9b3; }
|
||||
p { margin: 0 0 16px; }
|
||||
ul, ol { padding-inline-start: 22px; margin: 0 0 16px; }
|
||||
li { margin: 5px 0; }
|
||||
a { color: #f0892a; text-underline-offset: 2px; }
|
||||
strong { color: #e8e8eb; }
|
||||
code { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: .88em; background: #17171c; border: 1px solid rgba(255,255,255,.07); border-radius: 4px; padding: 1px 5px; color: #e8e8eb; }
|
||||
pre { background: #0b0b0e; border: 1px solid rgba(255,255,255,.08); border-radius: 8px; padding: 14px 16px; overflow-x: auto; margin: 0 0 18px; }
|
||||
pre code { background: none; border: 0; padding: 0; font-size: 13px; line-height: 1.62; color: #c9c9d1; }
|
||||
.tablewrap { overflow-x: auto; margin: 0 0 20px; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 14.5px; min-width: 30rem; }
|
||||
th { text-align: start; color: #8a8a92; font-weight: 600; font-size: 12px; letter-spacing: .08em; text-transform: uppercase; padding: 0 14px 8px 0; border-bottom: 1px solid rgba(255,255,255,.12); }
|
||||
td { padding: 9px 14px 9px 0; border-bottom: 1px solid rgba(255,255,255,.06); vertical-align: top; color: #a9a9b3; }
|
||||
td:first-child, th:first-child { padding-inline-start: 0; }
|
||||
blockquote { margin: 0 0 18px; padding: 2px 0 2px 16px; border-inline-start: 3px solid rgba(240,137,42,.4); color: #8a8a92; }
|
||||
hr { border: 0; border-top: 1px solid rgba(255,255,255,.08); margin: 34px 0; }
|
||||
img { max-width: 100%; height: auto; }
|
||||
a:focus-visible { outline: 2px solid #f0892a; outline-offset: 2px; border-radius: 2px; }
|
||||
.more { margin-top: 64px; padding-top: 22px; border-top: 1px solid rgba(255,255,255,.08); }
|
||||
.more h2 { font-size: 13px; letter-spacing: .12em; text-transform: uppercase; color: #6b6b73; border: 0; margin: 0 0 12px; padding: 0; font-weight: 700; }
|
||||
.more ul { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: 4px 24px; }
|
||||
.more li { margin: 0; padding: 7px 0; border-bottom: 1px solid rgba(255,255,255,.05); font-size: 14.5px; }
|
||||
@media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<nav class="top">
|
||||
<a class="brand" href="/">SecureBit.chat</a>
|
||||
<a href="/docs/">Documentation</a>
|
||||
<a href="https://github.com/SecureBitChat/securebit-chat" rel="noopener">GitHub</a>
|
||||
<span class="here">Contributing</span>
|
||||
</nav>
|
||||
<h1>Contributing</h1>
|
||||
<h2 id="workflow">Workflow</h2>
|
||||
<pre><code class="language-bash">npm install
|
||||
npm test # 41 suites, plain node:assert, no framework
|
||||
npm audit
|
||||
npm run build
|
||||
</code></pre>
|
||||
<p>Tests are individual <code>.mjs</code> files run in sequence by <code>npm test</code>. There is no test
|
||||
runner and no mocking library. A new suite is a new file, added to the <code>test</code>
|
||||
script in <code>package.json</code>.</p>
|
||||
<h2 id="areas-that-need-extra-care">Areas that need extra care</h2>
|
||||
<p>Changes touching any of these should come with tests that would fail without the
|
||||
change:</p>
|
||||
<ul>
|
||||
<li>Verification: the safety code, the gate on control frames, protocol
|
||||
compatibility</li>
|
||||
<li>The Double Ratchet: key derivation, chain advance, skipped-key bounds, the
|
||||
order in which state is committed</li>
|
||||
<li>The inbound message path: anything that decides what reaches the interface</li>
|
||||
<li>ICE and TURN behaviour, and the connection recovery cycle</li>
|
||||
<li>File transfer consent and type policy</li>
|
||||
<li>IndexedDB migration</li>
|
||||
<li>Disconnect and resource cleanup</li>
|
||||
</ul>
|
||||
<h2 id="writing-tests-that-are-worth-having">Writing tests that are worth having</h2>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<h2 id="documentation">Documentation</h2>
|
||||
<p>When behaviour changes, update the documentation in the same commit:</p>
|
||||
<div class="tablewrap"><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Change</th>
|
||||
<th>Documents</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td>Anything user-visible</td>
|
||||
<td><code>README.md</code>, <code>CHANGELOG.md</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Verification, keys, the ratchet</td>
|
||||
<td><code>doc/CRYPTOGRAPHY.md</code>, <code>doc/ARCHITECTURE.md</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>The invitation format or the in-band key exchange</td>
|
||||
<td><code>doc/DESCRIPTOR-SBQ2.md</code>, <code>doc/CRYPTOGRAPHY.md</code>, <code>doc/ARCHITECTURE.md</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Deployment, ICE, file policy</td>
|
||||
<td><code>doc/CONFIGURATION.md</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Calls, codecs, adaptation</td>
|
||||
<td><code>doc/CALLS.md</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Internal interfaces</td>
|
||||
<td><code>doc/API.md</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Anything security relevant</td>
|
||||
<td><code>SECURITY.md</code></td>
|
||||
</tr>
|
||||
</tbody></table></div>
|
||||
<p>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.</p>
|
||||
<h2 id="release-notes-and-security-fixes">Release notes and security fixes</h2>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<h2 id="pull-requests">Pull requests</h2>
|
||||
<p>Include:</p>
|
||||
<ul>
|
||||
<li>what the problem is</li>
|
||||
<li>what the change does</li>
|
||||
<li>which tests you ran, and which new ones you added</li>
|
||||
<li>what could regress</li>
|
||||
<li>for user-visible changes, a screenshot or a log</li>
|
||||
</ul>
|
||||
|
||||
<nav class="more">
|
||||
<h2>More documentation</h2>
|
||||
<ul>
|
||||
<li><a href="/docs/">Documentation</a></li>
|
||||
<li><a href="/docs/faq/">Frequently asked questions</a></li>
|
||||
<li><a href="/docs/architecture/">Architecture</a></li>
|
||||
<li><a href="/docs/cryptography/">Cryptography</a></li>
|
||||
<li><a href="/docs/descriptor-sbq2/">SBQ2 — connection descriptor v2</a></li>
|
||||
<li><a href="/docs/configuration/">Configuration</a></li>
|
||||
<li><a href="/docs/calls/">Voice and video calls</a></li>
|
||||
<li><a href="/docs/api/">API Notes</a></li>
|
||||
<li><a href="/docs/use-policy/">Use policy and limitations</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,469 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- Generated by scripts/build-docs.js from doc/*.md. Edits here are overwritten;
|
||||
change the Markdown instead. -->
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; upgrade-insecure-requests;">
|
||||
<meta http-equiv="X-Content-Type-Options" content="nosniff">
|
||||
<meta http-equiv="Referrer-Policy" content="strict-origin-when-cross-origin">
|
||||
<title>Cryptography - SecureBit.chat</title>
|
||||
<meta name="description" content="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…">
|
||||
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1">
|
||||
<link rel="canonical" href="https://securebit.chat/docs/cryptography/">
|
||||
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico">
|
||||
<meta property="og:site_name" content="SecureBit.chat">
|
||||
<meta property="og:title" content="Cryptography - SecureBit.chat">
|
||||
<meta property="og:description" content="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…">
|
||||
<meta property="og:url" content="https://securebit.chat/docs/cryptography/">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:locale" content="en_US">
|
||||
<meta property="og:image" content="https://securebit.chat/assets/social-card.png">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Cryptography - SecureBit.chat">
|
||||
<meta name="twitter:description" content="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…">
|
||||
<meta name="twitter:image" content="https://securebit.chat/assets/social-card.png">
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "TechArticle",
|
||||
"headline": "Cryptography",
|
||||
"description": "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…",
|
||||
"url": "https://securebit.chat/docs/cryptography/",
|
||||
"inLanguage": "en",
|
||||
"isPartOf": {
|
||||
"@type": "WebSite",
|
||||
"@id": "https://securebit.chat/#website"
|
||||
},
|
||||
"publisher": {
|
||||
"@id": "https://securebit.chat/#organization"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: #0f0f11;
|
||||
color: #d6d6dc;
|
||||
font-family: Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.68;
|
||||
}
|
||||
.wrap { max-width: 46rem; margin: 0 auto; padding: 28px 24px 90px; }
|
||||
.top {
|
||||
display: flex; flex-wrap: wrap; gap: 8px 18px; align-items: baseline;
|
||||
padding-bottom: 16px; margin-bottom: 40px;
|
||||
border-bottom: 1px solid rgba(255,255,255,.08);
|
||||
font-size: 13.5px;
|
||||
}
|
||||
.top a { color: #8a8a92; text-decoration: none; }
|
||||
.top a:hover, .top a:focus-visible { color: #f0892a; }
|
||||
.top .brand { color: #f0892a; font-weight: 700; letter-spacing: .04em; }
|
||||
.top .here { color: #d6d6dc; margin-inline-start: auto; }
|
||||
h1 { font-size: clamp(28px, 5vw, 36px); font-weight: 800; letter-spacing: -1px; line-height: 1.14; color: #f4f4f6; margin: 0 0 24px; }
|
||||
h2 { font-size: 22px; font-weight: 700; letter-spacing: -.4px; color: #f4f4f6; margin: 46px 0 12px; padding-top: 14px; border-top: 1px solid rgba(255,255,255,.07); }
|
||||
h3 { font-size: 17.5px; font-weight: 700; color: #e8e8eb; margin: 30px 0 8px; }
|
||||
h4 { font-size: 15.5px; font-weight: 700; color: #e8e8eb; margin: 22px 0 6px; }
|
||||
p, li { color: #a9a9b3; }
|
||||
p { margin: 0 0 16px; }
|
||||
ul, ol { padding-inline-start: 22px; margin: 0 0 16px; }
|
||||
li { margin: 5px 0; }
|
||||
a { color: #f0892a; text-underline-offset: 2px; }
|
||||
strong { color: #e8e8eb; }
|
||||
code { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: .88em; background: #17171c; border: 1px solid rgba(255,255,255,.07); border-radius: 4px; padding: 1px 5px; color: #e8e8eb; }
|
||||
pre { background: #0b0b0e; border: 1px solid rgba(255,255,255,.08); border-radius: 8px; padding: 14px 16px; overflow-x: auto; margin: 0 0 18px; }
|
||||
pre code { background: none; border: 0; padding: 0; font-size: 13px; line-height: 1.62; color: #c9c9d1; }
|
||||
.tablewrap { overflow-x: auto; margin: 0 0 20px; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 14.5px; min-width: 30rem; }
|
||||
th { text-align: start; color: #8a8a92; font-weight: 600; font-size: 12px; letter-spacing: .08em; text-transform: uppercase; padding: 0 14px 8px 0; border-bottom: 1px solid rgba(255,255,255,.12); }
|
||||
td { padding: 9px 14px 9px 0; border-bottom: 1px solid rgba(255,255,255,.06); vertical-align: top; color: #a9a9b3; }
|
||||
td:first-child, th:first-child { padding-inline-start: 0; }
|
||||
blockquote { margin: 0 0 18px; padding: 2px 0 2px 16px; border-inline-start: 3px solid rgba(240,137,42,.4); color: #8a8a92; }
|
||||
hr { border: 0; border-top: 1px solid rgba(255,255,255,.08); margin: 34px 0; }
|
||||
img { max-width: 100%; height: auto; }
|
||||
a:focus-visible { outline: 2px solid #f0892a; outline-offset: 2px; border-radius: 2px; }
|
||||
.more { margin-top: 64px; padding-top: 22px; border-top: 1px solid rgba(255,255,255,.08); }
|
||||
.more h2 { font-size: 13px; letter-spacing: .12em; text-transform: uppercase; color: #6b6b73; border: 0; margin: 0 0 12px; padding: 0; font-weight: 700; }
|
||||
.more ul { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: 4px 24px; }
|
||||
.more li { margin: 0; padding: 7px 0; border-bottom: 1px solid rgba(255,255,255,.05); font-size: 14.5px; }
|
||||
@media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<nav class="top">
|
||||
<a class="brand" href="/">SecureBit.chat</a>
|
||||
<a href="/docs/">Documentation</a>
|
||||
<a href="https://github.com/SecureBitChat/securebit-chat" rel="noopener">GitHub</a>
|
||||
<span class="here">Cryptography</span>
|
||||
</nav>
|
||||
<h1>Cryptography</h1>
|
||||
<p>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.</p>
|
||||
<div class="tablewrap"><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td>Release</td>
|
||||
<td>v6.7.3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Protocol version</td>
|
||||
<td>4.1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Ratchet wire version</td>
|
||||
<td>1</td>
|
||||
</tr>
|
||||
</tbody></table></div>
|
||||
<h2 id="primitives">Primitives</h2>
|
||||
<div class="tablewrap"><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Purpose</th>
|
||||
<th>Algorithm</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td>Key agreement</td>
|
||||
<td>ECDH P-384, falling back to P-256 if the browser refuses P-384</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Signatures</td>
|
||||
<td>ECDSA P-384 with SHA-384, falling back to P-256 with SHA-256</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Key derivation</td>
|
||||
<td>HKDF-SHA256</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Message encryption</td>
|
||||
<td>AES-256-GCM</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Message authentication</td>
|
||||
<td>HMAC-SHA256, and AES-GCM's own tag on the ratcheted path</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Password derivation</td>
|
||||
<td>PBKDF2-SHA256, 310,000 iterations, 32-byte salt</td>
|
||||
</tr>
|
||||
</tbody></table></div>
|
||||
<p>Session keys are non-extractable <code>CryptoKey</code> 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.</p>
|
||||
<h2 id="session-establishment">Session establishment</h2>
|
||||
<p>A session begins with one ECDH exchange. The public keys do <strong>not</strong> travel in the
|
||||
invitation — they are sent over the data channel once it opens, and are checked
|
||||
against a 16-byte commitment carried in the invitation before they are parsed or
|
||||
imported. The receiving side then validates the SPKI structure (algorithm OID,
|
||||
curve, point format and length) before importing anything. See
|
||||
<a href="/docs/descriptor-sbq2/">DESCRIPTOR-SBQ2.md</a> for the wire format and the reasoning.</p>
|
||||
<p>From the shared secret, HKDF-SHA256 derives five independent values, each under
|
||||
its own <code>info</code> label so that recovering one reveals nothing about the others:</p>
|
||||
<div class="tablewrap"><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Label</th>
|
||||
<th>Use</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><code>message-encryption-v4</code></td>
|
||||
<td>AES-256-GCM payload key on the static path</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>message-authentication-v4</code></td>
|
||||
<td>HMAC-SHA256 message authentication</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>metadata-protection-v4</code></td>
|
||||
<td>AES-256-GCM for message metadata</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>fingerprint-generation-v4</code></td>
|
||||
<td>Key fingerprint shown to the user and fed into the safety code</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>double-ratchet-root-v1</code></td>
|
||||
<td>Root key for the Double Ratchet</td>
|
||||
</tr>
|
||||
</tbody></table></div>
|
||||
<p>The raw ECDH output is produced with <code>deriveBits</code>, 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.</p>
|
||||
<p>The 64-byte session salt is <strong>not transmitted</strong>. Both sides derive it as
|
||||
SHA-512 of the handshake transcript — both invitations byte for byte, and both
|
||||
key blobs, each length-prefixed. That has two consequences: the salt cannot be
|
||||
steered by either side alone, and every key in the schedule is bound to both DTLS
|
||||
fingerprints and every ICE candidate that was exchanged.</p>
|
||||
<h2 id="verification">Verification</h2>
|
||||
<p>Both peers compute the same safety code with HKDF-SHA256, using the raw ECDH
|
||||
shared secret as input material and the SHA-256 of the handshake transcript as
|
||||
salt. The transcript covers <strong>both invitations verbatim</strong> — version byte, flags,
|
||||
expiry, fingerprints, ICE credentials, every candidate, the commitments — and
|
||||
<strong>both key blobs</strong>, each with a length prefix so no field boundary can be shifted.
|
||||
Components are ordered by role rather than by who is computing, so both sides
|
||||
reach the same seven digits.</p>
|
||||
<p>Because the shared secret is the input material, an attacker who observes the
|
||||
entire transcript still cannot predict the digits. Because the transcript is the
|
||||
salt, nothing exchanged anywhere in the handshake, in either direction, can be
|
||||
altered without changing them.</p>
|
||||
<p>Possession of the identity key is proved separately: each side signs the
|
||||
transcript with its ECDSA key and sends the signature over the channel. This
|
||||
replaced an earlier challenge/response that echoed a nonce back across seven
|
||||
fields; one signature binds the whole handshake at once.</p>
|
||||
<p>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.</p>
|
||||
<p>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
|
||||
and the invitations they arrived with, so a substitution anywhere changes the code
|
||||
the users read to each other.</p>
|
||||
<p>Key substitution alone — an attacker who can rewrite the in-band blob but not the
|
||||
invitation — does not get that far: the commitment check fails first and the
|
||||
connection is closed without anyone comparing anything.</p>
|
||||
<p>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.</p>
|
||||
<h2 id="forward-secrecy">Forward secrecy</h2>
|
||||
<p>The session keys above would last the whole conversation on their own. The Double
|
||||
Ratchet, implemented in <code>src/crypto/DoubleRatchet.js</code>, replaces them for message
|
||||
traffic so that protection does not rest on a single set of keys.</p>
|
||||
<h3 id="symmetric-ratchet">Symmetric ratchet</h3>
|
||||
<p>Each message key comes from the current chain key through <code>KDF_CK</code>, 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.</p>
|
||||
<h3 id="dh-ratchet">DH ratchet</h3>
|
||||
<p>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
|
||||
<code>KDF_RK</code> (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.</p>
|
||||
<h3 id="initialisation">Initialisation</h3>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<h3 id="frame-format">Frame format</h3>
|
||||
<p>A ratcheted message is <code>{ type: "ratchet_message", h, c }</code>, where <code>h</code> is a header
|
||||
string and <code>c</code> is the base64 body.</p>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<h3 id="out-of-order-messages">Out-of-order messages</h3>
|
||||
<p>Keys for messages that have not yet arrived are retained so they can still be
|
||||
read, within fixed bounds:</p>
|
||||
<div class="tablewrap"><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Bound</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td>Maximum skip within one chain</td>
|
||||
<td>512</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total retained keys</td>
|
||||
<td>1024, oldest evicted first</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Retention period</td>
|
||||
<td>5 minutes</td>
|
||||
</tr>
|
||||
</tbody></table></div>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<h3 id="state-is-committed-only-after-authentication">State is committed only after authentication</h3>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<h3 id="negotiation">Negotiation</h3>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<h2 id="message-protection-on-the-static-path">Message protection on the static path</h2>
|
||||
<p>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.</p>
|
||||
<p>Payloads are padded to a 16-byte boundary with random bytes, with the true length
|
||||
carried in the encrypted metadata.</p>
|
||||
<h2 id="rendering">Rendering</h2>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<h2 id="local-storage">Local storage</h2>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<h2 id="memory-handling">Memory handling</h2>
|
||||
<p>Values that can be overwritten are overwritten: the ECDH output, HKDF
|
||||
intermediates, the ratchet root and chain keys, and retained message keys.</p>
|
||||
<p>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 <code>CryptoKey</code> 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.</p>
|
||||
<h2 id="groups">Groups</h2>
|
||||
<p>A group owns no transport and no key of its own. Every byte it sends leaves over
|
||||
one of the pairwise sessions described above, already ratcheted and already
|
||||
authenticated by that session. There is no shared group key, so there is nothing
|
||||
to rotate when a member leaves: a removed member simply stops being sent
|
||||
anything.</p>
|
||||
<p>Each member holds a per-group ECDSA P-384 identity key, generated in the browser,
|
||||
non-extractable, and discarded with the group. A member is identified by the
|
||||
SHA-256 fingerprint of that key's SPKI encoding — never by a session, never by a
|
||||
name a peer supplied.</p>
|
||||
<h3 id="membership">Membership</h3>
|
||||
<p>The admin signs the full member set for an epoch: group id, epoch, operation,
|
||||
name, and the member fingerprints in canonical order. Members verify that
|
||||
signature against the key whose fingerprint they recorded when they were invited,
|
||||
not against whatever key the frame carries, and accept only an epoch that has not
|
||||
gone backwards. Identity keys travel in separate frames because eight SPKIs would
|
||||
overrun the transport's frame budget; each is checked against the fingerprint the
|
||||
signed roster commits to, so a substituted key is refused whichever frame carried
|
||||
it.</p>
|
||||
<h3 id="group-safety-code">Group safety code</h3>
|
||||
<p>Every member commits to a random 32-byte nonce, and nonces are published only
|
||||
once every commitment has arrived. The code is derived with HKDF-SHA-256 over the
|
||||
full set of fingerprints and nonces for that epoch, and rendered as seven digits.
|
||||
The commit-then-reveal ordering is what makes seven digits safe: a member who
|
||||
learnt the others' nonces first could otherwise grind its own until the digits
|
||||
came out however it liked. Membership changes open a new epoch, so the code
|
||||
changes with the member set and the old one no longer says anything about who is
|
||||
in the room.</p>
|
||||
<h3 id="messages">Messages</h3>
|
||||
<p>Group messages are signed with the sender's identity key over group id, epoch,
|
||||
sequence number, sender fingerprint and the SHA-256 of the body. Signing is not
|
||||
about confidentiality — each copy already travels inside a pairwise session — but
|
||||
about consistency: a member could otherwise send different text to different
|
||||
people under one sequence number and no recipient could tell. Two valid
|
||||
signatures on one sequence number are non-repudiable evidence of that, which is
|
||||
what a group without a shared transcript can honestly offer. It makes the split
|
||||
detectable, not impossible.</p>
|
||||
<h3 id="the-mesh">The mesh</h3>
|
||||
<p>A group starts as a star and dials itself into a mesh: once the code is
|
||||
confirmed, each pair with no link between them opens one, with the compact
|
||||
descriptors travelling over the relay path that already exists. The member with
|
||||
the smaller fingerprint dials, which is the whole glare protocol.</p>
|
||||
<p>A relayed descriptor is signed with the sender's group identity key, over the
|
||||
direction, both fingerprints, a per-attempt nonce and the descriptor bytes. The
|
||||
relaying member can drop a dial or delay it; it cannot substitute one, so it
|
||||
cannot place itself inside the link built to route around it. On the new
|
||||
connection the ordinary in-band key exchange runs unchanged, and the link is then
|
||||
released on the group's authority rather than by a human comparing digits — the
|
||||
descriptor was signed by a key the roster names and the group code already
|
||||
covers. The release path refuses any session whose in-band handshake has not
|
||||
completed and whose peer has not proved possession of that key.</p>
|
||||
<p>An existing 1:1 chat between two members is claimed instead of re-dialled, using
|
||||
a probe signed over the group id, epoch, member fingerprint and that session's
|
||||
own key fingerprint. Both endpoints of a session derive the same key fingerprint
|
||||
from the shared secret and nobody else can, so a probe replayed onto a different
|
||||
chat does not verify — which is what stops a member claiming to be someone else
|
||||
and receiving their group traffic.</p>
|
||||
<h3 id="group-calls">Group calls</h3>
|
||||
<p>A group call is not a conference. It is N-1 ordinary 1:1 calls, one to each other
|
||||
member, each carried by that member's own pairwise session — so every leg's media
|
||||
rides a DTLS-SRTP transport that a human already authenticated by comparing the
|
||||
pairwise safety code. There is no mixer and no selective forwarding unit, and at
|
||||
no point does two members' media meet anywhere but on a device.</p>
|
||||
<p>Call control is separate from call media, because the two can reach different
|
||||
sets of people. Control — a call was opened, a member joined, a member left —
|
||||
travels as group frames and therefore reaches members who are currently reachable
|
||||
only through a relay; media flows only where a direct link exists, so a member
|
||||
without one is shown as connecting rather than omitted.</p>
|
||||
<p>Each control frame is signed with the sender's group identity key over group id,
|
||||
epoch, call id, action, sender fingerprint, a per-sender sequence number and
|
||||
whether the call carries video. The signature is what makes relaying these frames
|
||||
safe: a relaying member can refuse to carry one — the availability cost relaying
|
||||
always has — but cannot add a member to a call, remove one, or end a call on
|
||||
somebody else's behalf. The sequence number is checked before the action is
|
||||
considered, so a captured frame cannot be replayed to drag a member back into a
|
||||
call they left or to close one that is running. Frames are refused outright
|
||||
unless the group is READY and its code confirmed.</p>
|
||||
<p>Call ids are 16 random bytes. Two members opening a call in the same instant
|
||||
therefore produce two ids, and every member resolves it identically by keeping
|
||||
the lower one — nothing is negotiated and no member arbitrates. Which side of a
|
||||
pair places its leg is the mesh's rule again: the smaller fingerprint dials, the
|
||||
other answers.</p>
|
||||
<p>A leg answers without prompting the user, and that is the one place a call is
|
||||
opened without a per-call confirmation. The flag permitting it is set only by the
|
||||
local group-call controller, only while this user is in that call, and is cleared
|
||||
when they leave; nothing arriving on the wire can set it. The alternative —
|
||||
prompting once per member — would be seven prompts for one decision and would
|
||||
train users to accept them. A single capture is opened when the user joins,
|
||||
shared across every leg, and stopped when the call, the group or the tab ends;
|
||||
no leg may stop a capture it borrowed.</p>
|
||||
<h2 id="scope">Scope</h2>
|
||||
<p>This describes the browser implementation as it stands in v6.7.3. It is not a
|
||||
substitute for independent cryptographic review.</p>
|
||||
|
||||
<nav class="more">
|
||||
<h2>More documentation</h2>
|
||||
<ul>
|
||||
<li><a href="/docs/">Documentation</a></li>
|
||||
<li><a href="/docs/faq/">Frequently asked questions</a></li>
|
||||
<li><a href="/docs/architecture/">Architecture</a></li>
|
||||
<li><a href="/docs/descriptor-sbq2/">SBQ2 — connection descriptor v2</a></li>
|
||||
<li><a href="/docs/configuration/">Configuration</a></li>
|
||||
<li><a href="/docs/calls/">Voice and video calls</a></li>
|
||||
<li><a href="/docs/api/">API Notes</a></li>
|
||||
<li><a href="/docs/contributing/">Contributing</a></li>
|
||||
<li><a href="/docs/use-policy/">Use policy and limitations</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,501 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- Generated by scripts/build-docs.js from doc/*.md. Edits here are overwritten;
|
||||
change the Markdown instead. -->
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; upgrade-insecure-requests;">
|
||||
<meta http-equiv="X-Content-Type-Options" content="nosniff">
|
||||
<meta http-equiv="Referrer-Policy" content="strict-origin-when-cross-origin">
|
||||
<title>SBQ2 — connection descriptor v2 - SecureBit.chat</title>
|
||||
<meta name="description" content="The descriptor is the blob a user carries from one device to the other by hand: a QR code, a deep link, or a paste into another messenger. There is no…">
|
||||
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1">
|
||||
<link rel="canonical" href="https://securebit.chat/docs/descriptor-sbq2/">
|
||||
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico">
|
||||
<meta property="og:site_name" content="SecureBit.chat">
|
||||
<meta property="og:title" content="SBQ2 — connection descriptor v2 - SecureBit.chat">
|
||||
<meta property="og:description" content="The descriptor is the blob a user carries from one device to the other by hand: a QR code, a deep link, or a paste into another messenger. There is no…">
|
||||
<meta property="og:url" content="https://securebit.chat/docs/descriptor-sbq2/">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:locale" content="en_US">
|
||||
<meta property="og:image" content="https://securebit.chat/assets/social-card.png">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="SBQ2 — connection descriptor v2 - SecureBit.chat">
|
||||
<meta name="twitter:description" content="The descriptor is the blob a user carries from one device to the other by hand: a QR code, a deep link, or a paste into another messenger. There is no…">
|
||||
<meta name="twitter:image" content="https://securebit.chat/assets/social-card.png">
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "TechArticle",
|
||||
"headline": "SBQ2 — connection descriptor v2",
|
||||
"description": "The descriptor is the blob a user carries from one device to the other by hand: a QR code, a deep link, or a paste into another messenger. There is no…",
|
||||
"url": "https://securebit.chat/docs/descriptor-sbq2/",
|
||||
"inLanguage": "en",
|
||||
"isPartOf": {
|
||||
"@type": "WebSite",
|
||||
"@id": "https://securebit.chat/#website"
|
||||
},
|
||||
"publisher": {
|
||||
"@id": "https://securebit.chat/#organization"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: #0f0f11;
|
||||
color: #d6d6dc;
|
||||
font-family: Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.68;
|
||||
}
|
||||
.wrap { max-width: 46rem; margin: 0 auto; padding: 28px 24px 90px; }
|
||||
.top {
|
||||
display: flex; flex-wrap: wrap; gap: 8px 18px; align-items: baseline;
|
||||
padding-bottom: 16px; margin-bottom: 40px;
|
||||
border-bottom: 1px solid rgba(255,255,255,.08);
|
||||
font-size: 13.5px;
|
||||
}
|
||||
.top a { color: #8a8a92; text-decoration: none; }
|
||||
.top a:hover, .top a:focus-visible { color: #f0892a; }
|
||||
.top .brand { color: #f0892a; font-weight: 700; letter-spacing: .04em; }
|
||||
.top .here { color: #d6d6dc; margin-inline-start: auto; }
|
||||
h1 { font-size: clamp(28px, 5vw, 36px); font-weight: 800; letter-spacing: -1px; line-height: 1.14; color: #f4f4f6; margin: 0 0 24px; }
|
||||
h2 { font-size: 22px; font-weight: 700; letter-spacing: -.4px; color: #f4f4f6; margin: 46px 0 12px; padding-top: 14px; border-top: 1px solid rgba(255,255,255,.07); }
|
||||
h3 { font-size: 17.5px; font-weight: 700; color: #e8e8eb; margin: 30px 0 8px; }
|
||||
h4 { font-size: 15.5px; font-weight: 700; color: #e8e8eb; margin: 22px 0 6px; }
|
||||
p, li { color: #a9a9b3; }
|
||||
p { margin: 0 0 16px; }
|
||||
ul, ol { padding-inline-start: 22px; margin: 0 0 16px; }
|
||||
li { margin: 5px 0; }
|
||||
a { color: #f0892a; text-underline-offset: 2px; }
|
||||
strong { color: #e8e8eb; }
|
||||
code { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: .88em; background: #17171c; border: 1px solid rgba(255,255,255,.07); border-radius: 4px; padding: 1px 5px; color: #e8e8eb; }
|
||||
pre { background: #0b0b0e; border: 1px solid rgba(255,255,255,.08); border-radius: 8px; padding: 14px 16px; overflow-x: auto; margin: 0 0 18px; }
|
||||
pre code { background: none; border: 0; padding: 0; font-size: 13px; line-height: 1.62; color: #c9c9d1; }
|
||||
.tablewrap { overflow-x: auto; margin: 0 0 20px; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 14.5px; min-width: 30rem; }
|
||||
th { text-align: start; color: #8a8a92; font-weight: 600; font-size: 12px; letter-spacing: .08em; text-transform: uppercase; padding: 0 14px 8px 0; border-bottom: 1px solid rgba(255,255,255,.12); }
|
||||
td { padding: 9px 14px 9px 0; border-bottom: 1px solid rgba(255,255,255,.06); vertical-align: top; color: #a9a9b3; }
|
||||
td:first-child, th:first-child { padding-inline-start: 0; }
|
||||
blockquote { margin: 0 0 18px; padding: 2px 0 2px 16px; border-inline-start: 3px solid rgba(240,137,42,.4); color: #8a8a92; }
|
||||
hr { border: 0; border-top: 1px solid rgba(255,255,255,.08); margin: 34px 0; }
|
||||
img { max-width: 100%; height: auto; }
|
||||
a:focus-visible { outline: 2px solid #f0892a; outline-offset: 2px; border-radius: 2px; }
|
||||
.more { margin-top: 64px; padding-top: 22px; border-top: 1px solid rgba(255,255,255,.08); }
|
||||
.more h2 { font-size: 13px; letter-spacing: .12em; text-transform: uppercase; color: #6b6b73; border: 0; margin: 0 0 12px; padding: 0; font-weight: 700; }
|
||||
.more ul { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: 4px 24px; }
|
||||
.more li { margin: 0; padding: 7px 0; border-bottom: 1px solid rgba(255,255,255,.05); font-size: 14.5px; }
|
||||
@media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<nav class="top">
|
||||
<a class="brand" href="/">SecureBit.chat</a>
|
||||
<a href="/docs/">Documentation</a>
|
||||
<a href="https://github.com/SecureBitChat/securebit-chat" rel="noopener">GitHub</a>
|
||||
<span class="here">SBQ2 — connection descriptor v2</span>
|
||||
</nav>
|
||||
<h1>SBQ2 — connection descriptor v2</h1>
|
||||
<p>The descriptor is the blob a user carries from one device to the other by hand:
|
||||
a QR code, a deep link, or a paste into another messenger. There is no signalling
|
||||
server, so this is the only channel that exists before the peers can talk.</p>
|
||||
<p>SBQ2 replaces the <code>SB1:bin:</code> format (CBOR + zlib + base64url of the whole offer
|
||||
package). Measured on real Chrome and Firefox SDP, a descriptor went from
|
||||
2000–2400 characters to <strong>98–149 bytes</strong>, and the QR from version 38–40 down to
|
||||
<strong>version 6–8</strong> at error-correction level M.</p>
|
||||
<p><strong>Status: live.</strong> Shipped in v5.9.0 with the in-band key exchange; new invitations
|
||||
use SBQ2 and SB1 is still read. See <a href="#7-migration">Migration</a>.</p>
|
||||
<hr>
|
||||
<h2 id="1-what-travels-where">1. What travels where</h2>
|
||||
<p>Out of band (this descriptor): ICE credentials, the DTLS certificate
|
||||
fingerprint, the candidate list, an expiry, and a 16-byte commitment.</p>
|
||||
<p>In band (over the DataChannel, once DTLS is up): identity key, ECDH key,
|
||||
signatures — everything that used to make the descriptor large.</p>
|
||||
<p>The fingerprint is what makes that split safe. It arrives over the channel the
|
||||
user already trusts, and DTLS completes only with the holder of the matching
|
||||
private key, so the transport is authenticated to whoever showed the code before
|
||||
any key material moves. The commitment makes substitution of that material fail
|
||||
closed automatically rather than relying on the human comparison, and the SAS
|
||||
covers a transcript containing both descriptors verbatim and both in-band blobs.</p>
|
||||
<hr>
|
||||
<h2 id="2-wire-layout">2. Wire layout</h2>
|
||||
<p>All integers big-endian. Offsets are for the offer; an answer inserts its
|
||||
8-byte binding tag at offset 5 and everything after shifts by 8.</p>
|
||||
<pre><code>off len field
|
||||
0 1 version = 0x02 mismatch is an error, never a reparse
|
||||
1 1 flags
|
||||
2 3 expiry u24, minutes since 2024-01-01T00:00:00Z
|
||||
5 [8] binding_tag ANSWERS ONLY
|
||||
.. 32 dtls_fingerprint SHA-256 of the certificate, raw
|
||||
.. 1 ufrag_len 4..64
|
||||
.. L1 ufrag ASCII, RFC 8839 ice-char alphabet
|
||||
.. 1 pwd_len 22..64
|
||||
.. L2 pwd ASCII, RFC 8839 ice-char alphabet
|
||||
.. 1 candidate_count 0..8
|
||||
.. .. candidates
|
||||
.. [16] commitment if flags bit 6
|
||||
.. [1] ext_len if flags bit 7
|
||||
.. .. TLV records if flags bit 7
|
||||
</code></pre>
|
||||
<h3 id="flags">flags</h3>
|
||||
<div class="tablewrap"><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>bits</th>
|
||||
<th>meaning</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td>0–1</td>
|
||||
<td>type: 0 offer, 1 answer. 2 and 3 are reserved → <strong>reject</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2–3</td>
|
||||
<td>DTLS setup role: 0 actpass, 1 active, 2 passive. 3 reserved → <strong>reject</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>4–5</td>
|
||||
<td>max-message-size: 0 = 262144, 1 = 1073741823, 2 = 65536, 3 = explicit, in extension <code>0x01</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>6</td>
|
||||
<td>a commitment follows the candidates</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>7</td>
|
||||
<td>a TLV extension area follows</td>
|
||||
</tr>
|
||||
</tbody></table></div>
|
||||
<p>Every bit is allocated. Future fields go in the TLV area, which is itself
|
||||
deny-by-default; there is deliberately no spare "ignore me" bit.</p>
|
||||
<h3 id="candidate">candidate</h3>
|
||||
<pre><code>1 byte kind << 4 | tcptype
|
||||
n bytes address (v4 = 4, v6 = 16, mDNS UUID = 16)
|
||||
2 bytes port
|
||||
</code></pre>
|
||||
<p><code>kind</code>: 0 host-v4, 1 host-mDNS, 2 srflx-v4, 3 relay-v4, 4 host-v6, 5 srflx-v6,
|
||||
6 relay-v6. 7–15 reserved → <strong>reject</strong>.
|
||||
<code>tcptype</code>: 0 udp, 1 tcp/passive, 2 tcp/active, 3 tcp/so. 4–15 → <strong>reject</strong>.</p>
|
||||
<p>Foundation and priority are <strong>not</strong> transmitted. Priority only orders
|
||||
connectivity checks, and each peer computes its own local priorities anyway; the
|
||||
serializer re-derives RFC 8445 §5.1.2.1 values with <code>localPref = 65535 - index</code>,
|
||||
so the sender's ordering intent survives at zero cost. Foundations are grouped by
|
||||
kind and transport, satisfying both halves of §5.1.1.3. <code>generation</code> and
|
||||
<code>network-cost</code> are Chrome extensions and are dropped.</p>
|
||||
<h3 id="what-the-serializer-must-emit-anyway">What the serializer must emit anyway</h3>
|
||||
<p>Three things are not in the descriptor but must appear in the rebuilt SDP. All
|
||||
three are constants or derived from data already present, so none costs a byte —
|
||||
and each was found by a live cross-browser test rather than by reading the spec.</p>
|
||||
<ul>
|
||||
<li><p><strong><code>raddr</code> / <code>rport</code> on every srflx, prflx and relay candidate.</strong> RFC 8839 §5.1
|
||||
makes rel-addr and rel-port mandatory for non-host candidates even though ICE's
|
||||
own algorithm never reads them. An earlier draft left them out as
|
||||
"diagnostics". Chrome accepts such a line; <strong>Firefox drops the candidate.</strong>
|
||||
Measured: relay-only connections to Firefox went 0/8 while the browser's own
|
||||
SDP went 8/8, and the fault was invisible in the STUN and TURN profiles because
|
||||
a host pair connected instead. <code>0.0.0.0</code>/<code>0</code> (or <code>::</code>/<code>0</code>) is emitted — the
|
||||
same placeholder Chrome uses when it has no base address to disclose.</p>
|
||||
</li>
|
||||
<li><p><strong>No <code>a=ice-options:trickle</code>, and an explicit <code>a=end-of-candidates</code>.</strong> A
|
||||
descriptor is a complete one-shot candidate set; there is no channel to trickle
|
||||
over. Advertising trickle promises candidates that can never arrive and leaves
|
||||
the peer waiting for them.</p>
|
||||
</li>
|
||||
<li><p><strong>A real default candidate on the <code>m=</code> port and <code>c=</code> line.</strong> <code>m=application 9</code>
|
||||
with <code>c=IN IP4 0.0.0.0</code> is the trickle convention for "nothing gathered yet"
|
||||
(RFC 8840 §4.1) and is simply false here. The serializer advertises the most
|
||||
publicly reachable candidate — relay, then srflx, then host, which is the order
|
||||
Chrome uses and the reverse of ICE priority — falling back to the null form
|
||||
only when every candidate is mDNS, which is also what Chrome does.</p>
|
||||
</li>
|
||||
</ul>
|
||||
<h3 id="tlv-extension-area">TLV extension area</h3>
|
||||
<pre><code>ext_len u8, 1..255, must be consumed exactly
|
||||
record: type u8, len u8, value[len]
|
||||
</code></pre>
|
||||
<p>Records must appear in <strong>ascending type order with no duplicates</strong>, so every
|
||||
descriptor has exactly one valid spelling. An unknown type is a hard error.</p>
|
||||
<div class="tablewrap"><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>type</th>
|
||||
<th>len</th>
|
||||
<th>value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><code>0x01</code></td>
|
||||
<td>4</td>
|
||||
<td>max-message-size, u32, 1024..2^31-1, and not equal to a value the flags already encode</td>
|
||||
</tr>
|
||||
</tbody></table></div>
|
||||
<hr>
|
||||
<h2 id="3-sizes-measured">3. Sizes measured</h2>
|
||||
<p>Real SDP, identical ICE configuration on both peers, each peer gathering in its
|
||||
own browser process. QR versions are byte mode at level M.</p>
|
||||
<div class="tablewrap"><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>browser</th>
|
||||
<th>profile</th>
|
||||
<th>offer</th>
|
||||
<th>QR</th>
|
||||
<th>answer</th>
|
||||
<th>QR</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td>Chrome</td>
|
||||
<td>host_only</td>
|
||||
<td>103 B</td>
|
||||
<td>v6</td>
|
||||
<td>111 B</td>
|
||||
<td>v7</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Chrome</td>
|
||||
<td>stun</td>
|
||||
<td>110 B</td>
|
||||
<td>v7</td>
|
||||
<td>118 B</td>
|
||||
<td>v7</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Chrome</td>
|
||||
<td>turn_all</td>
|
||||
<td>124 B</td>
|
||||
<td>v8</td>
|
||||
<td>132 B</td>
|
||||
<td>v8</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Chrome</td>
|
||||
<td>turn_relay_only</td>
|
||||
<td>98 B</td>
|
||||
<td>v6</td>
|
||||
<td>106 B</td>
|
||||
<td>v6</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Firefox</td>
|
||||
<td>host_only</td>
|
||||
<td>134 B</td>
|
||||
<td>v8</td>
|
||||
<td>142 B</td>
|
||||
<td>v8</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Firefox</td>
|
||||
<td>stun</td>
|
||||
<td>141 B</td>
|
||||
<td>v8</td>
|
||||
<td>149 B</td>
|
||||
<td>v8</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Firefox</td>
|
||||
<td>turn_all</td>
|
||||
<td>136 B</td>
|
||||
<td>v8</td>
|
||||
<td>144 B</td>
|
||||
<td>v8</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Firefox</td>
|
||||
<td>turn_relay_only</td>
|
||||
<td>110 B</td>
|
||||
<td>v7</td>
|
||||
<td>118 B</td>
|
||||
<td>v7</td>
|
||||
</tr>
|
||||
</tbody></table></div>
|
||||
<p>Firefox descriptors run ~12 bytes larger because its ICE credentials are longer
|
||||
(8-char ufrag and 32-char pwd against Chrome's 4 and 24).</p>
|
||||
<p>Transport: <strong>raw bytes in QR byte mode</strong>. base45 buys nothing (198 alphanumeric
|
||||
characters = 1089 bits against 1056 bits raw for the same 132-byte payload), and
|
||||
base64url costs a QR version. For text channels the form is <code>SB2:</code> + base64url,
|
||||
whose alphabet survives messenger auto-formatting; the decoder strips whitespace
|
||||
so a wrapped paste still works. <strong>DEFLATE is not used</strong> — on this payload it adds
|
||||
2 to 11 bytes, and dropping it removes the decompression-bomb surface with it.</p>
|
||||
<hr>
|
||||
<h2 id="4-candidate-pruning-coverage-before-count">4. Candidate pruning: coverage before count</h2>
|
||||
<p>A count limit is the wrong policy. Sorted v4-first it can evict the only usable
|
||||
candidate on an IPv6-only network, which is a normal mode on several mobile
|
||||
carriers, and one that ignores transport can evict the TCP candidate that exists
|
||||
precisely for networks where UDP is blocked.</p>
|
||||
<p>The rule, in order:</p>
|
||||
<ol>
|
||||
<li><strong>Coverage.</strong> Every <code>(address family, candidate type, transport)</code> combination
|
||||
present in the input keeps its highest-priority representative. Families are
|
||||
<code>v4</code>, <code>v6</code> and <code>mdns</code> — mDNS is its own family because it resolves only on
|
||||
the sender's link, covering a case neither of the others does. Coverage is
|
||||
never cut, not even to stay inside the byte budget: a QR one version larger
|
||||
costs less than a connection that cannot be made.</li>
|
||||
<li><strong>Surplus,</strong> by the sender's own priority, until either <code>MAX_CANDIDATES</code> (8)
|
||||
or <code>SURPLUS_CANDIDATE_BYTES</code> (48) runs out, with relays capped at 2.</li>
|
||||
</ol>
|
||||
<p>Candidates a peer cannot dial — ICE-TCP <code>active</code> and <code>so</code>, which are
|
||||
outbound-only sockets on the discard port — are excluded from coverage and
|
||||
compete only for surplus. Firefox advertises an <code>active</code> host candidate on every
|
||||
connection; at 19 bytes for an mDNS address it must not hold a coverage slot it
|
||||
cannot use.</p>
|
||||
<p>The 48-byte surplus budget is derived, not chosen: the largest answer head
|
||||
measured is Firefox's at 104 bytes, and QR version 8 at level M holds 152, so
|
||||
152 − 104 = 48.</p>
|
||||
<p>The relay cap of 2 applies only to surplus. A TURN server offering udp/tcp/tls
|
||||
hands out one allocation per transport and they all resolve to the same relayed
|
||||
address, so the third adds no reachability; two survive in case one allocation's
|
||||
binding dies.</p>
|
||||
<hr>
|
||||
<h2 id="5-freshness-uniqueness-one-shot">5. Freshness, uniqueness, one-shot</h2>
|
||||
<p><strong>Expiry</strong> is absolute, at minute granularity, with a ±2-minute skew allowance.
|
||||
Two minutes is sized against the failure it exists for: an NTP-synced device is
|
||||
within milliseconds and an unsynced modern device drifts seconds per day, so two
|
||||
minutes swallows every ordinary case while still refusing a grossly wrong clock
|
||||
(manually set, or reset by a dead battery) — a device that cannot be given a
|
||||
meaningful freshness guarantee should be told so, and the error message names the
|
||||
clock as the likely cause. The cost is a replay window of 12 minutes instead of
|
||||
10.</p>
|
||||
<p><strong>The offer carries no nonce.</strong> It does not need one: <code>ice-pwd</code> is in the hashed
|
||||
bytes, RFC 8839 §5.4 requires it to contain at least 128 bits of randomness, and
|
||||
every browser regenerates it per peer connection and per ICE restart. A separate
|
||||
8-byte random field would have been 8 bytes restating entropy already present.</p>
|
||||
<p><strong>The answer carries an 8-byte binding tag</strong>, <code>SHA-256("sbq2/bind\0" || offer_bytes)[0..8]</code>. The offerer keeps the tag of the offer it is currently
|
||||
showing and refuses anything else, which is both the answer's replay defence and
|
||||
what makes each offer exactly one-shot — without any stored state between
|
||||
sessions.</p>
|
||||
<blockquote>
|
||||
<p><strong>Limitation, on the record:</strong> 64 bits is not a standalone integrity primitive.
|
||||
The tag is a duplicate-detection device whose security comes from the SAS
|
||||
transcript, which covers both descriptors in full. Nothing may be built on this
|
||||
tag alone. If a future change needs one, widen the field rather than lean on it.</p>
|
||||
</blockquote>
|
||||
<hr>
|
||||
<h2 id="6-sas">6. SAS</h2>
|
||||
<pre><code>transcript = "sbq2/sas/v1\0"
|
||||
|| len32(offer_bytes) || offer_bytes
|
||||
|| len32(answer_bytes) || answer_bytes
|
||||
|| len32(offer_blob) || offer_blob
|
||||
|| len32(answer_blob) || answer_blob
|
||||
|
||||
SAS = HKDF-SHA256(IKM = ECDH shared secret,
|
||||
salt = SHA-256(transcript),
|
||||
info = "sbq2-sas-v1") -> 64 bits -> 7 digits
|
||||
</code></pre>
|
||||
<p>The transcript covers both descriptors byte for byte — version, flags, expiry,
|
||||
binding tag, fingerprints, ICE credentials, every candidate, the commitment and
|
||||
the whole extension area — plus both in-band blobs. Lengths are prefixed so no
|
||||
field boundary can be shifted to produce a colliding transcript.</p>
|
||||
<hr>
|
||||
<h2 id="7-migration">7. Migration</h2>
|
||||
<p>The two formats separate without heuristics: an SBQ2 QR starts with byte <code>0x02</code>,
|
||||
an SB1 payload starts with ASCII <code>S</code> (<code>0x53</code>); in text, the prefixes are <code>SB2:</code>
|
||||
and <code>SB1:bin:</code> / <code>SB1:gz:</code>.</p>
|
||||
<div class="tablewrap"><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>phase</th>
|
||||
<th>state</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td>1</td>
|
||||
<td>Codec and tests in the tree. <strong>Done, v5.8.0/v5.8.1.</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>Receiver accepts both formats. <strong>Done, v5.9.0</strong> — reception is unconditional and not governed by the send flag.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td>Sender emits SBQ2. <strong>Done, v5.9.0</strong>, together with the in-band key exchange that made it safe.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>4</td>
|
||||
<td>Remove SB1 emission (already gone) and, after a deprecation window, SB1 parsing along with <code>cose-qr.js</code>, <code>inflateBounded</code> and the animated multi-frame QR path. <strong>Not yet</strong> — the animated path is still required while SB1 invitations are accepted.</td>
|
||||
</tr>
|
||||
</tbody></table></div>
|
||||
<h3 id="rollback">Rollback</h3>
|
||||
<p><code>EnhancedSecureWebRTCManager.SBQ2_SEND_ENABLED = false</code> and redeploy. That single
|
||||
value governs only what is emitted; a client built with it off still reads SBQ2
|
||||
invitations, so the two ends never have to be rolled back together.</p>
|
||||
<p>The switch is deliberately not consulted in the receive path and never inside an
|
||||
established session. <code>_handshakeMode</code> latches per connection, so a session that
|
||||
began as SBQ2 cannot be pushed back onto SB1 partway through — every SBQ2 failure
|
||||
tears the connection down instead.</p>
|
||||
<h3 id="what-phase-3-depended-on-and-what-landed">What phase 3 depended on, and what landed</h3>
|
||||
<p>Phase 3 was blocked on the in-band key exchange, because shrinking the descriptor
|
||||
without it would have removed key material from the invitation with nothing
|
||||
carrying it instead. v5.9.0 delivered that: a key-exchange phase after the
|
||||
DataChannel opens, the commitment verified before the blob is parsed, the
|
||||
transcript SAS in place of <code>_computeSAS</code>, the salt derived instead of
|
||||
transmitted, <code>authProof</code> replaced by a signature over the transcript, and the
|
||||
Double Ratchet started from the transcript-derived material.</p>
|
||||
<h3 id="compatibility">Compatibility</h3>
|
||||
<p>A client older than v5.9.0 cannot read an SBQ2 invitation.</p>
|
||||
<p>From v5.9.0 onward an unrecognised <code>SB<n>:</code> family is reported as <em>"This
|
||||
invitation was created by a newer version of SecureBit. Please update the app to
|
||||
connect."</em> — so the next format change explains itself.</p>
|
||||
<p>Versions <strong>5.8.1 and earlier predate that check</strong>: they fall through to
|
||||
<code>JSON.parse</code> and show <code>Invalid invitation format: Unexpected token 'S', "SB2:Ak…" is not valid JSON</code>. That is accurate but unhelpful, and it cannot be fixed
|
||||
retroactively in builds already shipped. Both ends must be on 5.9.0+.</p>
|
||||
<h2 id="8-decoder-rules">8. Decoder rules</h2>
|
||||
<p>The decoder parses fully attacker-controlled input.</p>
|
||||
<ul>
|
||||
<li>Payload ceiling (512 B) checked before any structure is walked.</li>
|
||||
<li>Version compared first; a mismatch throws.</li>
|
||||
<li>Reserved values (descriptor type 2–3, setup role 3, candidate kind 7–15,
|
||||
tcptype 4–15) are refused, never coerced.</li>
|
||||
<li>Unknown TLV types are refused. Records must be ascending and unique, and a TLV
|
||||
restating a value the flags already encode is refused as non-canonical.</li>
|
||||
<li>Flags and extension area must agree in both directions: <code>mms = 3</code> without
|
||||
extension <code>0x01</code> is an error, and extension <code>0x01</code> without <code>mms = 3</code> is too.</li>
|
||||
<li>ufrag and pwd are range-checked and alphabet-checked; any byte outside
|
||||
printable ASCII fails before the value is used, so a CR or LF cannot reach the
|
||||
serializer and inject an SDP line.</li>
|
||||
<li>Trailing bytes after the structure are an error. A decoder that tolerated them
|
||||
would let a second reading of the same QR slip past whatever hashed the
|
||||
canonical form.</li>
|
||||
<li>Base64url input must be canonical: non-zero padding bits are refused, so a
|
||||
descriptor has exactly one textual spelling.</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="9-provenance-of-the-numbers">9. Provenance of the numbers</h2>
|
||||
<p>Everything above was measured, not estimated.</p>
|
||||
<ul>
|
||||
<li>Chrome fixtures: <code>tests/fixtures/sdp-chrome.json</code>, captured over CDP from a
|
||||
real Chrome across four network profiles, both peers configured identically,
|
||||
each gathering in isolation.</li>
|
||||
<li>Firefox fixtures: <code>tests/fixtures/sdp-firefox.json</code>, Firefox 153 over
|
||||
Marionette, same method.</li>
|
||||
<li>Round-trip, rejection, coverage, TLV, skew and transcript tests:
|
||||
<code>tests/descriptor-sbq2.test.mjs</code>.</li>
|
||||
</ul>
|
||||
<p>The original brief's payload (CBOR 2391 B, 991 B of SDP) was an <strong>offer</strong> — it
|
||||
carries <code>sl</code>, <code>si</code>, <code>vc</code> and <code>ac</code>, and has no <code>ap</code> block. An earlier draft of the
|
||||
analysis matched it against a STUN-profile answer on the strength of the post-
|
||||
zlib and post-base64 sizes; those agreed by coincidence while the CBOR sizes
|
||||
differ by 135 bytes. Conclusions were unaffected, but the attribution was wrong.</p>
|
||||
|
||||
<nav class="more">
|
||||
<h2>More documentation</h2>
|
||||
<ul>
|
||||
<li><a href="/docs/">Documentation</a></li>
|
||||
<li><a href="/docs/faq/">Frequently asked questions</a></li>
|
||||
<li><a href="/docs/architecture/">Architecture</a></li>
|
||||
<li><a href="/docs/cryptography/">Cryptography</a></li>
|
||||
<li><a href="/docs/configuration/">Configuration</a></li>
|
||||
<li><a href="/docs/calls/">Voice and video calls</a></li>
|
||||
<li><a href="/docs/api/">API Notes</a></li>
|
||||
<li><a href="/docs/contributing/">Contributing</a></li>
|
||||
<li><a href="/docs/use-policy/">Use policy and limitations</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,319 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- Generated by scripts/build-docs.js from doc/*.md. Edits here are overwritten;
|
||||
change the Markdown instead. -->
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; upgrade-insecure-requests;">
|
||||
<meta http-equiv="X-Content-Type-Options" content="nosniff">
|
||||
<meta http-equiv="Referrer-Policy" content="strict-origin-when-cross-origin">
|
||||
<title>Frequently asked questions - SecureBit.chat</title>
|
||||
<meta name="description" content="Short answers to what people ask before they trust the software with anything. Every one of them is drawn from the documents linked alongside it, so if an…">
|
||||
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1">
|
||||
<link rel="canonical" href="https://securebit.chat/docs/faq/">
|
||||
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico">
|
||||
<meta property="og:site_name" content="SecureBit.chat">
|
||||
<meta property="og:title" content="Frequently asked questions - SecureBit.chat">
|
||||
<meta property="og:description" content="Short answers to what people ask before they trust the software with anything. Every one of them is drawn from the documents linked alongside it, so if an…">
|
||||
<meta property="og:url" content="https://securebit.chat/docs/faq/">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:locale" content="en_US">
|
||||
<meta property="og:image" content="https://securebit.chat/assets/social-card.png">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Frequently asked questions - SecureBit.chat">
|
||||
<meta name="twitter:description" content="Short answers to what people ask before they trust the software with anything. Every one of them is drawn from the documents linked alongside it, so if an…">
|
||||
<meta name="twitter:image" content="https://securebit.chat/assets/social-card.png">
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
"url": "https://securebit.chat/docs/faq/",
|
||||
"inLanguage": "en",
|
||||
"isPartOf": {
|
||||
"@type": "WebSite",
|
||||
"@id": "https://securebit.chat/#website"
|
||||
},
|
||||
"publisher": {
|
||||
"@id": "https://securebit.chat/#organization"
|
||||
},
|
||||
"mainEntity": [
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Do I need an account?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "<p>No. There is nothing to register, no email address, no phone number and no password. You open the page, your browser generates a key pair, and you send the resulting invitation to the person you want to talk to. Nothing about you is stored anywhere, because there is nowhere to store it.</p>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Where are my messages stored?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "<p>Nowhere. Messages travel directly between the two browsers over WebRTC and are held only in the memory of the two devices taking part. There is no message server, no database and no backup — which also means a conversation you close is gone, and nobody can hand it over later because nobody has it.</p>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Is there a server in the middle?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "<p>No, and there is not even a signalling service. The offer and the answer that set up a connection are moved between the two people by whatever channel they already have: a QR code, a pasted block of text, a link. That is the unusual part of the design and it shapes everything else — the out-of-band channel is assumed to be readable and rewritable by an attacker, which is exactly why the safety code comparison exists. See <a href=\"/docs/architecture/\">ARCHITECTURE.md</a>.</p> <p>A TURN server, if you use one, relays encrypted packets. It never sees message content, and it is optional.</p>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "What encryption does SecureBit.chat use?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "<p>Key exchange is ECDH on P-384, message payloads are AES-256-GCM, and messages carry ECDSA signatures with full ASN.1 validation of every key that arrives. Session keys rotate continuously through a Double Ratchet and are discarded after use, so a key recovered later cannot open earlier conversations. The transport underneath is DTLS, as WebRTC requires. Everything runs on the browser's Web Crypto API rather than hand-rolled primitives — <a href=\"/docs/cryptography/\">CRYPTOGRAPHY.md</a> sets out the key schedule in full.</p>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "What is the safety code, and why do I have to compare it?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "<p>It is a short code derived from both sides of the completed key exchange. If the two of you see the same code, nobody sat in the middle of the exchange; if someone did, the codes differ and the session is not what it claims to be.</p> <p>The comparison has to happen over a channel an attacker cannot impersonate — in person, or a voice you recognise. Comparing it inside the chat itself proves nothing. It is enforced rather than merely displayed: until both sides confirm, reconnection signalling, call setup, delivery receipts and incoming file transfers are all refused, and three incorrect entries end the session.</p>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Is it free, and can I read the source?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "<p>Yes to both. The software is free, has no paid tier and no upsell, and is published under the MIT licence at <a href=\"https://github.com/SecureBitChat/securebit-chat\">github.com/SecureBitChat/securebit-chat</a>. Nothing is minified beyond recognition or hidden behind a build you cannot reproduce; <a href=\"/docs/configuration/\">CONFIGURATION.md</a> describes building and running it yourself.</p>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Do I have to install anything?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "<p>No. It runs in any current browser with WebRTC and the Web Crypto API. It can be installed as a Progressive Web App if you would rather have an icon, and there are native desktop builds for Windows, macOS and Linux. Calls are not yet available on the Linux desktop build; they work everywhere else, including in the browser.</p>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Can the person I am talking to see my IP address?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "<p>On a default connection, yes — a direct WebRTC path means each side learns the other's address. That is how a direct connection works, not a flaw in this implementation.</p> <p>Relay-only mode prevents it: it sets <code>iceTransportPolicy: "relay"</code>, so all traffic goes through a TURN server and the peers see only the relay. The trade is that the relay operator can then see both addresses and the timing of the traffic, though never the content. Choose according to who you are protecting against.</p>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Do I need my own TURN server?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "<p>Only for relay-only mode, or when a direct path cannot be established at all. Public STUN defaults are built in and are enough for most connections. You can paste your own STUN and TURN servers under Advanced network settings; they are validated against an allowlist of schemes, hosts and transports before they reach the connection.</p> <p>One thing worth knowing: any TURN credential sent to a browser is public by definition, because the browser has to read it. Apply quotas on the TURN server rather than relying on the credential staying secret. <a href=\"/docs/configuration/\">CONFIGURATION.md</a> covers the setup and how to verify it works.</p>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Can I send files, and how large?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "<p>Yes, over the same encrypted channel, with a per-file AES-GCM key and a SHA-256 integrity check. The receiver sees name, size and type and has to accept before any buffer is allocated or a single chunk is sent.</p> <p>The ceiling is 100 MB per file, with lower limits by category: images 25 MB, PDFs 50 MB, text 10 MB, archives 100 MB, audio 20 MB. Executable and scriptable formats are blocked outright — <code>.exe</code>, <code>.js</code>, <code>.html</code>, <code>.svg</code> and others — and the file extension, not the browser-supplied MIME type, is the boundary that decides.</p>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Are group chats and calls supported?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "<p>Yes. Encrypted voice and video calls shipped in early 2026, and group communications are current work. Group sessions keep the same properties as one-to-one ones: no server holds the conversation, and members verify each other. <a href=\"/docs/calls/\">CALLS.md</a> covers codec choices and how the connection adapts to a poor network.</p>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Is there a mobile app?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "<p>Not yet. The web application works in mobile browsers today and can be installed to the home screen as a PWA. Native iOS and Android builds are planned; the roadmap on the front page lists where they sit relative to the rest of the work.</p>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "What does it not protect against?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "<p>Read <a href=\"/docs/use-policy/\">USE-POLICY.md</a> before relying on this for anything that matters. In short: a compromised device sees your messages exactly as you do, and no transport encryption helps. The person you are talking to can screenshot or repeat what you said. A safety code you skipped protects nothing. And someone watching your network can see that you are using a WebRTC application, even though they cannot see what you send — the software does not hide its own use.</p>"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: #0f0f11;
|
||||
color: #d6d6dc;
|
||||
font-family: Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.68;
|
||||
}
|
||||
.wrap { max-width: 46rem; margin: 0 auto; padding: 28px 24px 90px; }
|
||||
.top {
|
||||
display: flex; flex-wrap: wrap; gap: 8px 18px; align-items: baseline;
|
||||
padding-bottom: 16px; margin-bottom: 40px;
|
||||
border-bottom: 1px solid rgba(255,255,255,.08);
|
||||
font-size: 13.5px;
|
||||
}
|
||||
.top a { color: #8a8a92; text-decoration: none; }
|
||||
.top a:hover, .top a:focus-visible { color: #f0892a; }
|
||||
.top .brand { color: #f0892a; font-weight: 700; letter-spacing: .04em; }
|
||||
.top .here { color: #d6d6dc; margin-inline-start: auto; }
|
||||
h1 { font-size: clamp(28px, 5vw, 36px); font-weight: 800; letter-spacing: -1px; line-height: 1.14; color: #f4f4f6; margin: 0 0 24px; }
|
||||
h2 { font-size: 22px; font-weight: 700; letter-spacing: -.4px; color: #f4f4f6; margin: 46px 0 12px; padding-top: 14px; border-top: 1px solid rgba(255,255,255,.07); }
|
||||
h3 { font-size: 17.5px; font-weight: 700; color: #e8e8eb; margin: 30px 0 8px; }
|
||||
h4 { font-size: 15.5px; font-weight: 700; color: #e8e8eb; margin: 22px 0 6px; }
|
||||
p, li { color: #a9a9b3; }
|
||||
p { margin: 0 0 16px; }
|
||||
ul, ol { padding-inline-start: 22px; margin: 0 0 16px; }
|
||||
li { margin: 5px 0; }
|
||||
a { color: #f0892a; text-underline-offset: 2px; }
|
||||
strong { color: #e8e8eb; }
|
||||
code { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: .88em; background: #17171c; border: 1px solid rgba(255,255,255,.07); border-radius: 4px; padding: 1px 5px; color: #e8e8eb; }
|
||||
pre { background: #0b0b0e; border: 1px solid rgba(255,255,255,.08); border-radius: 8px; padding: 14px 16px; overflow-x: auto; margin: 0 0 18px; }
|
||||
pre code { background: none; border: 0; padding: 0; font-size: 13px; line-height: 1.62; color: #c9c9d1; }
|
||||
.tablewrap { overflow-x: auto; margin: 0 0 20px; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 14.5px; min-width: 30rem; }
|
||||
th { text-align: start; color: #8a8a92; font-weight: 600; font-size: 12px; letter-spacing: .08em; text-transform: uppercase; padding: 0 14px 8px 0; border-bottom: 1px solid rgba(255,255,255,.12); }
|
||||
td { padding: 9px 14px 9px 0; border-bottom: 1px solid rgba(255,255,255,.06); vertical-align: top; color: #a9a9b3; }
|
||||
td:first-child, th:first-child { padding-inline-start: 0; }
|
||||
blockquote { margin: 0 0 18px; padding: 2px 0 2px 16px; border-inline-start: 3px solid rgba(240,137,42,.4); color: #8a8a92; }
|
||||
hr { border: 0; border-top: 1px solid rgba(255,255,255,.08); margin: 34px 0; }
|
||||
img { max-width: 100%; height: auto; }
|
||||
a:focus-visible { outline: 2px solid #f0892a; outline-offset: 2px; border-radius: 2px; }
|
||||
.more { margin-top: 64px; padding-top: 22px; border-top: 1px solid rgba(255,255,255,.08); }
|
||||
.more h2 { font-size: 13px; letter-spacing: .12em; text-transform: uppercase; color: #6b6b73; border: 0; margin: 0 0 12px; padding: 0; font-weight: 700; }
|
||||
.more ul { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: 4px 24px; }
|
||||
.more li { margin: 0; padding: 7px 0; border-bottom: 1px solid rgba(255,255,255,.05); font-size: 14.5px; }
|
||||
@media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<nav class="top">
|
||||
<a class="brand" href="/">SecureBit.chat</a>
|
||||
<a href="/docs/">Documentation</a>
|
||||
<a href="https://github.com/SecureBitChat/securebit-chat" rel="noopener">GitHub</a>
|
||||
<span class="here">Frequently asked questions</span>
|
||||
</nav>
|
||||
<h1>Frequently asked questions</h1>
|
||||
<p>Short answers to what people ask before they trust the software with anything.
|
||||
Every one of them is drawn from the documents linked alongside it, so if an answer
|
||||
here and a document disagree, the document is right and this page is stale.</p>
|
||||
<h2 id="do-i-need-an-account">Do I need an account?</h2>
|
||||
<p>No. There is nothing to register, no email address, no phone number and no
|
||||
password. You open the page, your browser generates a key pair, and you send the
|
||||
resulting invitation to the person you want to talk to. Nothing about you is
|
||||
stored anywhere, because there is nowhere to store it.</p>
|
||||
<h2 id="where-are-my-messages-stored">Where are my messages stored?</h2>
|
||||
<p>Nowhere. Messages travel directly between the two browsers over WebRTC and are
|
||||
held only in the memory of the two devices taking part. There is no message
|
||||
server, no database and no backup — which also means a conversation you close is
|
||||
gone, and nobody can hand it over later because nobody has it.</p>
|
||||
<h2 id="is-there-a-server-in-the-middle">Is there a server in the middle?</h2>
|
||||
<p>No, and there is not even a signalling service. The offer and the answer that set
|
||||
up a connection are moved between the two people by whatever channel they already
|
||||
have: a QR code, a pasted block of text, a link. That is the unusual part of the
|
||||
design and it shapes everything else — the out-of-band channel is assumed to be
|
||||
readable and rewritable by an attacker, which is exactly why the safety code
|
||||
comparison exists. See <a href="/docs/architecture/">ARCHITECTURE.md</a>.</p>
|
||||
<p>A TURN server, if you use one, relays encrypted packets. It never sees message
|
||||
content, and it is optional.</p>
|
||||
<h2 id="what-encryption-does-securebitchat-use">What encryption does SecureBit.chat use?</h2>
|
||||
<p>Key exchange is ECDH on P-384, message payloads are AES-256-GCM, and messages
|
||||
carry ECDSA signatures with full ASN.1 validation of every key that arrives.
|
||||
Session keys rotate continuously through a Double Ratchet and are discarded after
|
||||
use, so a key recovered later cannot open earlier conversations. The transport
|
||||
underneath is DTLS, as WebRTC requires. Everything runs on the browser's Web
|
||||
Crypto API rather than hand-rolled primitives — <a href="/docs/cryptography/">CRYPTOGRAPHY.md</a>
|
||||
sets out the key schedule in full.</p>
|
||||
<h2 id="what-is-the-safety-code-and-why-do-i-have-to-compare-it">What is the safety code, and why do I have to compare it?</h2>
|
||||
<p>It is a short code derived from both sides of the completed key exchange. If the
|
||||
two of you see the same code, nobody sat in the middle of the exchange; if someone
|
||||
did, the codes differ and the session is not what it claims to be.</p>
|
||||
<p>The comparison has to happen over a channel an attacker cannot impersonate — in
|
||||
person, or a voice you recognise. Comparing it inside the chat itself proves
|
||||
nothing. It is enforced rather than merely displayed: until both sides confirm,
|
||||
reconnection signalling, call setup, delivery receipts and incoming file
|
||||
transfers are all refused, and three incorrect entries end the session.</p>
|
||||
<h2 id="is-it-free-and-can-i-read-the-source">Is it free, and can I read the source?</h2>
|
||||
<p>Yes to both. The software is free, has no paid tier and no upsell, and is
|
||||
published under the MIT licence at
|
||||
<a href="https://github.com/SecureBitChat/securebit-chat">github.com/SecureBitChat/securebit-chat</a>.
|
||||
Nothing is minified beyond recognition or hidden behind a build you cannot
|
||||
reproduce; <a href="/docs/configuration/">CONFIGURATION.md</a> describes building and running it
|
||||
yourself.</p>
|
||||
<h2 id="do-i-have-to-install-anything">Do I have to install anything?</h2>
|
||||
<p>No. It runs in any current browser with WebRTC and the Web Crypto API. It can be
|
||||
installed as a Progressive Web App if you would rather have an icon, and there are
|
||||
native desktop builds for Windows, macOS and Linux. Calls are not yet available on
|
||||
the Linux desktop build; they work everywhere else, including in the browser.</p>
|
||||
<h2 id="can-the-person-i-am-talking-to-see-my-ip-address">Can the person I am talking to see my IP address?</h2>
|
||||
<p>On a default connection, yes — a direct WebRTC path means each side learns the
|
||||
other's address. That is how a direct connection works, not a flaw in this
|
||||
implementation.</p>
|
||||
<p>Relay-only mode prevents it: it sets <code>iceTransportPolicy: "relay"</code>, so all traffic
|
||||
goes through a TURN server and the peers see only the relay. The trade is that the
|
||||
relay operator can then see both addresses and the timing of the traffic, though
|
||||
never the content. Choose according to who you are protecting against.</p>
|
||||
<h2 id="do-i-need-my-own-turn-server">Do I need my own TURN server?</h2>
|
||||
<p>Only for relay-only mode, or when a direct path cannot be established at all.
|
||||
Public STUN defaults are built in and are enough for most connections. You can
|
||||
paste your own STUN and TURN servers under Advanced network settings; they are
|
||||
validated against an allowlist of schemes, hosts and transports before they reach
|
||||
the connection.</p>
|
||||
<p>One thing worth knowing: any TURN credential sent to a browser is public by
|
||||
definition, because the browser has to read it. Apply quotas on the TURN server
|
||||
rather than relying on the credential staying secret. <a href="/docs/configuration/">CONFIGURATION.md</a>
|
||||
covers the setup and how to verify it works.</p>
|
||||
<h2 id="can-i-send-files-and-how-large">Can I send files, and how large?</h2>
|
||||
<p>Yes, over the same encrypted channel, with a per-file AES-GCM key and a SHA-256
|
||||
integrity check. The receiver sees name, size and type and has to accept before
|
||||
any buffer is allocated or a single chunk is sent.</p>
|
||||
<p>The ceiling is 100 MB per file, with lower limits by category: images 25 MB, PDFs
|
||||
50 MB, text 10 MB, archives 100 MB, audio 20 MB. Executable and scriptable formats
|
||||
are blocked outright — <code>.exe</code>, <code>.js</code>, <code>.html</code>, <code>.svg</code> and others — and the file
|
||||
extension, not the browser-supplied MIME type, is the boundary that decides.</p>
|
||||
<h2 id="are-group-chats-and-calls-supported">Are group chats and calls supported?</h2>
|
||||
<p>Yes. Encrypted voice and video calls shipped in early 2026, and group
|
||||
communications are current work. Group sessions keep the same properties as
|
||||
one-to-one ones: no server holds the conversation, and members verify each other.
|
||||
<a href="/docs/calls/">CALLS.md</a> covers codec choices and how the connection adapts to a poor
|
||||
network.</p>
|
||||
<h2 id="is-there-a-mobile-app">Is there a mobile app?</h2>
|
||||
<p>Not yet. The web application works in mobile browsers today and can be installed
|
||||
to the home screen as a PWA. Native iOS and Android builds are planned; the
|
||||
roadmap on the front page lists where they sit relative to the rest of the work.</p>
|
||||
<h2 id="what-does-it-not-protect-against">What does it not protect against?</h2>
|
||||
<p>Read <a href="/docs/use-policy/">USE-POLICY.md</a> before relying on this for anything that
|
||||
matters. In short: a compromised device sees your messages exactly as you do, and
|
||||
no transport encryption helps. The person you are talking to can screenshot or
|
||||
repeat what you said. A safety code you skipped protects nothing. And someone
|
||||
watching your network can see that you are using a WebRTC application, even though
|
||||
they cannot see what you send — the software does not hide its own use.</p>
|
||||
|
||||
<nav class="more">
|
||||
<h2>More documentation</h2>
|
||||
<ul>
|
||||
<li><a href="/docs/">Documentation</a></li>
|
||||
<li><a href="/docs/architecture/">Architecture</a></li>
|
||||
<li><a href="/docs/cryptography/">Cryptography</a></li>
|
||||
<li><a href="/docs/descriptor-sbq2/">SBQ2 — connection descriptor v2</a></li>
|
||||
<li><a href="/docs/configuration/">Configuration</a></li>
|
||||
<li><a href="/docs/calls/">Voice and video calls</a></li>
|
||||
<li><a href="/docs/api/">API Notes</a></li>
|
||||
<li><a href="/docs/contributing/">Contributing</a></li>
|
||||
<li><a href="/docs/use-policy/">Use policy and limitations</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- Generated by scripts/build-docs.js from doc/*.md. Edits here are overwritten;
|
||||
change the Markdown instead. -->
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; upgrade-insecure-requests;">
|
||||
<meta http-equiv="X-Content-Type-Options" content="nosniff">
|
||||
<meta http-equiv="Referrer-Policy" content="strict-origin-when-cross-origin">
|
||||
<title>Documentation - SecureBit.chat</title>
|
||||
<meta name="description" content="Technical documentation for SecureBit.chat. Start with the project README if you are looking for an overview or a quick start.">
|
||||
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1">
|
||||
<link rel="canonical" href="https://securebit.chat/docs/">
|
||||
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico">
|
||||
<meta property="og:site_name" content="SecureBit.chat">
|
||||
<meta property="og:title" content="Documentation - SecureBit.chat">
|
||||
<meta property="og:description" content="Technical documentation for SecureBit.chat. Start with the project README if you are looking for an overview or a quick start.">
|
||||
<meta property="og:url" content="https://securebit.chat/docs/">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:locale" content="en_US">
|
||||
<meta property="og:image" content="https://securebit.chat/assets/social-card.png">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Documentation - SecureBit.chat">
|
||||
<meta name="twitter:description" content="Technical documentation for SecureBit.chat. Start with the project README if you are looking for an overview or a quick start.">
|
||||
<meta name="twitter:image" content="https://securebit.chat/assets/social-card.png">
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "TechArticle",
|
||||
"headline": "Documentation",
|
||||
"description": "Technical documentation for SecureBit.chat. Start with the project README if you are looking for an overview or a quick start.",
|
||||
"url": "https://securebit.chat/docs/",
|
||||
"inLanguage": "en",
|
||||
"isPartOf": {
|
||||
"@type": "WebSite",
|
||||
"@id": "https://securebit.chat/#website"
|
||||
},
|
||||
"publisher": {
|
||||
"@id": "https://securebit.chat/#organization"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: #0f0f11;
|
||||
color: #d6d6dc;
|
||||
font-family: Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.68;
|
||||
}
|
||||
.wrap { max-width: 46rem; margin: 0 auto; padding: 28px 24px 90px; }
|
||||
.top {
|
||||
display: flex; flex-wrap: wrap; gap: 8px 18px; align-items: baseline;
|
||||
padding-bottom: 16px; margin-bottom: 40px;
|
||||
border-bottom: 1px solid rgba(255,255,255,.08);
|
||||
font-size: 13.5px;
|
||||
}
|
||||
.top a { color: #8a8a92; text-decoration: none; }
|
||||
.top a:hover, .top a:focus-visible { color: #f0892a; }
|
||||
.top .brand { color: #f0892a; font-weight: 700; letter-spacing: .04em; }
|
||||
.top .here { color: #d6d6dc; margin-inline-start: auto; }
|
||||
h1 { font-size: clamp(28px, 5vw, 36px); font-weight: 800; letter-spacing: -1px; line-height: 1.14; color: #f4f4f6; margin: 0 0 24px; }
|
||||
h2 { font-size: 22px; font-weight: 700; letter-spacing: -.4px; color: #f4f4f6; margin: 46px 0 12px; padding-top: 14px; border-top: 1px solid rgba(255,255,255,.07); }
|
||||
h3 { font-size: 17.5px; font-weight: 700; color: #e8e8eb; margin: 30px 0 8px; }
|
||||
h4 { font-size: 15.5px; font-weight: 700; color: #e8e8eb; margin: 22px 0 6px; }
|
||||
p, li { color: #a9a9b3; }
|
||||
p { margin: 0 0 16px; }
|
||||
ul, ol { padding-inline-start: 22px; margin: 0 0 16px; }
|
||||
li { margin: 5px 0; }
|
||||
a { color: #f0892a; text-underline-offset: 2px; }
|
||||
strong { color: #e8e8eb; }
|
||||
code { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: .88em; background: #17171c; border: 1px solid rgba(255,255,255,.07); border-radius: 4px; padding: 1px 5px; color: #e8e8eb; }
|
||||
pre { background: #0b0b0e; border: 1px solid rgba(255,255,255,.08); border-radius: 8px; padding: 14px 16px; overflow-x: auto; margin: 0 0 18px; }
|
||||
pre code { background: none; border: 0; padding: 0; font-size: 13px; line-height: 1.62; color: #c9c9d1; }
|
||||
.tablewrap { overflow-x: auto; margin: 0 0 20px; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 14.5px; min-width: 30rem; }
|
||||
th { text-align: start; color: #8a8a92; font-weight: 600; font-size: 12px; letter-spacing: .08em; text-transform: uppercase; padding: 0 14px 8px 0; border-bottom: 1px solid rgba(255,255,255,.12); }
|
||||
td { padding: 9px 14px 9px 0; border-bottom: 1px solid rgba(255,255,255,.06); vertical-align: top; color: #a9a9b3; }
|
||||
td:first-child, th:first-child { padding-inline-start: 0; }
|
||||
blockquote { margin: 0 0 18px; padding: 2px 0 2px 16px; border-inline-start: 3px solid rgba(240,137,42,.4); color: #8a8a92; }
|
||||
hr { border: 0; border-top: 1px solid rgba(255,255,255,.08); margin: 34px 0; }
|
||||
img { max-width: 100%; height: auto; }
|
||||
a:focus-visible { outline: 2px solid #f0892a; outline-offset: 2px; border-radius: 2px; }
|
||||
.more { margin-top: 64px; padding-top: 22px; border-top: 1px solid rgba(255,255,255,.08); }
|
||||
.more h2 { font-size: 13px; letter-spacing: .12em; text-transform: uppercase; color: #6b6b73; border: 0; margin: 0 0 12px; padding: 0; font-weight: 700; }
|
||||
.more ul { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: 4px 24px; }
|
||||
.more li { margin: 0; padding: 7px 0; border-bottom: 1px solid rgba(255,255,255,.05); font-size: 14.5px; }
|
||||
@media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<nav class="top">
|
||||
<a class="brand" href="/">SecureBit.chat</a>
|
||||
<a href="/docs/">Documentation</a>
|
||||
<a href="https://github.com/SecureBitChat/securebit-chat" rel="noopener">GitHub</a>
|
||||
<span class="here">Documentation</span>
|
||||
</nav>
|
||||
<h1>Documentation</h1>
|
||||
<p>Technical documentation for SecureBit.chat. Start with the project <a href="https://github.com/SecureBitChat/securebit-chat#readme">README</a>
|
||||
if you are looking for an overview or a quick start.</p>
|
||||
<div class="tablewrap"><table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Document</th>
|
||||
<th>What it covers</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td><a href="/docs/architecture/">ARCHITECTURE.md</a></td>
|
||||
<td>How a session is established, verified and torn down, and where each guarantee comes from</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="/docs/cryptography/">CRYPTOGRAPHY.md</a></td>
|
||||
<td>Key schedule, the Double Ratchet, SAS verification, memory handling</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="/docs/descriptor-sbq2/">DESCRIPTOR-SBQ2.md</a></td>
|
||||
<td>The invitation format: wire layout, why it is small, the in-band key exchange, decoder rules</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="/docs/configuration/">CONFIGURATION.md</a></td>
|
||||
<td>Deployment, ICE and TURN setup, privacy modes, file transfer policy</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="/docs/calls/">CALLS.md</a></td>
|
||||
<td>Voice and video: codec choices, adaptation, and why each value was picked</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="/docs/api/">API.md</a></td>
|
||||
<td>Internal interfaces of the WebRTC manager and file transfer system</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="/docs/contributing/">CONTRIBUTING.md</a></td>
|
||||
<td>Development workflow and what needs extra review</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="/docs/use-policy/">USE-POLICY.md</a></td>
|
||||
<td>Terms of use, intended use, and the limits of what the software can protect</td>
|
||||
</tr>
|
||||
</tbody></table></div>
|
||||
<p>Security policy and vulnerability reporting live in <a href="https://github.com/SecureBitChat/securebit-chat/blob/main/SECURITY.md">SECURITY.md</a> at
|
||||
the repository root, where GitHub expects to find them.</p>
|
||||
<h2 id="keeping-this-accurate">Keeping this accurate</h2>
|
||||
<p>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. <a href="/docs/contributing/">CONTRIBUTING.md</a> lists which documents
|
||||
are affected by which areas of the code.</p>
|
||||
|
||||
<nav class="more">
|
||||
<h2>More documentation</h2>
|
||||
<ul>
|
||||
<li><a href="/docs/faq/">Frequently asked questions</a></li>
|
||||
<li><a href="/docs/architecture/">Architecture</a></li>
|
||||
<li><a href="/docs/cryptography/">Cryptography</a></li>
|
||||
<li><a href="/docs/descriptor-sbq2/">SBQ2 — connection descriptor v2</a></li>
|
||||
<li><a href="/docs/configuration/">Configuration</a></li>
|
||||
<li><a href="/docs/calls/">Voice and video calls</a></li>
|
||||
<li><a href="/docs/api/">API Notes</a></li>
|
||||
<li><a href="/docs/contributing/">Contributing</a></li>
|
||||
<li><a href="/docs/use-policy/">Use policy and limitations</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,185 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- Generated by scripts/build-docs.js from doc/*.md. Edits here are overwritten;
|
||||
change the Markdown instead. -->
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; upgrade-insecure-requests;">
|
||||
<meta http-equiv="X-Content-Type-Options" content="nosniff">
|
||||
<meta http-equiv="Referrer-Policy" content="strict-origin-when-cross-origin">
|
||||
<title>Use policy and limitations - SecureBit.chat</title>
|
||||
<meta name="description" content="SecureBit.chat is open-source software for private communication, research and education. It is provided as is, without warranty of any kind. This…">
|
||||
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1">
|
||||
<link rel="canonical" href="https://securebit.chat/docs/use-policy/">
|
||||
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico">
|
||||
<meta property="og:site_name" content="SecureBit.chat">
|
||||
<meta property="og:title" content="Use policy and limitations - SecureBit.chat">
|
||||
<meta property="og:description" content="SecureBit.chat is open-source software for private communication, research and education. It is provided as is, without warranty of any kind. This…">
|
||||
<meta property="og:url" content="https://securebit.chat/docs/use-policy/">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:locale" content="en_US">
|
||||
<meta property="og:image" content="https://securebit.chat/assets/social-card.png">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Use policy and limitations - SecureBit.chat">
|
||||
<meta name="twitter:description" content="SecureBit.chat is open-source software for private communication, research and education. It is provided as is, without warranty of any kind. This…">
|
||||
<meta name="twitter:image" content="https://securebit.chat/assets/social-card.png">
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "TechArticle",
|
||||
"headline": "Use policy and limitations",
|
||||
"description": "SecureBit.chat is open-source software for private communication, research and education. It is provided as is, without warranty of any kind. This…",
|
||||
"url": "https://securebit.chat/docs/use-policy/",
|
||||
"inLanguage": "en",
|
||||
"isPartOf": {
|
||||
"@type": "WebSite",
|
||||
"@id": "https://securebit.chat/#website"
|
||||
},
|
||||
"publisher": {
|
||||
"@id": "https://securebit.chat/#organization"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: #0f0f11;
|
||||
color: #d6d6dc;
|
||||
font-family: Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.68;
|
||||
}
|
||||
.wrap { max-width: 46rem; margin: 0 auto; padding: 28px 24px 90px; }
|
||||
.top {
|
||||
display: flex; flex-wrap: wrap; gap: 8px 18px; align-items: baseline;
|
||||
padding-bottom: 16px; margin-bottom: 40px;
|
||||
border-bottom: 1px solid rgba(255,255,255,.08);
|
||||
font-size: 13.5px;
|
||||
}
|
||||
.top a { color: #8a8a92; text-decoration: none; }
|
||||
.top a:hover, .top a:focus-visible { color: #f0892a; }
|
||||
.top .brand { color: #f0892a; font-weight: 700; letter-spacing: .04em; }
|
||||
.top .here { color: #d6d6dc; margin-inline-start: auto; }
|
||||
h1 { font-size: clamp(28px, 5vw, 36px); font-weight: 800; letter-spacing: -1px; line-height: 1.14; color: #f4f4f6; margin: 0 0 24px; }
|
||||
h2 { font-size: 22px; font-weight: 700; letter-spacing: -.4px; color: #f4f4f6; margin: 46px 0 12px; padding-top: 14px; border-top: 1px solid rgba(255,255,255,.07); }
|
||||
h3 { font-size: 17.5px; font-weight: 700; color: #e8e8eb; margin: 30px 0 8px; }
|
||||
h4 { font-size: 15.5px; font-weight: 700; color: #e8e8eb; margin: 22px 0 6px; }
|
||||
p, li { color: #a9a9b3; }
|
||||
p { margin: 0 0 16px; }
|
||||
ul, ol { padding-inline-start: 22px; margin: 0 0 16px; }
|
||||
li { margin: 5px 0; }
|
||||
a { color: #f0892a; text-underline-offset: 2px; }
|
||||
strong { color: #e8e8eb; }
|
||||
code { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: .88em; background: #17171c; border: 1px solid rgba(255,255,255,.07); border-radius: 4px; padding: 1px 5px; color: #e8e8eb; }
|
||||
pre { background: #0b0b0e; border: 1px solid rgba(255,255,255,.08); border-radius: 8px; padding: 14px 16px; overflow-x: auto; margin: 0 0 18px; }
|
||||
pre code { background: none; border: 0; padding: 0; font-size: 13px; line-height: 1.62; color: #c9c9d1; }
|
||||
.tablewrap { overflow-x: auto; margin: 0 0 20px; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 14.5px; min-width: 30rem; }
|
||||
th { text-align: start; color: #8a8a92; font-weight: 600; font-size: 12px; letter-spacing: .08em; text-transform: uppercase; padding: 0 14px 8px 0; border-bottom: 1px solid rgba(255,255,255,.12); }
|
||||
td { padding: 9px 14px 9px 0; border-bottom: 1px solid rgba(255,255,255,.06); vertical-align: top; color: #a9a9b3; }
|
||||
td:first-child, th:first-child { padding-inline-start: 0; }
|
||||
blockquote { margin: 0 0 18px; padding: 2px 0 2px 16px; border-inline-start: 3px solid rgba(240,137,42,.4); color: #8a8a92; }
|
||||
hr { border: 0; border-top: 1px solid rgba(255,255,255,.08); margin: 34px 0; }
|
||||
img { max-width: 100%; height: auto; }
|
||||
a:focus-visible { outline: 2px solid #f0892a; outline-offset: 2px; border-radius: 2px; }
|
||||
.more { margin-top: 64px; padding-top: 22px; border-top: 1px solid rgba(255,255,255,.08); }
|
||||
.more h2 { font-size: 13px; letter-spacing: .12em; text-transform: uppercase; color: #6b6b73; border: 0; margin: 0 0 12px; padding: 0; font-weight: 700; }
|
||||
.more ul { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: 4px 24px; }
|
||||
.more li { margin: 0; padding: 7px 0; border-bottom: 1px solid rgba(255,255,255,.05); font-size: 14.5px; }
|
||||
@media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<nav class="top">
|
||||
<a class="brand" href="/">SecureBit.chat</a>
|
||||
<a href="/docs/">Documentation</a>
|
||||
<a href="https://github.com/SecureBitChat/securebit-chat" rel="noopener">GitHub</a>
|
||||
<span class="here">Use policy and limitations</span>
|
||||
</nav>
|
||||
<h1>Use policy and limitations</h1>
|
||||
<p>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.</p>
|
||||
<h2 id="what-it-protects">What it protects</h2>
|
||||
<p>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.</p>
|
||||
<h2 id="what-it-does-not-protect">What it does not protect</h2>
|
||||
<p>Be clear about these before relying on the software for anything that matters.</p>
|
||||
<p><strong>A compromised device.</strong> 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.</p>
|
||||
<p><strong>The person you are talking to.</strong> 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.</p>
|
||||
<p><strong>Verification you skipped.</strong> 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.</p>
|
||||
<p><strong>Metadata, depending on your setup.</strong> 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.</p>
|
||||
<p><strong>The fact that you are using it.</strong> Someone watching your network can see a
|
||||
WebRTC connection and can see you loading this application. The software does not
|
||||
hide its own use.</p>
|
||||
<h2 id="your-responsibilities">Your responsibilities</h2>
|
||||
<ul>
|
||||
<li>Comply with the law where you are and with any policies that apply to you</li>
|
||||
<li>Keep your device and browser current and under your control</li>
|
||||
<li>Compare the safety code out of band, every time, on every new session</li>
|
||||
<li>Configure TURN correctly if you need relay-only mode, and verify it works</li>
|
||||
<li>Understand that endpoint compromise defeats everything above the endpoint</li>
|
||||
</ul>
|
||||
<h2 id="intended-use">Intended use</h2>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<p>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 <a href="https://github.com/SecureBitChat/securebit-chat/blob/main/SECURITY.md">SECURITY.md</a>.</p>
|
||||
<h2 id="operational-notes">Operational notes</h2>
|
||||
<p>If your threat model is serious, the software is only one part of it.</p>
|
||||
<p>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.</p>
|
||||
<p>Know the law where you are. Encryption is regulated differently in different
|
||||
places, and in some jurisdictions there are disclosure requirements attached to
|
||||
it.</p>
|
||||
<h2 id="contributing">Contributing</h2>
|
||||
<p>Contributions are welcome under the same expectations. Report vulnerabilities
|
||||
through the process in <a href="https://github.com/SecureBitChat/securebit-chat/blob/main/SECURITY.md">SECURITY.md</a> 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.</p>
|
||||
<p>See <a href="/docs/contributing/">CONTRIBUTING.md</a> for the development workflow.</p>
|
||||
|
||||
<nav class="more">
|
||||
<h2>More documentation</h2>
|
||||
<ul>
|
||||
<li><a href="/docs/">Documentation</a></li>
|
||||
<li><a href="/docs/faq/">Frequently asked questions</a></li>
|
||||
<li><a href="/docs/architecture/">Architecture</a></li>
|
||||
<li><a href="/docs/cryptography/">Cryptography</a></li>
|
||||
<li><a href="/docs/descriptor-sbq2/">SBQ2 — connection descriptor v2</a></li>
|
||||
<li><a href="/docs/configuration/">Configuration</a></li>
|
||||
<li><a href="/docs/calls/">Voice and video calls</a></li>
|
||||
<li><a href="/docs/api/">API Notes</a></li>
|
||||
<li><a href="/docs/contributing/">Contributing</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user