Skip to content
  • P2P
  • E2EE
  • No accounts
  • Nothing stored
  • Free

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.

256Bit link secretIn the URL fragment, never sent.
96Bit IV per frameFresh and random, every frame.
48Bit safety codeCompare it to rule out interception.
15 sHandshake limitCounted from the moment the channel opens.
Session cryptography
Key exchangeECDH on P-256, new key pairs every session
Key derivationHKDF-SHA-256 over the ECDH secret plus the link secret or code
EncryptionAES-256-GCM, fresh random 96-bit IV per frame
Integrity128-bit GCM tags; sequence number and direction bound as associated data
KeysOne key per direction, held as non-extractable Web Crypto keys
TransportWebRTC data channel, itself encrypted with DTLS
CallsWebRTC DTLS-SRTP, not covered by the safety code
Verification48-bit safety code, shown as XXXX-XXXX-XXXX
StorageNone. Keys and messages live in tab memory only
ImplementationThe 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.

Fig. 01Handshake sequence
GUESThas the secretHOSTmade the secretDATA CHANNEL OPEN15 SECONDS TO FINISH01hellokey · nonce · modeCHECK + DERIVEversion, mode, keyor reject + close02hellokey · nonce · modeCHECK + DERIVEsame checks,same two keys03readyencrypted · seq 0VERIFY + ADMITdecrypt its readyone guest only04readyencrypted · seq 0VERIFYdecrypt its ready05 · SESSION ESTABLISHEDCOMPARE THE SAFETY CODE
  • Plain JSON, inside the data channel’s DTLS encryption
  • Encrypted with the new session keys (AES-256-GCM)
The guest speaks first. The host confirms only after verifying the guest’s ready message, so a guest that is turned away is told so and never sees a half-open session.
  1. 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…"}
  2. 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 a reject message, 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.
  3. 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.
  4. 04

    Host to Guest

    The host confirms

    Only once the guest’s ready decrypts. If it doesn’t, the host answers reject: invalid. If another guest got in first, it answers reject: busy. Otherwise it sends its own ready under the host-to-guest key.
  5. 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.

Fig. 02Key schedule

Input 1 · 32 bytes

ECDH shared secret

Computed on P-256 from your private key and their public key. Never sent.

Input 2 · 32 bytes

Pre-shared key

The link secret, or SHA-256 of nulbridge/v2/code/<code>.
Joined: input key material

Salt

Transcript hash

SHA-256 of nulbridge/v2|mode|hostPub|guestPub|hostNonce|guestNonce

Key derivation

HKDF-SHA-256

Both browsers run it separately and get identical results, unless the secret, a key or a nonce differs.
Expanded with two info strings

Bytes 0–31 of 64

Host-to-guest key

AES-256-GCM, from nulbridge/v2/keys. The host can only encrypt with it, the guest only decrypt.

Bytes 32–63 of 64

Guest-to-host key

AES-256-GCM, from the same nulbridge/v2/keys output. Mirror image of the first.

48 bits

Safety code

From nulbridge/v2/safety, shown as 12 hex characters: XXXX-XXXX-XXXX.
Both browsers get the same two AES keys and the same safety code only if they used the same secret and saw the same transcript, which is exactly what an attacker in the middle can’t arrange.

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|42

Binary frames

File chunks of up to 64 KiB. The whole 25-byte header plus the direction (g2h or h2g) is the associated data.

File chunk layout
Byte 0Frame type, 0x02
Bytes 1–4Sequence number (uint32)
Bytes 5–20Transfer ID (16 bytes)
Bytes 21–24Chunk index (uint32)
Bytes 25–36IV (12 random bytes)
Bytes 37 onwardCiphertext 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.

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.

How codes are protected
One-timeClaimed atomically: deleted as it is read
LifetimeUnusable after 10 minutes, then purged
Wrong guesses10 per network every 10 minutes
New codes20 per network every 10 minutes
NetworksCounted per IP address; IPv6 per /64
RandomnessUniform over 000000–999999, from a cryptographic generator
Stored IPsOnly 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

A million possible values is plenty against guessing through the code service. It is not enough against someone who controls the signaling server: within seconds, they could test every possible code against an intercepted handshake, then relay your session through themselves. Comparing safety codes exposes this. Private links don’t have this weakness.

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

Example: both screens show the same safety code, 4F2A-9C1B-77DE.

How to compare

  1. 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.
  2. 02

    Read it aloud

    One of you reads the 12 characters while the other checks them, for example 4F2A-9C1B-77DE.
  3. 03

    They match: confirm

    Tap They match. Your header shows VERIFIED, and nobody is in the middle of this session.
  4. 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.

  1. T0

    Session starts

    New ECDH key pairs are made in each browser. Their private halves can’t be exported.

  2. 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.

  3. T2

    Conversation

    Only the two AES keys remain, held in the memory of the two tabs.

  4. 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.

NulBridge doesn’t rotate keys within a session. Sessions are short by design, but if a device is compromised while a session is open, that session is exposed.

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 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.

It is built only on the browser’s standard Web Crypto API — no custom cryptographic primitives — and the protocol is covered by automated tests, including tampering, replay and wrong-key cases. That is not a substitute for an external review.

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.

Verify it yourself

Open a bridge.

Start a session, then compare the safety code with the other person. Free, no account, nothing stored.