Protocol v1.0 February 2026 AGPL-3.0

Protocol Specification

A complete description of NeverSaid's cryptographic protocol — a simplified X3DH key agreement with Double Ratchet message encryption and HMAC-only authentication. Designed for offline deniability against an adversary who is a participant in the conversation.

§1 — Threat Model

Adversary definition

The adversary is a participant in the chat who has the NeverSaid extension installed. They can decrypt every message in real time, inspect protocol state from their side, and attempt to produce evidence of authorship for a third party (a court, a journalist, an employer). They may also collude with Microsoft to correlate ciphertext blobs with metadata.

Design principle

The goal is not to prevent the adversary from reading messages — they are a legitimate participant. The goal is to ensure they cannot produce cryptographic proof of authorship that a third party can independently verify.

Guarantees and limits

✓ Content confidentiality ✓ Offline deniability ✓ Forward secrecy ✓ Post-compromise security ✗ Metadata protection ✗ Online deniability ✗ Screenshot prevention

Content confidentiality: Messages are encrypted with AES-256-GCM using keys derived from X25519 Diffie-Hellman. Microsoft observes only opaque ciphertext.

Offline deniability: After a conversation ends, a simulator with access only to public keys can fabricate a transcript indistinguishable from a real one. No digital signatures exist in the message flow. Authentication uses HMAC from shared symmetric keys — both parties possess the key and either could have authored any message.

Forward secrecy: The DH ratchet ensures that compromising current key material does not expose past messages. Old chain keys are deleted after use.

Metadata is not protected. Microsoft retains full metadata: who communicates with whom, when, message sizes, typing indicators, read receipts, presence, and device information. NeverSaid operates at the content layer only.

Cannot mitigate

Screenshots, screen recordings, physical observation, memory dumps via DevTools, compromised operating systems, and enterprise admin policies (Intune extension blocking) are outside the protocol's scope. Cryptography protects bits, not eyeballs.


§2 — Key Types

Key Algorithm Purpose Lifetime
Identity Key (IK) X25519 Long-term DH identity Years
Signing Key Ed25519 Signs prekey bundles only Same as IK
Signed PreKey (SPK) X25519 Medium-term DH prekey Monthly rotation
One-Time PreKey (OPK) X25519 Single-use forward secrecy Consumed on use
Ephemeral Key (EK) X25519 Per-session DH ratchet seed Single use
Ratchet Key X25519 DH ratchet step Per ratchet turn
Note on Ed25519

The Ed25519 signing key is used only to sign prekey bundles during distribution — proving a prekey was published by the identity holder. It is never used to sign messages. This is the critical design choice that enables deniability.


§3 — Session Establishment

Session establishment uses a simplified X3DH (Extended Triple Diffie-Hellman) key agreement. Alice initiates a session with Bob, who may be offline. Alice fetches Bob's prekey bundle from the key server and computes a shared secret without any interactive round trips.

DH computations

Alice                                                    Key Server                Bob

                                                                                  
  ──── GET /api/v1/prekeys/bob@company.com ──────────►                       
  ◄─── { IK_B, SPK_B, SPK_sig, OPK_B } ────────────                       
                                                                                  
    verify Ed25519(SPK_sig, IK_B.signing_key)                                      
    generate ephemeral EK_A                                                        
                                                                                  
    DH1 = X25519(IK_A.priv,  SPK_B.pub)                                           
    DH2 = X25519(EK_A.priv,  IK_B.pub)                                            
    DH3 = X25519(EK_A.priv,  SPK_B.pub)                                           
    DH4 = X25519(EK_A.priv,  OPK_B.pub)  (if available)                          
                                                                                  
    SK = HKDF(DH1‖DH2‖DH3‖DH4)                                                   
                                                                                  
  ──── PreKeyMessage { IK_A, EK_A, SPK_id, OPK_id, ... } ──────────────────────►
                                                              Bob recomputes SK  
                                                              from his private   
                                                              keys + Alice's     
                                                              public values      

