Enkrypted Chat — Complete Protocol Specification
This document is an initial draft by Positive Intentions. Enkrypted Chat has not been independently audited. Do not rely on it for sensitive communications or production use.
Document status: Initial draft (informal v0.1) · Product: Enkrypted Chat · Organization: Positive Intentions
Document overview
Normative protocol specification for Enkrypted Chat: gap analysis, design rationales (P0), profile overview (P1–P8), and appendices. For the navigable multi-page version, see the Protocol spec section in the sidebar.
Protocol specification gap analysis
This analysis does not claim equivalence to Noise or the WireGuard paper.
Purpose
The Enkrypted Chat specification is a hybrid product + protocol document. This page tracks readiness for reviewers who need behavior without reading TypeScript.
Reference bar (updated)
| Spec element | Noise / WireGuard bar | Enkrypted Chat status |
|---|---|---|
| Scope | Single protocol | Met (split) — P0–P8 + appendices vs ch. 1–12; composition documented |
| Wire formats | Byte-level packets | Partial — JSON normative; 16 MiB cap; P4.13 full field reference; not binary |
| Crypto primitives | Named profile | Met — P1 |
| Processing rules | Pseudocode | Met — P6 |
| State machines | FSM + diagrams | Met — P7, P3 sequences |
| Security properties | Formal claims | Met — P1.4 Provides/Partial/Does-not-provide + ch. 9 |
| Limits / errors | Caps + codes | Met — P8; protocol-error implemented in p2p |
| Test vectors | Golden hex | Partial — V1–V7, V9 in Appendix A; live cascade V8 deferred |
| Rationales | Design why | Met — P0 |
| Signal deltas | N/A | Met — Appendix B |
| Versioning | Frozen id | Partial — EnkryptedChat-Profile-v0; v1 criteria in P1.7 |
| Change log | Rev history | Met — CHANGELOG-SPEC |
Structural difference (honest)
Enkrypted Chat is a composition, not one monolithic protocol:
Auditor checklist
| # | Question | Where answered |
|---|---|---|
| 1 | Session message order? | P3, P7 |
| 2 | Algorithms for a text message? | P1, P5 |
| 3 | JSON on the wire? | P4, P5 |
| 4 | Decrypt failure / oversize? | P6, P8 |
| 5 | What is not guaranteed? | P1.5, ch. 11 |
| 6 | Reproduce a round-trip? | Appendix A + repo tests |
| 7 | P2P vs standard Signal? | Appendix B |
Deferred (documented, not hidden)
- Live MLS+Signal+ML-KEM+AES hex (Appendix A V8, Profile-v1 gate).
- Mandatory
specVersionon all PDUs (Profile-v1 gate). - Binary wire profile (
EnkryptedChat-Profile-v2-binaryinformative only).
Protocol document map
| Chapter | Title |
|---|---|
| P0 | Design rationales |
| P1 | Protocol overview and profile |
| P2 | Signaling (broker path) |
| P3 | Session establishment |
| P4 | PDU catalog and schemas |
| P5 | Cascade wire encoding |
| P6 | Processing rules |
| P7 | State machines |
| P8 | Limits, errors, edge cases |
| Appendix A | Test vectors |
| Appendix B | P2P Signal deltas |
| Appendix C | Glossary |
| Appendix D | SFrame (informative WIP) |
P0. Design rationales
Informative design background for EnkryptedChat-Profile-v0. Normative behavior is in P1–P8.
P0.1 Purpose
This chapter explains why the protocol is shaped as it is. It does not replace normative rules in P1–P8.
P0.2 JSON application PDUs on WebRTC data channels
Choice: UTF-8 JSON objects with a string type discriminator on SCTP data channels.
Rationale:
- Browser-first stack:
JSON.stringify/parseintegrate cleanly with React state and debugging tools. - Inspectability during development: handshake and cascade envelopes can be logged (with secrets redacted) without a custom binary decoder.
- Single implementation: no interoperability requirement for a compact binary codec.
Trade-off: Larger on-wire size than a typed binary protocol (e.g. WireGuard packets). Mitigated by chunking and fast-file paths for bulk data.
Future: An informative EnkryptedChat-Profile-v2-binary MAY be defined later; v0/v1 documentation remains JSON-normative.
P0.3 Cascade order MLS → Signal → ML-KEM → AES
Choice: Four application layers applied in registration order inside CascadingCipherManager.
Rationale:
- MLS (RFC 9420): Group epoch semantics and future multi-device alignment; outermost for group-aware deployments.
- Signal: Battle-tested 1:1 forward secrecy and break-in recovery via Double Ratchet (Appendix B).
- ML-KEM-768: Post-quantum confidentiality layer on message content (not full-PQ transport).
- AES-GCM-256: Fast symmetric envelope keyed from peer-derived material; supports rotation suffix.
Trade-off: Defense-in-depth adds latency and complexity. Cascade is not a substitute for auditing Signal, MLS, or the browser delivery chain.
P0.4 No third-party client interoperability
Choice: One product (Enkrypted Chat) and one codebase family; no version negotiation with external clients.
Rationale: Rapid iteration, module federation, and tight coupling between p2p and glitr-chat. A public interop surface would require frozen schemas and conformance certification before it adds value.
P0.5 Broker-first signaling (PeerJS / WSS)
Choice: Default path uses an online PeerJS-compatible broker for SDP/ICE exchange.
Rationale:
- NAT traversal for browser peers without manual copy-paste in the common case.
- Clear trust boundary: broker is semi-trusted for connection setup, not for message plaintext (P2).
Trade-off: Broker compromise enables signaling MITM until channel binding exists. QR/manual SDP remains a product option (ch. 3) but is out of scope for the v0 protocol profile.
P0.6 Cleartext handshake material (known limitation)
Handshake PDUs (P3) carry key packages and Signal exchange payloads inside JSON on the data channel before encryptionReady. Confidentiality during this phase is partial (P1.4). Encrypting the handshake wrapper is future work.
P0.7 TOFU and optional verification
Peers are addressed by Peer ID on the broker. Cryptographic binding comes from in-band Signal/MLS handshakes, not a global PKI. High-risk deployments SHOULD use safety-number or QR verification (product UX) in addition to TOFU.
P0.8 Related documents
| Topic | Document |
|---|---|
| Normative profile | P1 |
| Signal deltas | Appendix B |
| Supply chain | ch. 8 |
| Change history | CHANGELOG-SPEC |
P1. Protocol overview and profile
Normative protocol documentation for the current implementation. Not independently audited.
P1.0 Notation
| Symbol / term | Meaning |
|---|---|
| Byte string | Uint8Array; on the wire as JSON array of integers 0–255 |
| Concatenation | A || B |
| Hex | Lowercase hex digits; test vectors omit 0x prefix |
| Text | UTF-8 unless noted |
IK, EK, … | Signal/MLS symbols per upstream specs when cited |
Keywords MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are interpreted per RFC 2119 in all P-chapters and appendices.
P1.1 Protocol identifier
This document defines profile:
EnkryptedChat-Profile-v0
Implementations claiming this profile MUST:
- Use WebRTC data channels as the transport (see P2, ch. 3).
- Use JSON UTF-8 objects as application PDUs on the data channel (see P4).
- When application E2EE is enabled, apply cascade layers in order MLS → Signal → ML-KEM → AES on plaintext (see P5).
- Follow processing rules in P6.
Alternate profiles (non-normative for v0):
| Profile | Description |
|---|---|
EnkryptedChat-Profile-v0-MLS-fallback | Cascade disabled or failed; MLS-only envelope |
EnkryptedChat-Profile-v0-fast-file | Fast file transfer; cascade MAY be bypassed; DTLS only for bulk data |
P1.2 Layer stack
| Layer | Component | Normative reference |
|---|---|---|
| L0 | Signaling (PeerJS / WSS) | P2 |
| L1 | WebRTC (DTLS, SCTP data channel, SRTP for media) | RFC 8825–8829; ch. 3 |
| L2 | Session handshake PDUs | P3 |
| L3 | Cascade layer 1 — MLS | RFC 9420 |
| L4 | Cascade layer 2 — Signal (X3DH + Double Ratchet) | Signal spec; Appendix B |
| L5 | Cascade layer 3 — ML-KEM-768 | FIPS 203 / project quantum doc |
| L6 | Cascade layer 4 — AES-GCM-256 | Web Crypto API; password-derived in implementation |
| L7 | Application PDU (type field) | P4 |
Encrypt direction (plaintext to wire): L7 payload → L3…L6 cascade → wrapped in encryptedMessage → L2 PDU → L1 DTLS.
P1.3 Product configuration (Enkrypted Chat)
Authoritative product settings in glitr-chat:
enableCascadingCipher = true
cipherLayers = ["MLS", "Signal", "ML-KEM", "AES"]
enableSignalProtocol = true
The p2p library defaults enableCascadingCipher = false; the product shell overrides to true.
P1.4 Security properties (normative summary)
Legend: Provides · Partial · Does not provide
| Property | Signaling | Handshake | Transport (DTLS/SRTP) | Application message |
|---|---|---|---|---|
| Confidentiality (content) | N/A | Partial — key material in JSON PDUs | Provides | Provides — cascade + DTLS |
| Integrity | Provides (TLS) | Partial | Provides | Provides — AEAD per layer |
| Forward secrecy (message) | Does not provide | Partial | Provides (ephemeral) | Provides — Signal + MLS epochs |
| Post-quantum confidentiality | Does not provide | Does not provide | Does not provide | Partial — ML-KEM layer only |
| Authentication (peer identity) | Partial — Peer ID | Partial — in-band keys | Does not provide | Partial — no global PKI |
| Metadata minimization | Does not provide | Does not provide | Does not provide | Partial |
| Offline / async delivery | Does not provide | Does not provide | Does not provide | Does not provide |
PQ wording: ML-KEM-768 adds post-quantum protection for message content at the cascade layer; transport and handshake are not post-quantum.
At-rest protection for local keys is product-dependent and not claimed by this profile.
P1.5 Explicit non-properties
Implementations of EnkryptedChat-Profile-v0 do not provide:
- Offline / asynchronous message delivery without both peers online
- Global identity or account system
- Guaranteed anonymity (IPs visible to TURN/peers)
- Interoperability with Signal, WhatsApp, or Matrix clients
- Independently audited security (see disclaimers)
P1.6 Formal verification scope
In-house formal work applies to the Rust Signal Protocol core only. It does not cover the browser stack, cascade composition, WebRTC, or JSON PDU layer. See Signal Protocol formal verification.
P1.7 EnkryptedChat-Profile-v1 (future freeze)
EnkryptedChat-Profile-v0 remains unfrozen. Before publishing v1, implementations MUST meet:
- Frozen PDU schemas (no new required fields without version bump).
- Mandatory
specVersion: "1.0"on every application PDU. - Implemented
protocol-errorPDUs (P8.8). - Golden hex vectors in Appendix A.
- Normative Appendix B.
- Independent security audit completed or documented waiver.
Until then, receivers SHOULD ignore unknown JSON fields. See CHANGELOG-SPEC.
Optional field in current builds: specVersion (string) MAY appear on PDUs for forward compatibility; MUST NOT be required until v1.
P1.8 Vulnerability disclosure
Report security issues via GitHub Security Advisories on positive-intentions/chat. See SECURITY.md in that repository.
P1.9 Document boundary
| In protocol spec (P0, P1–P8, appendices) | In product spec (ch. 1–12) |
|---|---|
| Data channel PDUs, handshake, cascade | Module federation, UI, OPFS, calendar |
| WebRTC/signaling profile | Competitive analysis, roadmap |
| Limits, errors, test vectors | Deployment, CSP/SRI MUSTs |
| Design rationales (P0) | SFrame UX (informative Appendix D) |
P2. Signaling protocol
Normative protocol documentation for the current implementation. Not independently audited.
P2.1 Scope
This chapter specifies connection establishment signaling for EnkryptedChat-Profile-v0 when using an online PeerJS-compatible broker over WSS.
Out of scope for v0: QR-based or manual SDP exchange (ch. 3).
P2.2 Roles
| Role | Responsibility |
|---|---|
| Initiator | Peer that opens connection to remote peerId |
| Responder | Peer that accepts incoming connection |
| Broker | Relays signaling messages; semi-trusted (ch. 2) |
P2.3 Signaling sequence
P2.4 Normative requirements
- Signaling MUST use TLS (WSS) in production.
- The broker MUST NOT be assumed to protect message application plaintext (it does not receive cascade ciphertext except as opaque SDP/ICE side channels).
- Peers MUST treat broker compromise as signaling MITM risk until channel binding exists (future work).
- After data channel
open, peers MUST run the crypto handshake in P3 before sendingtype: "message"withencryptedMessageunless encryption is disabled for debugging.
P2.5 Peer ID
- Generated client-side; opaque string used as broker address.
- Not a cryptographic identity key.
- Collision resistance is probabilistic; no global registry.
P2.6 STUN/TURN (transport adjunct)
ICE candidates may traverse STUN/TURN as defined by WebRTC. TURN policy is product-configurable (ch. 3). TURN is not part of the JSON PDU catalog.
P2.7 Signaling vs data channel
| Traffic | Channel | Format |
|---|---|---|
| SDP, ICE | Broker (PeerJS) | PeerJS protocol |
| Application PDUs | WebRTC data channel | JSON UTF-8 (P4) |
No application type from P4 is sent over the signaling broker in v0.
P2.8 Compromised broker
A malicious or compromised broker can:
- Observe Peer IDs and connection timing (social graph metadata).
- Perform signaling MITM on SDP/ICE unless peers verify out-of-band or channel binding is added (future work).
A broker cannot (in the v0 architecture):
- Read cascade ciphertext (not carried on signaling).
- Inject application handshake PDUs (those use the data channel only).
Peers SHOULD use WSS with certificate validation in production. See ch. 9 threat model and ch. 8 deployment.
P2.9 Future signaling authentication (informative)
Roadmap options (not normative in v0/v1): broker API token, mutual TLS on signaling. No implementation deadline is specified in this specification.
P3. Session establishment
Normative protocol documentation for the current implementation. Not independently audited.
P3.1 Preconditions
- Both peers registered on signaling broker.
- WebRTC data channel state is
open. enableEncryptionis true (production default).
P3.2 Handshake order (normative)
When EnkryptedChat-Profile-v0 cascade is enabled, peers SHOULD exchange PDUs in this order (either peer may send first where symmetric):
| Step | PDU type | Purpose |
|---|---|---|
| 1 | mls-key-package-request | Request remote MLS key package |
| 2 | mls-key-package | Deliver MLS key package |
| 3 | mls-encryption-sync-request | Synchronize encryption state |
| 4 | mls-encryption-sync-response | Respond to sync |
| 5 | signal-key-exchange-request | Start P2P Signal material exchange |
| 6 | signal-key-exchange-response | Respond to Signal exchange |
| 7 | signal-session-complete | Signal session ready |
| 8 | mlkem-key-exchange | ML-KEM public key material |
| 9 | mlkem-key-exchange-response | ML-KEM response |
| 10 | mls-commit / mls-welcome | MLS group commits (as needed) |
| 11 | mls-welcome-ack | Ack welcome (when applicable) |
Exact ordering may include additional mls-commit rounds for groups. Implementations MUST tolerate interleaved handshake PDUs until encryptionReady is true.
P3.3 Completion criterion
Session crypto is ready when:
encryptionReadyRefis true inMLSProvider, and- Remote peer ID is in
encryptedConnectionsset.
Only then MUST encrypted message PDUs be sent (see P6).
P3.4 Sequence diagram (1:1)
P3.5 P2P Signal deltas
Without a central pre-key server, X3DH material is exchanged in live PDUs over the data channel. Normative deltas: Appendix B. Tutorial: P2P Signal Protocol.
P3.5.1 Trust on first use
Peers MUST treat broker Peer IDs as routing handles only. Cryptographic trust comes from in-band Signal/MLS handshakes (TOFU). Deployments SHOULD offer safety-number or QR verification for high-risk users (product UX; not required for encryptionReady).
P3.6 Reconnection
| State persisted locally | On reconnect |
|---|---|
| Signal ratchet state | MAY resume if both peers retain state and peer IDs unchanged |
| MLS epoch / group state | MAY resume for same groupId |
| WebRTC session | MUST re-establish ICE/signaling |
If crypto state is lost, peers MUST repeat handshake from step 1.
P3.7 Reconnection sequence
P3.8 Key rotation sequence
P3.9 Out-of-order chunks
P3.10 Group sessions
Group chat uses additional PDUs (group-member-joined, mls-welcome, etc.). Status: experimental — normative PDU shapes in P4; security claims in ch. 11.
P4. PDU catalog and schemas
Normative protocol documentation for the current implementation. Not independently audited.
P4.1 Encoding
- All PDUs described here are sent over WebRTC data channels unless noted.
- Serialization: JSON encoded as UTF-8 text unless
binary/Uint8Arrayfields are noted. - Every PDU MUST include a string field
typeas the discriminator. - Schema version:
EnkryptedChat-Profile-v0— field names may change before v1.0 freeze.
P4.2 PDU categories
| Category | Purpose |
|---|---|
| Handshake | Establish encryptionReady |
| Encrypted transport | User content with encryptedMessage wrapper |
| Chunking | Split large ciphertext |
| Messaging | Cleartext routing metadata (payload may be encrypted inside wrapper) |
| Files | File transfer (standard and fast paths) |
| Calls | Group/voice/video signaling |
| Presence | Typing, peer list |
P4.3 Handshake PDUs
type | Direction | Required fields (minimum) | Notes |
|---|---|---|---|
mls-key-package-request | Either | — | Triggers key package send |
mls-key-package | Either | keyPackage (implementation-defined) | MLS RFC 9420 structure |
mls-encryption-sync-request | Either | — | State sync |
mls-encryption-sync-response | Either | — | State sync response |
signal-key-exchange-request | Either | Signal payload fields (WASM) | P2P X3DH start |
signal-key-exchange-response | Either | Signal payload fields | |
signal-session-complete | Either | — | Signal ready |
mlkem-key-exchange | Either | ML-KEM public key material | |
mlkem-key-exchange-response | Either | ML-KEM response | |
mls-commit | Either | MLS commit bytes / structure | Group ops |
mls-welcome | Either | MLS welcome | New member |
mls-welcome-ack | Either | — | Ack |
rotation-timestamp-broadcast | Either | rotation timestamp | AES key rotation suffix |
handshake | Either | — | Legacy/generic |
protocol-error | Either | code number MUST; messageId string MAY; detail string MAY | See P8.8 |
P4.4 Encrypted message wrapper
User-visible chat uses outer PDU:
| Field | Type | Required | Description |
|---|---|---|---|
type | string | MUST | "message" for chat |
encryptedMessage | object | MUST when E2EE on | See P4.5 |
callback | any | MAY | Implementation callback |
Inner cleartext fields (before encryption) for message action:
| Field | Type | Description |
|---|---|---|
messageId | string | Unique id |
message | string | Text body |
timestamp | number | Unix ms |
groupId | string | MAY — group route |
attachment | object | MAY — media metadata |
P4.5 encryptedMessage object
P4.5.1 Cascade mode (cascaded: true)
| Field | Type | Required | Description |
|---|---|---|---|
encrypted | boolean | MUST | true |
cascaded | boolean | MUST | true |
cascadedPayload | object | MUST | See P5 |
timestamp | number | MUST | Encryption time |
chunked | boolean | MAY | true if split |
messageId | string | MAY | Required if chunked |
totalChunks | number | MAY | Chunk count |
chunks | number[] | MAY | Byte arrays as JSON numbers |
P4.5.2 MLS-only fallback (cascaded absent)
| Field | Type | Required | Description |
|---|---|---|---|
encrypted | boolean | MUST | true |
envelope | object | MUST | groupId, epoch, ciphertext (byte array) |
chunked | boolean | MAY | MLS chunking |
MLS envelope fields:
| Field | Type | Description |
|---|---|---|
groupId | number[] | Byte array as JSON numbers |
epoch | number | MLS epoch |
ciphertext | number[] | MLS ciphertext bytes |
P4.6 Chunk transport PDU
type | Fields | Description |
|---|---|---|
__mls_chunk__ | messageType, messageId, chunkIndex, totalChunks, chunk, envelope?, timestamp | Reassembles to full encryptedMessage |
Reassembly MUST complete before decrypt. Out-of-order chunks SHOULD be buffered by chunkIndex.
P4.7 Messaging and presence PDUs
type | Purpose |
|---|---|
message | Chat (see P4.4) |
deleteMessage | Delete by messageId |
typing-indicator | Typing state |
peer-list | Known peers |
peer-joined | Peer presence |
group-member-joined | Group membership |
group-invite | Group invite |
P4.8 File transfer PDUs (standard path)
type | Purpose |
|---|---|
file-metadata | Start transfer |
request-chunk | Request chunk index |
file-chunk | Chunk payload |
chunk-received | Ack |
file-transfer-complete | Done |
chunk-ack | Ack variant |
P4.9 File transfer PDUs (fast path)
Used when fast file transfer is enabled; cascade MAY be bypassed.
type | Purpose |
|---|---|
chunk-request | Request chunk |
file-chunk | Data |
byte-range-request | Range request |
byte-range-response | Range response |
transfer-verify-request | Verify |
transfer-verify-response | Verify response |
transfer-chunk-status-request | Status |
transfer-chunk-status-response | Status response |
transfer-complete-notify | Complete |
transfer-complete-verify | Verify complete |
missing-data-response | Missing data |
fast-file-metadata | Fast path metadata (same role as file-metadata) |
Chunk sizes: 1 MB–16 MB adaptive (useFastFileTransfer); per-message cap 256 KB metadata overhead in some paths.
P4.10 Call PDUs
type | Purpose |
|---|---|
groupCallRequest | Incoming group call |
groupCallAccepted | Accepted |
groupCallEnded | Ended |
call-ended | 1:1 call end |
Media uses WebRTC SRTP; optional SFrame layer — experimental (ch. 4).
P4.11 Unknown types
Receivers MUST drop unknown type values without crashing; MAY log at debug only. Senders MUST NOT send handshake PDUs after encryptionReady except for rotation or group maintenance.
P4.12 Global PDU fields
| Field | Type | Required | Max / notes |
|---|---|---|---|
type | string | MUST | Discriminator; see tables below |
specVersion | string | MAY | MUST in Profile-v1; value "1.0" |
Total serialized PDU size MUST NOT exceed 16 MiB (P8).
P4.13 Complete PDU field reference
Extracted from p2p routing (MLSProvider.tsx) and p2p/src/features/. WASM/MLS binary blobs are opaque string or number[] on the wire.
Handshake and crypto control
type | Fields (name · type · required) |
|---|---|
mls-key-package-request | — (no additional fields) |
mls-key-package | keyPackage · string/object · MUST |
mls-encryption-sync-request | implementation-defined · MAY |
mls-encryption-sync-response | implementation-defined · MAY |
signal-key-exchange-request | Signal WASM payload · MUST · Appendix B |
signal-key-exchange-response | Signal WASM payload · MUST |
signal-session-complete | — · MAY |
mlkem-key-exchange | ML-KEM public key material · MUST |
mlkem-key-exchange-response | ML-KEM response · MUST |
mls-commit | MLS commit bytes/structure · MUST |
mls-welcome | MLS welcome · MUST |
mls-welcome-ack | — · MAY |
rotation-timestamp-broadcast | rotationTimestamp · number · SHOULD |
handshake | legacy · MAY |
Messaging and presence
type | Fields |
|---|---|
message | messageId string SHOULD · message string MAY · timestamp number MAY · groupId string MAY · attachment object MAY · encryptedMessage object MUST when E2EE · originalSender string MAY (relay) · _senderId string MAY |
deleteMessage | messageId string MUST · groupId string MAY |
typing-indicator | indicatorType string SHOULD (typing, recording-audio, sharing-location, stopped) · chatId string MAY |
peer-list | peer list payload · implementation-defined |
peer-joined | peerId string SHOULD |
group-member-joined | groupId string MUST · member fields · implementation-defined |
group-invite | groupId string MUST · invite payload · implementation-defined |
Chunk transport
type | Fields |
|---|---|
__mls_chunk__ | messageId MUST · chunkIndex MUST · totalChunks MUST · chunk number[] MUST · messageType MAY · timestamp MAY · envelope object MAY (MLS chunk path) |
Standard file transfer
type | Fields |
|---|---|
file-metadata | fileId MUST · fileName MAY · fileSize number MAY · mimeType string MAY · totalChunks number MAY · prefix string MAY · thumbnail string/object MAY · groupId string MAY |
fast-file-metadata | same as file-metadata |
request-chunk | fileId MUST · chunkIndex number MUST · requestedSize number MAY · groupId string MAY · originalRequester string MAY |
file-chunk | fileId MUST · chunkIndex number MUST · chunk number[]/binary MUST · groupId string MAY |
chunk-received | fileId MUST · chunkIndex number MUST |
chunk-ack | ack fields · implementation-defined |
file-transfer-complete | fileId MUST · completion metadata MAY |
Fast file transfer
type | Fields |
|---|---|
chunk-request | fileId MUST · chunkIndex number MUST · requestedSize number MAY |
byte-range-request | range fields · implementation-defined |
byte-range-response | range + data · implementation-defined |
transfer-verify-request | fileId MUST · verify metadata MAY |
transfer-verify-response | verify result · implementation-defined |
transfer-chunk-status-request | status query · implementation-defined |
transfer-chunk-status-response | status result · implementation-defined |
transfer-complete-notify | fileId MUST |
transfer-complete-verify | verify complete · implementation-defined |
missing-data-response | missing chunk indices · implementation-defined |
Calls
type | Fields |
|---|---|
groupCallRequest | callType string MUST · groupId string MAY · callerName string MAY · originalCaller string MAY · relayedBy string MAY |
groupCallAccepted | group/call ids · implementation-defined |
groupCallEnded | group/call ids · implementation-defined |
call-ended | from string MAY (peer id) |
| protocol-error | code MUST · messageId MAY · detail MAY (no secrets) |
protocol-error (Profile-v0 implemented)
| Field | Type | Required | Notes |
|---|---|---|---|
type | string | MUST | "protocol-error" |
code | number | MUST | 1–5 per P8.8 |
messageId | string | MAY | Related message |
detail | string | MAY | Debug only; no secrets |
Relay metadata (group PDUs)
Group relay PDUs MAY include: originalSender, _senderId, relayedBy (string). Receivers SHOULD use originalSender for routing when present.
P4.14 Source of truth
PDU handlers are centralized in MLSProvider.tsx (routing) and feature modules under p2p/src/features/. This catalog reflects that implementation as of profile v0. See ch. 10 for repository map.
P5. Cascade wire encoding
Normative protocol documentation for the current implementation. Not independently audited.
P5.1 Scope
Defines the cascadedPayload object inside encryptedMessage for EnkryptedChat-Profile-v0. Cryptographic primitives of each layer are defined by Signal, MLS, ML-KEM, and Web Crypto — this chapter defines composition and serialization only.
P5.2 Layer processing order
CascadingCipherManager applies layers in registration order:
plaintext: Uint8Array
→ layer[0] MLS encrypt
→ layer[1] Signal encrypt (input: base64 decode of prior output)
→ layer[2] ML-KEM encrypt
→ layer[3] AES-GCM encrypt
→ finalCiphertext: Uint8Array
Between layers, intermediate ciphertext is represented as base64 strings internally; finalCiphertext is raw bytes.
Decrypt MUST reverse layer order.
P5.3 cascadedPayload schema
| Field | JSON type | Required | Description |
|---|---|---|---|
finalCiphertext | array of numbers | MUST (unless chunked) | Each element 0–255 |
layers | array of objects | MUST | Per-layer metadata |
layerParameters | array of objects | MUST | Per-layer decrypt params |
totalProcessingTime | number | MUST | Milliseconds |
originalSize | number | MUST | Plaintext byte length |
finalSize | number | MUST | Ciphertext byte length |
timestamp | number | SHOULD | Unix ms; informative for interop debugging |
Decrypt MUST use finalCiphertext and layerParameters. layers[] statistics are informative but SHOULD be included for diagnostics.
P5.3.1 layers[] entry (LayerMetadata)
| Field | Type | Description |
|---|---|---|
algorithm | string | e.g. MLS, Signal, ML-KEM, AES-GCM-256 |
version | string | Layer implementation version |
timestamp | number | Layer processing time |
inputSize | number | Bytes in |
outputSize | number | Bytes out |
processingTime | number | ms |
metadata | object | MAY — algorithm-specific |
P5.3.2 layerParameters[]
Opaque per-layer decryption parameters (keys, nonces, Signal state handles). Structure is layer-specific; auditors SHOULD inspect cryptography module layer implementations.
P5.4 Key material (implementation notes)
For each encrypted message, MLSProvider builds CipherKeys:
| Layer key name | Source |
|---|---|
| MLS | mlsManager + groupId |
| Signal | WASM session after handshake |
ML-KEM-768 | peerMLKEMKeyPairsRef for target peer |
AES-GCM-256 | Password derived from sorted peer IDs + optional rotation suffix |
AES password format (normative for v0 implementation):
1:1 chat: "{sortedPeerA}-{sortedPeerB}-aes-key" or with "-{rotationTimestamp}" suffix
Group: "{localPeerId}-group-aes-key" (+ suffix)
P5.5 Chunking cascaded ciphertext
| Constant | Value |
|---|---|
CHUNK_SIZE | 8192 bytes (8 KiB) |
If finalCiphertext.length ≤ CHUNK_SIZE:
- Send single PDU with full
cascadedPayload.
If larger:
- Set
chunked: true,messageId,totalChunks. - Omit
finalCiphertextfrom inline payload; sendchunks[]arrays. - Optionally use
__mls_chunk__transport (P4).
Receivers MUST reassemble chunks in order before decrypt.
P5.6 Failure behavior
| Failure | Behavior |
|---|---|
| Missing layer keys | Throw CascadingCipherError; encrypt path falls back to MLS-only |
| Layer decrypt/auth fail | MUST NOT emit plaintext; abort message (P6) |
| Zero layers configured | MUST NOT call encrypt |
P5.7 Reference implementation
CascadingCipherManager.ts— layer chaintypes.ts—CascadedPayloadinterfaceMLSProvider.tsx— serialization to JSON number arrays
P6. Processing rules
Normative protocol documentation for the current implementation. Not independently audited.
P6.1 Terminology
Keywords MUST, MUST NOT, SHOULD, and MAY are used per RFC 2119.
P6.2 Outbound encrypt path
Given application PDU data with type and payload fields:
- If
enableEncryptionis false → senddataunchanged; STOP. - If remote peer ∉
encryptedConnections→ send cleartext or queue handshake; implementation-defined; MUST NOT claim E2EE. - Serialize inner payload to bytes (typically UTF-8 JSON of message fields).
- If
enableCascadingCipherand cascade ready: - Else → MLS-only
envelopeper P4. - Set
data.encryptedMessage= result; preservedata.type. JSON.stringify→ send on data channel via PeerJS.
On any encrypt failure at step 4, implementation MAY fall back to step 5 and SHOULD log.
P6.3 Inbound decrypt path
On data channel data event:
- Parse JSON; if parse fails → MUST drop.
- If
type === "__mls_chunk__"→ buffer; if complete → reconstructencryptedMessage; goto 4. - If
encryptedMessageabsent → dispatch cleartext PDU to action router; STOP. - If
encryptedMessage.chunked→ reassemble chunks → single payload. - If
encryptedMessage.cascaded:- Rebuild
Uint8ArrayfromfinalCiphertextor chunks. cascadingCipher.decrypt(cascadedPayload, keys)reverse layer order.- On error → MUST NOT deliver plaintext; MUST drop or surface error.
- Rebuild
- Else MLS-only →
mlsManager.decryptMessage(envelope). - Parse decrypted bytes to inner fields (
message, etc.). - Dispatch to handler for original
type(e.g.messageaction).
P6.4 Handshake dispatch
PDUs listed in P3 MUST be processed even when encryptionReady is false. Handlers MUST update local crypto state before replying.
P6.5 Ordering
- JSON PDUs have no global sequence number at application layer.
- Signal ratchet MUST handle out-of-order ciphertext within a session.
- Chunk PDUs MUST be ordered by
chunkIndexbefore decrypt.
P6.6 Fast file transfer profile
When fast transfer active:
file-chunk/byte-range-*PDUs MAY carry binary withoutencryptedMessage.- Protection is DTLS + optional path security only.
- UI MUST indicate reduced protection (product requirement).
P6.7 Debug / disabled encryption
When encryption disabled for development:
- Senders MUST NOT set
encryptedMessage. - Receivers MUST accept cleartext
messagefields. - Deployments MUST NOT disable encryption in production.
P7. State machines
Normative protocol documentation for the current implementation. Not independently audited.
P7.1 Connection FSM
| State | Allows message E2EE |
|---|---|
Idle | No |
SignalingRegistered | No |
WebRTCConnecting | No |
DataChannelOpen | No |
CryptoHandshaking | No (handshake PDUs only) |
Ready | Yes |
Disconnected | No |
P7.2 Crypto handshake FSM
Implementations MAY parallelize Signal and MLS substates; they MUST reach Ready only when all enabled layers in cipherLayers are initialized.
P7.3 Per-peer encryption set
- Global set
encryptedConnectionscontains remotepeerIdstrings. - Send path checks membership before encrypt (P6).
- On disconnect, peer SHOULD be removed from set.
P7.4 Reconnection transitions
| From | Event | To | Crypto action |
|---|---|---|---|
Ready | ICE lost | Disconnected | Keep local state |
Disconnected | ICE restored | DataChannelOpen | If state intact → Ready; else → CryptoHandshaking |
Ready | Session corrupt | CryptoHandshaking | Full handshake |
P7.5 Group state (experimental)
Additional states for groupId, MLS epochs, and groupMembers map. Not normative for production security claims until audited.
P8. Limits, errors, and edge cases
Normative protocol documentation for the current implementation. Not independently audited.
P8.1 Size limits
| Limit | Value | Applies to |
|---|---|---|
| Max JSON PDU (data channel) | 16 MiB | Any single JSON object; MUST reject larger |
| Cascade chunk size | 8192 bytes | finalCiphertext over data channel |
| Fast transfer chunk (initial) | 8 MiB | Fast file path |
| Fast transfer chunk range | 1 MiB – 16 MiB | Adaptive |
| Fast per-message metadata cap | 256 KiB | Some fast paths |
| WebRTC buffer threshold | 8 MiB | bufferedAmount backpressure |
Oversized cascade payloads MUST use chunking (P5); senders MUST NOT truncate without chunking.
P8.2 Error handling — Profile-v0 (current)
| Condition | Receiver behavior |
|---|---|
| JSON parse error | MUST drop PDU |
| PDU size > 16 MiB | MUST drop before full parse if possible |
Unknown type | MUST drop; MAY log at debug only |
| Decrypt/auth failure | MUST NOT deliver plaintext |
| Missing chunk | MUST wait or timeout; MUST NOT decrypt partial |
| Handshake PDU after fatal error | SHOULD ignore |
| Encrypt failure | MAY fallback MLS-only; SHOULD log |
Profile-v0 has no application-level error codes on the wire.
P8.3 Recommended timeouts
Implementations MAY use other values until Profile-v1. Recommended SHOULD values:
| Timer | Recommended | Applies to |
|---|---|---|
| Chunk reassembly | 120 s | Per messageId |
| Handshake stall | 300 s | From data channel open to encryptionReady |
P8.4 Denial of service
Receivers SHOULD cap:
- 4096 chunks per
messageId - 64 concurrent
messageIdreassemblies per remote peer
Additional mitigations:
- Reject excessive JSON array lengths before allocation.
- Signaling rate limits: broker policy (out of band).
P8.5 Security considerations
This profile addresses mitigations for:
- Chunk flooding / reassembly DoS (P8.4 caps).
- Oversize JSON / memory exhaustion (16 MiB cap).
- Signaling MITM via broker compromise (P2.8).
- Malicious peer (invalid PDUs, spam) — drop on failure.
- Application-layer replay — dedup by
messageId(product layer). - Supply-chain / subverted JS — see ch. 8.
P8.6 Edge cases
| Case | Rule |
|---|---|
| Cascade disabled mid-session | MUST re-handshake or send cleartext only if encryption off |
| Peer ID change | MUST full handshake |
| MLS fallback after cascade fail | encryptedMessage uses envelope not cascadedPayload |
| Empty plaintext | Allowed; still encrypted |
Duplicate messageId | Application-layer dedup; not protocol-forbidden |
P8.7 Versioning (v0)
Until EnkryptedChat-Profile-v1 is published, implementations MAY add JSON fields without breaking single-codebase deployments. Third parties MUST NOT assume stability.
Profile-v0 implements protocol-error PDUs in p2p (MLSProvider, PeerProvider). Receivers MUST still drop failing PDUs if no error PDU is received.
P8.8 protocol-error PDU (implemented)
| Field | Type | Required | Description |
|---|---|---|---|
type | string | MUST | "protocol-error" |
code | number | MUST | See table below |
messageId | string | MAY | Related message |
detail | string | MAY | Debug text; MUST NOT contain secrets |
| Code | Name | When |
|---|---|---|
| 1 | DECRYPT_FAILED | AEAD/auth failure |
| 2 | UNKNOWN_PDU | Unknown type |
| 3 | CHUNK_TIMEOUT | Reassembly timeout |
| 4 | OVERSIZE | PDU over limit |
| 5 | HANDSHAKE_REQUIRED | E2EE before encryptionReady |
Senders SHOULD emit protocol-error on the paths implemented in Profile-v0 (MLSProvider, PeerProvider); receivers MUST still drop failing PDUs if no error PDU is received.
Appendix A. Test vectors
Golden hex fixtures are committed in cryptography and p2p (see below). Live MLS+Signal+ML-KEM+AES vectors with production keys remain a Profile-v1 gate.
A.1 Conformance statement
An implementation conforms to this specification when it:
- Implements all MUST rules in P6.
- Recognizes all PDU types in P4.
- Passes all Published (hex) vectors in this appendix (V1–V7).
- SHOULD pass automated golden tests (commands in A.9).
Profile id for mock cascade vectors: EnkryptedChat-Profile-v0-golden-mock.
Profile id for wire PDU vectors: EnkryptedChat-Profile-v0-golden-wire.
A.2 Minimum vector set
| ID | Path | Status |
|---|---|---|
| V1 | Mock cascade "Hi" (3 layers) | Published (hex) |
| V2 | Profile-order mock cascade (4 layers) | Published (hex) |
| V3 | cascadedPayload JSON shape | Published (structure) |
| V4 | Handshake PDU pair UTF-8 | Published (hex) |
| V5 | MLS-only fallback envelope | Published (hex) |
| V6 | __mls_chunk__ PDU | Published (hex) |
| V7 | Chunk count for 9000 B payload | Published (math) |
| V8 | Live MLS+Signal+ML-KEM+AES round-trip | Deferred — Profile-v1 |
| V9 | protocol-error PDU | Published (hex) |
A.3 Vector V1 — Three-layer mock cascade
Source: golden-vectors.fixtures.js
| Step | Hex |
|---|---|
Plaintext ("Hi") | 4869 |
finalCiphertext (MockCipher1→3) | 4869010203 |
cd cryptography && npm test -- golden-vectors.test.js
A.4 Vector V2 — Four-layer profile-order mock cascade
Simulates layer depth MLS → Signal → ML-KEM → AES using deterministic mock layers (append 0x01…0x04).
| Step | Hex |
|---|---|
| Plaintext | 0a141e28 |
finalCiphertext | 0a141e2801020304 |
Same test file as V1.
A.5 Vector V3 — cascadedPayload JSON shape
Structure normative when wrapping V1/V2 bytes in encryptedMessage:
{
"encrypted": true,
"cascaded": true,
"cascadedPayload": {
"finalCiphertext": [72, 105, 1, 2, 3],
"layerParameters": [{}],
"layers": [{ "algorithm": "MLS", "version": "1.0.0-golden" }],
"originalSize": 2,
"finalSize": 5,
"timestamp": 1717000000000
}
}
A.6 Vector V4 — Handshake PDU pair (UTF-8 hex)
Source: protocol-golden-vectors.fixtures.js
| PDU | JSON | UTF-8 hex |
|---|---|---|
| Request | {"type":"mls-key-package-request"} | 7b2274797065223a226d6c732d6b65792d7061636b6167652d72657175657374227d |
| Response | {"type":"mls-key-package","keyPackage":"golden-fixture-key-package-bytes"} | 7b2274797065223a226d6c732d6b65792d7061636b616765222c226b65795061636b616765223a22676f6c64656e2d666978747572652d6b65792d7061636b6167652d6279746573227d |
Full handshake order: P3.
A.7 Vector V5 — MLS-only fallback envelope (UTF-8 hex)
{"encrypted":true,"envelope":{"groupId":[1,2,3],"epoch":0,"ciphertext":[10,20,30]}}
UTF-8 hex:
7b22656e63727970746564223a747275652c22656e76656c6f7065223a7b2267726f75704964223a5b312c322c335d2c2265706f6368223a302c2263697068657274657874223a5b31302c32302c33305d7d7d
A.8 Vector V6 — __mls_chunk__ PDU (UTF-8 hex)
JSON:
{"type":"__mls_chunk__","messageType":"message","messageId":"golden-msg-0001","chunkIndex":0,"totalChunks":2,"chunk":[1,2,3],"timestamp":1717000000000}
UTF-8 hex:
7b2274797065223a225f5f6d6c735f6368756e6b5f5f222c226d65737361676554797065223a226d657373616765222c226d6573736167654964223a22676f6c64656e2d6d73672d30303031222c226368756e6b496e646578223a302c22746f74616c4368756e6b73223a322c226368756e6b223a5b312c322c335d2c2274696d657374616d70223a313731373030303030303030307d
A.9 Vector V7 — Chunk count (9000 B finalCiphertext)
Per P8 cascade chunk size 8192:
| Input | Expected totalChunks |
|---|---|
| 9000 bytes | 2 |
Verified in protocol-golden-vectors.test.js.
A.11 Vector V9 — protocol-error PDU (UTF-8 hex)
{"type":"protocol-error","code":1,"messageId":"golden-msg-0001"}
UTF-8 hex:
7b2274797065223a2270726f746f636f6c2d6572726f72222c22636f6465223a312c226d6573736167654964223a22676f6c64656e2d6d73672d30303031227d
Codes 1–5 defined in P8.8. Implementation: p2p/src/utils/protocolError.ts.
A.12 Authoritative automated tests
| Repository | Command | Vectors |
|---|---|---|
cryptography | npm test -- golden-vectors.test.js | V1, V2 |
p2p | npm test -- protocol-golden-vectors.test.js | V4, V5, V6, V7, V9 |
p2p | npm test -- protocolError.test.ts | protocol-error helper |
cryptography | npm test -- cascading-cipher-manager.test.js | Layer order (legacy) |
signal-protocol | npm test | X3DH, ratchet, WASM |
p2p | npm test -- MLSProvider | Handshake routing |
Formal models: ProVerif.
A.13 Profile-v1 requirement (V8)
Before EnkryptedChat-Profile-v1, add V8: fixed hex for a full encrypt/decrypt round-trip using live MLS, Signal, ML-KEM, and AES layers with frozen keys in a committed fixture.
Appendix B. P2P Signal protocol deltas
Normative for auditors reviewing Signal usage in Enkrypted Chat. Upstream definitions: Signal Protocol documentation. Tutorial background: P2P Signal Protocol.
B.1 Scope
This appendix lists only deltas from the standard Signal deployment model. Where this document is silent, implementations SHOULD follow the Signal specification for primitive choice (Curve25519, AES-256, HMAC-SHA256, etc.) unless noted here.
B.2 No central pre-key server
| Standard Signal | Enkrypted Chat P2P |
|---|---|
| Bob uploads pre-key bundle to server before Alice messages | Peers exchange material live over WebRTC data channel after ICE |
| Alice fetches bundle asynchronously | Synchronous handshake PDUs: signal-key-exchange-request, signal-key-exchange-response, signal-session-complete (P3) |
| One-time prekeys consumed from server pool | No one-time prekeys — 3-DH X3DH variant instead of 4-DH |
Implication: No offline initial message to a peer who has never been online in the same session establishment window.
B.3 X3DH variant (3-DH)
The P2P handshake performs three DH contributions (identity, signed prekey, ephemeral) without a one-time prekey DH4 term. Forward secrecy for the first application message relies on ephemeral keys plus WebRTC DTLS and subsequent Double Ratchet steps.
B.4 WASM implementation bridge
- Browser builds use WebAssembly compiled from the
signal-protocolRust core. - JavaScript in
p2pmarshals keys and ciphertext across the WASM boundary; wire PDUs remain JSON (P4). - Formal verification (overview) applies to the Rust core, not the JS/WASM glue or cascade wrapper.
B.5 Local storage
| Asset | Typical location |
|---|---|
| Identity and session state | Browser storage (IndexedDB / OPFS per product build) |
| Ratchet state | Persisted per peer session in MLSProvider / Signal store |
Storage encryption at rest is product-dependent and not guaranteed by EnkryptedChat-Profile-v0 (P1.5).
B.6 Identity key vs Peer ID
| Identifier | Role |
|---|---|
| Peer ID | Broker routing address; opaque string; not a Signal identity key |
| Signal identity key | Long-term key inside WASM store; used in X3DH and ratchet |
Verifying that a Peer ID maps to an expected identity key is out of band (TOFU or future safety-number UX).
B.7 Session lifecycle
| Event | Behavior |
|---|---|
| First connect | Full Signal exchange via P3 handshake |
| Reconnect, state retained | MAY resume ratchet if peer IDs unchanged (P3.6) |
| Lost state / peer ID change | MUST repeat Signal handshake |
| Teardown | Local session state SHOULD be cleared on explicit disconnect or unrecoverable decrypt failure |
B.8 Out-of-order and skipped messages
Double Ratchet MUST accept out-of-order ciphertext within a session per Signal semantics. Maximum skip windows follow the WASM library configuration; auditors SHOULD inspect signal-protocol tests for bounds.
B.9 Composition with cascade
Signal encrypts the output of the MLS layer (or plaintext entry) inside the cascade (P5). Signal security properties apply to that layer’s input/output; they do not extend to broker metadata, MLS group administration plaintext in handshake, or fast-file DTLS-only paths.
B.10 Security properties (Signal layer only)
| Property | P2P Signal layer |
|---|---|
| Forward secrecy (messages) | Provides (after handshake complete) |
| Break-in recovery | Provides (Double Ratchet) |
| Asynchronous delivery | Does not provide (no relay server) |
| Server-assisted key directory | Does not provide |
B.11 References
Appendix C. Glossary
Definitions are normative for interpreting P1–P8 unless a chapter defines a term more specifically.
| Term | Definition |
|---|---|
| Application PDU | JSON object on a WebRTC data channel with required type field (P4). |
| Broker | PeerJS-compatible signaling server over WSS; relays SDP/ICE only in v0 profile. |
| Cascade | Ordered stack MLS → Signal → ML-KEM → AES applied by CascadingCipherManager (P5). |
cascadedPayload | Object inside encryptedMessage holding finalCiphertext and layer metadata. |
encryptionReady | Implementation flag: local crypto handshake complete for a peer; required before E2EE message PDUs (P3). |
encryptedConnections | Set of remote Peer IDs allowed on encrypt path (P7). |
encryptedMessage | Wrapper object carrying ciphertext (cascade or MLS-only). |
| Fast file profile | Bulk transfer that MAY bypass cascade; DTLS-only protection (P6.6). |
| Handshake PDU | Control type values in P3 establishing crypto state. |
| Initiator / Responder | WebRTC roles for outbound connect vs inbound accept (P2). |
| MLS | Messaging Layer Security (RFC 9420). |
| Peer ID | Opaque broker address string; not a global cryptographic identity (P2.5). |
| PDU | Protocol data unit; here, one JSON object on the data channel. |
| Profile | Named configuration EnkryptedChat-Profile-v0 (P1). |
| Signaling | WSS broker traffic for WebRTC setup, distinct from application PDUs. |
| TOFU | Trust on first use — accept Peer ID and in-band keys without prior out-of-band verification. |
| TURN | Relay for ICE when direct UDP fails; sees traffic metadata (ch. 3). |
Appendix D. SFrame (informative, WIP)
Not normative for EnkryptedChat-Profile-v0. Voice and video use WebRTC SRTP by default (ch. 3).
D.1 Status
SFrame integration in the cryptography module is experimental. This appendix documents intent only; wire formats and key schedules are not frozen.
D.2 Relationship to protocol spec
| Layer | Normative in P1–P8? |
|---|---|
| SRTP (WebRTC media) | Referenced via WebRTC |
| SFrame end-to-end media | No — product/ch. 4 only |
Implementers MUST NOT claim SFrame protection in conformance statements until a future profile adds normative rules.