Guide · Security
Security model
What NulBridge protects, exactly how, and where its limits are. Every claim on this page describes the code that runs in your browser — including the parts that can’t protect you.
- Updated
- Protocol
- Version 2
- Reading time
- 10 minutes
- Audit
- Not yet audited
01Summary
At a glance
The technical short version. Each line is explained further down the page.
| Key exchange | ECDH on P-256, new key pairs every session |
|---|---|
| Key derivation | HKDF-SHA-256 over the ECDH secret plus the link secret or code |
| Encryption | AES-256-GCM, fresh random 96-bit IV per frame |
| Integrity | 128-bit GCM tags; sequence number and direction bound as associated data |
| Keys | One key per direction, held as non-extractable Web Crypto keys |
| Transport | WebRTC data channel, itself encrypted with DTLS |
| Calls | WebRTC DTLS-SRTP, not covered by the safety code |
| Verification | 48-bit safety code, shown as XXXX-XXXX-XXXX |
| Storage | None. Keys and messages live in tab memory only |
| Implementation | The browser’s Web Crypto API, no custom primitives |
02Protocol v2
The handshake, step by step
It runs inside the WebRTC data channel as soon as the channel opens, and has 15 seconds to finish. Four messages, in this order.
- Plain JSON, inside the data channel’s DTLS encryption
- Encrypted with the new session keys (AES-256-GCM)
- 01
Guest to Host
The guest says hello
Plain JSON inside the data channel, which WebRTC already encrypts with DTLS: protocol version 2, role, mode, a fresh 65-byte P-256 public key and a 16-byte random nonce, both in base64url.
hello · guest to host
{"t":"hello","v":2,"role":"guest","mode":"link","pub":"BCu4…","nonce":"q9Lm…"} - 02
Host to Guest
The host checks, derives and replies
It refuses anything unexpected (another version, the wrong mode, missing or extra fields, a key that isn’t on the curve) with arejectmessage, then closes the connection. Otherwise it builds the transcript, derives the session keys and sends its own hello with its public key and a fresh nonce. - 03
Guest to Host
The guest proves it holds the secret
It runs the same checks on the host’s hello, derives the same keys and sends “ready”: the first encrypted frame, sequence number 0, under the guest-to-host key. Only a browser with the right secret can produce it. - 04
Host to Guest
The host confirms
Only once the guest’s ready decrypts. If it doesn’t, the host answersreject: invalid. If another guest got in first, it answersreject: busy. Otherwise it sends its own ready under the host-to-guest key. - 05
Both browsers
Session established
The guest decrypts the host’s ready, and both screens show the safety code. If any step fails, or 15 seconds pass first, the attempt ends. A waiting host keeps waiting for the right guest.
Where the keys come from
Each browser runs the same key schedule on its own. The transcript joins the label, the mode, both public keys and both nonces exactly as they were sent, so any change to the handshake changes every key.
Input 1 · 32 bytes
ECDH shared secret
Input 2 · 32 bytes
Pre-shared key
nulbridge/v2/code/<code>.Salt
Transcript hash
nulbridge/v2|mode|hostPub|guestPub|hostNonce|guestNonceKey derivation
HKDF-SHA-256
Bytes 0–31 of 64
Host-to-guest key
nulbridge/v2/keys. The host can only encrypt with it, the guest only decrypt.Bytes 32–63 of 64
Guest-to-host key
nulbridge/v2/keys output. Mirror image of the first.48 bits
Safety code
nulbridge/v2/safety, shown as 12 hex characters: XXXX-XXXX-XXXX.03Wire format
Every message, every chunk
After the handshake, everything travels as encrypted frames: messages, typing indicators, file chunks, call invitations and keep-alives.
Text frames
One JSON object per message. The sequence number travels in the clear; the direction and the number are bound to the tag as associated data.
Frame
{"t":"enc","s":42,"iv":"…","ct":"…"}Associated data
nb2|g2h|42Binary frames
File chunks of up to 64 KiB. The whole 25-byte header plus the direction (g2h or h2g) is the associated data.
| Byte 0 | Frame type, 0x02 |
|---|---|
| Bytes 1–4 | Sequence number (uint32) |
| Bytes 5–20 | Transfer ID (16 bytes) |
| Bytes 21–24 | Chunk index (uint32) |
| Bytes 25–36 | IV (12 random bytes) |
| Bytes 37 onward | Ciphertext and 16-byte tag |
A fresh IV for every frame
96 random bits from the browser’s secure random generator, so a repeated IV under the same key is vanishingly unlikely.
Strict order
Sequence numbers start at 0 and rise by one in each direction. A replayed, dropped, reordered or altered frame ends the session.
Direction bound in
Each direction has its own key, and the direction is part of the associated data, so a frame can’t be reflected back to its sender.
Bounded sizes
Text frames over 256 KiB, chunks over 64 KiB and malformed frames close the session.
Strict schemas
Every message type has a fixed shape. Unknown types and extra fields are dropped before the app sees them.
Careful with files
File names lose path separators, control characters and invisible direction marks. Only common image types keep their type, for previews; everything else downloads as plain data.
04Private links
Why private links are strong
Its secret is long, never transmitted, and bound into the keys. That makes a private link the strongest way to connect.
- 01
256 bits of randomness
32 bytes from
crypto.getRandomValues, written as 43 characters. Far beyond any guessing. - 02
Never sent to a server
It lives in the URL fragment, after the #. Browsers leave fragments out of every request, so the website, the signaling server and the code service never receive it. Only the room ID in the path reaches the website.
- 03
Bound into the keys
Without the secret, even a signaling server that tampers with the connection can’t finish the handshake. It can’t read your messages, and it can’t forge them.
- 04
Opened on purpose
Joining takes a click on Join session, so link previews and email scanners that open links on their own can’t take the guest’s place.
- 05
Done after one use
The join page removes the secret from the address bar. Once a guest is in, the host turns everyone else away, and when the session ends nothing answers the link.
WarningTreat a link like a key.
05Codes
6-digit codes and safety codes
A code is easy to read out, but short. Here is how it is protected, where that protection ends, and how the safety code covers the gap.
| One-time | Claimed atomically: deleted as it is read |
|---|---|
| Lifetime | Unusable after 10 minutes, then purged |
| Wrong guesses | 10 per network every 10 minutes |
| New codes | 20 per network every 10 minutes |
| Networks | Counted per IP address; IPv6 per /64 |
| Randomness | Uniform over 000000–999999, from a cryptographic generator |
| Stored IPs | Only as peppered SHA-256 hashes, deleted after about an hour |
That allows about 1,440 wrong guesses per network per day, against 1,000,000 possible codes that each live at most 10 minutes and work once.
WarningWhere a short code is weaker
The safety code closes the gap
Both browsers derive a 48-bit safety code from the handshake: both public keys, both nonces, the mode and the secret. Someone in the middle runs two separate handshakes, one with each of you, so your screens show different codes. To fool you, they would have to make two separate handshakes produce the same code, one of about 281 trillion, within the 15-second limit.
Your screen
4F2A-9C1B-77DE
Their screen
4F2A-9C1B-77DE
How to compare
- 01
Use a second channel
In person, on a phone call, or in a messenger you already trust. Never in the NulBridge chat itself: whoever sits in the middle could rewrite it. - 02
Read it aloud
One of you reads the 12 characters while the other checks them, for example 4F2A-9C1B-77DE. - 03
They match: confirm
Tap They match. Your header shows VERIFIED, and nobody is in the middle of this session. - 04
They differ: end it
End the session right away and start again, with a private link if you can.
06Keys over time
Forward secrecy
Recordings stay locked. Someone who captures your encrypted traffic today and gets hold of the link or code tomorrow still can’t read it.
- T0
Session starts
New ECDH key pairs are made in each browser. Their private halves can’t be exported.
- T1
Handshake
The shared secret becomes two session keys. Raw secret bytes are overwritten with zeros; the private keys and the link secret or code are let go.
- T2
Conversation
Only the two AES keys remain, held in the memory of the two tabs.
- T3
Tab closed
Everything is gone. No key that could decrypt a recording exists anywhere.
The link secret or the code alone is useless against a recording. The session keys also depend on the ECDH shared secret, and computing it takes one of the two private keys. Those never leave the browsers, and they are discarded as soon as the session is set up.
InfoOne key per direction, for the whole session.
07Threats
Threat model
What NulBridge protects against, and what it doesn’t. Read the notes: they matter as much as the verdicts.
- Yes
- Protected.
- If verifiedMitigatedPartial
- Protected only if you do your part, or the risk is reduced but not removed.
- No
- Not protected. Plan around it.
| Threat | Protected? | Notes |
|---|---|---|
| Someone watching the network: public Wi-Fi, your provider, a hacked router | Yes | Everything is encrypted twice, by WebRTC’s DTLS and by NulBridge’s AES-256-GCM layer, with keys they never see. |
| The servers involved, working as intended: website, signaling, STUN, code service, relay | Yes | They help the browsers find each other and see metadata, never keys or content. |
| A malicious signaling server, with a private link | Yes | Without the 256-bit secret it can’t complete the handshake: key confirmation fails and no session opens. |
| A malicious signaling server, with a 6-digit code | If verified | It could try every code against a captured handshake and sit in the middle. Different safety codes on your two screens expose it, so compare them. |
| Someone guessing your 6-digit code | Mitigated | One-time codes, a 10-minute life and 10 wrong tries per network per 10 minutes. If a guess ever lands, the person you expected has no matching safety code to read out. |
| Call interception by a malicious signaling server | Partial | Calls use WebRTC’s DTLS-SRTP. Its keys are vouched for by the signaling server, not by the safety code. Messages and files aren’t affected. |
| The person you’re talking to | No | They see everything you send, and can copy, record or forward it. |
| A compromised device or browser extension | No | Malware, or an extension with access to the page, can read the conversation on screen. |
| Altered code served by the website | No | Like every web app, NulBridge runs whatever code the site serves at that moment. A compromised host could ship code that leaks keys. |
| IP addresses and timing | No | The other person and the services involved see IP addresses and when you connect. A trusted VPN hides your address from them. |
Threat model
Someone watching the network: public Wi-Fi, your provider, a hacked router
Protected? Yes- Notes
- Everything is encrypted twice, by WebRTC’s DTLS and by NulBridge’s AES-256-GCM layer, with keys they never see.
The servers involved, working as intended: website, signaling, STUN, code service, relay
Protected? Yes- Notes
- They help the browsers find each other and see metadata, never keys or content.
A malicious signaling server, with a private link
Protected? Yes- Notes
- Without the 256-bit secret it can’t complete the handshake: key confirmation fails and no session opens.
A malicious signaling server, with a 6-digit code
Protected? If verified- Notes
- It could try every code against a captured handshake and sit in the middle. Different safety codes on your two screens expose it, so compare them.
Someone guessing your 6-digit code
Protected? Mitigated- Notes
- One-time codes, a 10-minute life and 10 wrong tries per network per 10 minutes. If a guess ever lands, the person you expected has no matching safety code to read out.
Call interception by a malicious signaling server
Protected? Partial- Notes
- Calls use WebRTC’s DTLS-SRTP. Its keys are vouched for by the signaling server, not by the safety code. Messages and files aren’t affected.
The person you’re talking to
Protected? No- Notes
- They see everything you send, and can copy, record or forward it.
A compromised device or browser extension
Protected? No- Notes
- Malware, or an extension with access to the page, can read the conversation on screen.
Altered code served by the website
Protected? No- Notes
- Like every web app, NulBridge runs whatever code the site serves at that moment. A compromised host could ship code that leaks keys.
IP addresses and timing
Protected? No- Notes
- The other person and the services involved see IP addresses and when you connect. A trusted VPN hides your address from them.
08Disclosure
Audits and reporting a vulnerability
Security claims are only worth what you can check. Here is where this implementation stands, and how to tell us when something is wrong.
Audit status
This implementation has not been independently audited yet.
If you find a vulnerability, please report it privately and give us a reasonable window to fix it before you disclose it. To report, contact the operator of this deployment with the steps to reproduce, the affected page or flow, and what an attacker could gain.
In scope: the web app, the session protocol (handshake, encrypted channel, file transfer, call signaling), the site’s security headers and the 6-digit code backend. Third-party services — the signaling server, STUN or TURN providers, Supabase and the hosting provider — have their own disclosure programs.
For how the protocol fits together end to end, read how NulBridge works.