The shared secret SK is derived via HKDF:

Key derivation
SK = HKDF-SHA256(
    salt  = 0x00 * 32,
    ikm   = DH1  DH2  DH3  DH4,
    info  = "NeverSaid_v1",
    len   = 32
)

// Initial chain keys
sending_chain   = HKDF(SK, "ns_send", len=32)
receiving_chain = HKDF(SK, "ns_recv", len=32)

If no one-time prekey is available (OPK pool exhausted), DH4 is omitted. The protocol remains secure but loses one layer of forward secrecy for the initial message. The key server should alert clients when OPK supplies run low.

Deniability property

No signatures appear anywhere in the X3DH flow. The shared secret SK is computable by both Alice and Bob. A third-party simulator with knowledge of both public identity keys can fabricate all ephemeral DH values and produce a session transcript that is computationally indistinguishable from a real one. This provides offline deniability as defined by Di Raimondo, Gennaro, and Krawczyk (2005).

Why not online deniability?

Online deniability requires the simulator to forge transcripts while the session is in progress, without knowledge of private keys. Vatandas et al. (2020) proved this is impossible in the async X3DH setting. NeverSaid does not claim online deniability.


§4 — Double Ratchet

After session establishment, message keys are derived via two interlocking ratchets: a symmetric ratchet (KDF chain) for per-message keys, and a DH ratchet for periodic forward secrecy renewal.

Symmetric ratchet (KDF chain)

Each message derives a unique key from the current chain key. After derivation, the old chain key is replaced and the message key is deleted after encryption.

Chain key advancement
message_key    = HMAC-SHA256(chain_key, 0x01)
next_chain_key = HMAC-SHA256(chain_key, 0x02)

// chain_key is replaced with next_chain_key
// message_key is deleted after encryption
// old chain_key is destroyed

This provides forward secrecy within a chain: if the current chain key is compromised at step N, messages 0 through N−1 remain secure because their message keys were derived from prior chain states that no longer exist.

DH ratchet (inter-chain forward secrecy)

When a party sends a message, they include a fresh X25519 ephemeral public key. When the other party responds, they perform DH with this new key and mix the result into the root key, creating a new sending chain.

DH ratchet step
// Bob receives Alice's new ratchet public key
dh_secret = X25519(bob_ratchet.priv, alice_new_ratchet.pub)

// Derive new root key and chain key
root_key, recv_chain = HKDF(
    salt = root_key,
    ikm  = dh_secret,
    info = "ns_ratchet",
    len  = 64   // split: first 32 = root, second 32 = chain
)

// Bob generates his own new ratchet keypair for reply
bob_ratchet = X25519.generateKeyPair()
_, send_chain = HKDF(
    salt = root_key,
    ikm  = X25519(bob_ratchet.priv, alice_new_ratchet.pub),
    info = "ns_ratchet",
    len  = 64
)

// Old root key and old ratchet private keys are destroyed

This provides post-compromise security: after one DH ratchet step, a previously compromised session self-heals because the adversary cannot compute the new DH secret without the fresh ephemeral private key (which was generated after the compromise).


§5 — Message Encryption

Each message is encrypted with AES-256-GCM using the derived message key. The GCM authentication tag provides integrity — but because the key is derived from a shared DH secret, both parties can produce valid ciphertexts, preserving deniability.

Encryption
nonce = HKDF(message_key, "ns_nonce", len=12)

associated_data = version  sender_identity_key  recipient_identity_key

ciphertext = AES-256-GCM(
    key   = message_key,
    nonce = nonce,
    pt    = pad(plaintext),
    aad   = associated_data
)

// message_key deleted immediately after encryption

Message padding

All plaintext is padded to the nearest 256-byte boundary before encryption to resist traffic analysis via message length correlation. Padding uses PKCS#7: append N bytes each with value N, where N is the number of bytes needed to reach the next 256-byte boundary. Minimum padding is 1 byte; maximum is 256 bytes.

Padding
function pad(plaintext) {
    const block = 256
    const padLen = block - (plaintext.length % block) || block
    return concat(plaintext, new Uint8Array(padLen).fill(padLen))
}

§6 — Wire Format

PreKeyMessage (session initiation)

Sent when Alice initiates a new session with Bob.

Type 0x00 — PreKeyMessage
version uint8 Protocol version (0x01)
type uint8 0x00 for PreKeyMessage
sender_identity_key bytes[32] Sender's X25519 public identity key
sender_ephemeral_key bytes[32] Freshly generated X25519 ephemeral key
recipient_spk_id uint32 ID of recipient's signed prekey used
recipient_opk_id uint32 ID of one-time prekey (0xFFFFFFFF if none)
sender_ratchet_key bytes[32] Initial DH ratchet public key
counter uint32 Message counter (0 for first message)
prev_counter uint32 Previous chain's final counter
ciphertext bytes[] AES-256-GCM output (includes 16-byte tag)

Message (normal)

All subsequent messages within an established session.

Type 0x01 — Message
version uint8 Protocol version (0x01)
type uint8 0x01 for Message
sender_ratchet_key bytes[32] Current DH ratchet public key
counter uint32 Message counter in current chain
prev_counter uint32 Last counter in previous sending chain
ciphertext bytes[] AES-256-GCM output (includes 16-byte tag)

Teams envelope

The serialized message is base64url-encoded and wrapped in a recognizable envelope that is pasted into the Teams compose box:

Envelope format
🔒 This message is encrypted with NeverSaid.
━━━━━━━━━━━━━━━━━━━━
neversaid://v1/<base64url-encoded-message>
━━━━━━━━━━━━━━━━━━━━
Get NeverSaid → neversaid.eu

The neversaid://v1/ prefix allows the extension's MutationObserver to efficiently identify encrypted messages in the DOM without false positives. The human-readable wrapper instructs non-extension users how to decrypt.


§7 — Key Server

The key server at keys.neversaid.eu provides key discovery and prekey distribution. It never sees plaintext messages. It is a minimal REST API backed by SQLite and authenticated via email verification.

Endpoint Method Purpose
/api/v1/keys POST Publish identity key + prekey bundle
/api/v1/keys/verify POST Request email verification
/api/v1/keys/by-email/:email GET Lookup key bundle by email
/api/v1/prekeys/:email GET Fetch prekeys (consumes one OPK)
/api/v1/keys/:fingerprint PUT Rotate keys (signed by IK)
/api/v1/keys/:fingerprint DELETE Revoke key (signed by IK)

Key fingerprints are JWK Thumbprints (RFC 7638) using SHA-256, base64url-encoded. One-time prekeys are consumed on fetch: the server returns one OPK and marks it as used. Clients should maintain a pool of 50 OPKs and replenish when the server indicates the supply is low (via an X-OPK-Remaining response header).

Endpoint Rate limit
/api/v1/keys/by-email 60 req/min per IP
/api/v1/prekeys 120 req/min per IP
/api/v1/keys (POST) 5 req/hour per IP
/api/v1/keys/verify 3 emails/hour per address

§8 — Trust Model

NeverSaid uses enhanced Trust-On-First-Use (TOFU) with optional out-of-band verification, following a three-tier model designed for progressive trust.

Tier Mechanism Behaviour on key change
Tier 1 — TOFU First key seen for email, post email-verification Advisory notice (non-blocking)
Tier 2 — Safety code 60-digit numeric code verified out-of-band Blocking alert, re-verification required
Tier 3 — Log Append-only transparency log (future v2) Cryptographic proof of key history

Safety code derivation: The 60-digit numeric code is derived from both parties' identity keys and presented as 12 groups of 5 digits for readability.

Safety code
hash = HKDF-SHA256(
    ikm  = sort(IK_A.pub  IK_B.pub),  // deterministic ordering
    salt = 0x00 * 32,
    info = "ns_safety_code",
    len  = 30
)

// Convert to decimal digits, format as 12 groups of 5
code = "38291 04827 19384 50172 ..."

§9 — Group Chats

Group encryption uses a hybrid approach based on group size, following the principle that deniable authentication in groups inherently requires pairwise operations.

Group size Approach Deniability PCS
2–10 Pairwise Double Ratchet Full Full
11–50 Sender Keys Within group On re-key only
50+ Not supported (v1)

Pairwise mode (≤ 10): The sender encrypts the message N−1 times, once per pairwise ratchet session. Each session has independent forward secrecy, post-compromise security, and deniability. The O(N) cost is negligible for small groups.

Sender Keys mode (11–50): Each member generates a Sender Key (symmetric chain key + X25519 authentication keypair) and distributes it to all others via pairwise channels. Messages are encrypted once with the Sender Key's derived message key. This sacrifices post-compromise security within a Sender Key epoch but scales to O(1) per message.

Member departure triggers re-keying: When a member leaves, all remaining members generate fresh Sender Keys and redistribute pairwise. The departed member cannot decrypt subsequent messages.

Collusion limit

If N−1 members of an N-person group collude, they can attribute messages to the remaining member by elimination. This is a fundamental limitation of group deniability and is not specific to NeverSaid.


§10 — Transcript Forgery Tool

NeverSaid ships a built-in transcript forgery tool that makes deniability concrete and demonstrable. The tool generates cryptographically valid conversation transcripts between any two public key holders with arbitrary message content.

The forgery process exploits the core deniability property: since session establishment uses only DH operations (no signatures), anyone with knowledge of two public identity keys can simulate a complete session.

Forgery algorithm
function forgeTranscript(IK_A_pub, IK_B_pub, messages[]) {
    // Generate fake ephemeral keys for session init
    fake_EK  = X25519.generateKeyPair()
    fake_SPK = X25519.generateKeyPair()
    fake_OPK = X25519.generateKeyPair()

    // Derive a plausible (but fabricated) shared secret
    fake_SK = HKDF(
        random(128),  // stand-in for DH1‖DH2‖DH3‖DH4
        "NeverSaid_v1"
    )

    // Run the ratchet forward, encrypting each fake message
    for (msg of messages) {
        ciphertext = encryptWithRatchet(fake_SK, msg.text)
        emit formatEnvelope(ciphertext, msg.timestamp)
    }

    // Output: valid neversaid://v1/... blobs
    // Indistinguishable from real transcripts
    // without knowledge of private keys
}

The resulting transcript is structurally identical to a real NeverSaid conversation. A forensic examiner cannot distinguish forged ciphertext from authentic ciphertext without access to the participants' private identity keys — which neither the adversary nor the examiner possesses.

Why ship this?

If anyone can trivially produce "evidence" of any conversation, then evidence of a conversation proves nothing. The forgery tool transforms deniability from an abstract mathematical property into a demonstrable, courtroom-ready argument.


§11 — Honest Limitations

Transparency is a security property. NeverSaid's documentation and settings page explicitly communicate what the protocol cannot do.

Attack vector Mitigated? Notes
Message content interception ✓ Yes AES-256-GCM with per-message keys
Authorship attribution ✓ Yes HMAC-only auth, offline deniability
Retroactive decryption ✓ Yes Forward secrecy via DH ratchet
Key compromise recovery ✓ Yes Post-compromise security via DH ratchet
Metadata (who, when, size) ✗ No Microsoft retains full metadata
Screenshots / recording ✗ No Outside cryptographic scope
Browser memory inspection ✗ No DevTools can read decrypted content
Compromised OS / keylogger ✗ No Applies to all software encryption
Enterprise admin blocking ✗ No Intune can block extension by ID
Hardware attestation ✗ No SGX/TrustZone can defeat deniability
Design philosophy

NeverSaid protects content at the cryptographic layer. It does not attempt to solve metadata leakage (use Tor), physical observation (use a private room), or compromised endpoints (use a trusted device). Each layer of defense has its own tools. NeverSaid is one layer, not all of them.