1# SophaxChat Wire Protocol
2
3All messages are JSON-encoded and sent over the P2P mesh (MultipeerConnectivity on iOS, Nearby Connections on Android) or over TCP/Tor.
4
5## Transport
6
7Every message is wrapped in a `WireMessage` envelope, signed with the sender's Ed25519 identity key.
8
9### WireMessage
10
11```json
12{
13 "type": "message",
14 "payload": "<base64-encoded JSON of the inner message>",
15 "senderID": "<peerID — SHA-256(signingKey || dhKey) hex>",
16 "timestamp": "2025-01-01T00:00:00Z",
17 "signature": "<base64 Ed25519 signature>"
18}
19```
20
21**Signing bytes** (deterministic, no extra Date() call):
22```
23type.rawValue.utf8 || payload || senderID.utf8 || ISO8601(timestamp).utf8
24```
25
26---
27
28## Message Types
29
30| `type` | Inner struct | Direction | Encrypted |
31|--------|-------------|-----------|-----------|
32| `hello` | `HelloMessage` | peer ↔ peer | No (public keys) |
33| `initiateSession` | `InitiateSessionMessage` | A → B | Partial (X3DH) |
34| `message` | `ChatMessagePayload` | A ↔ B | Yes (Double Ratchet) |
35| `ack` | `AckMessage` | B → A | No |
36| `relay` | `RelayEnvelope` | any → any | Inner only |
37| `typing` | `TypingMessage` | A → B | No |
38| `sealed` | `SealedMessage` | A → B | Yes (ChaCha20-Poly1305) |
39| `readReceipt` | `ReadReceiptMessage` | B → A | No |
40| `reaction` | `ReactionMessage` | A → B | No |
41| `groupMessage` | `GroupWireMessage` | A → members | Yes (Sender Keys) |
42| `groupReaction` | `GroupReactionMessage` | any → members | No |
43| `groupMemberLeft` | `GroupMemberLeftMessage` | leaver → members | No |
44| `groupReadReceipt` | `GroupReadReceiptMessage` | receiver → sender | No |
45| `storeAndForward` | `StoreAndForwardRequest` | A → relay | Outer no, inner sealed |
46| `storeAndForwardDelivery` | `StoreAndForwardDelivery` | relay → B | Sealed items |
47| `channelAnnouncement` | `ChannelAnnouncement` | creator → mesh | No |
48
49---
50
51## Protocol Flow
52
53### 1. Discovery + Handshake
54
55```
56A connects to B (MPC/Nearby/TCP)
57A → B: hello { bundle: PreKeyBundle }
58B → A: hello { bundle: PreKeyBundle }
59```
60
61**PreKeyBundle** contains:
62- `peerID` — `hex(SHA-256(signingKey || dhKey))[0..<16]`
63- `username`
64- `signingKeyPublic` — Ed25519 (32 bytes, base64)
65- `dhIdentityKeyPublic` — X25519 (32 bytes, base64)
66- `signedPreKeyPublic` / `signedPreKeyId` / `signedPreKeySignature`
67- `oneTimePreKeys` — array of `{ id, publicKey }` (optional)
68- `tcpAddress` — `"host:port"` if peer advertises a Tor/TCP address (optional)
69
70### 2. Session Initiation (X3DH)
71
72Alice sends the first message:
73
74```
75A → B: initiateSession {
76 senderBundle: PreKeyBundle,
77 ephemeralPublicKey: Data, // EK_A (32 bytes)
78 usedSignedPreKeyId: UInt32,
79 usedOneTimePreKeyId: UInt32?,
80 initialMessage: RatchetMessage // first DR message
81}
82```
83
84Bob derives the shared secret using X3DH and initialises the Double Ratchet as responder.
85
86### 3. Normal Messages
87
88```
89A → B: message {
90 ratchetMessage: RatchetMessage,
91 messageID: "UUID"
92}
93```
94
95**RatchetMessage**:
96```json
97{
98 "header": {
99 "dhPublicKey": "<base64>",
100 "previousChainLength": 0,
101 "messageNumber": 0
102 },
103 "ciphertext": "<base64 ChaCha20-Poly1305>"
104}
105```
106
107**MessageContent** (plaintext inside ciphertext):
108```json
109{
110 "body": "Hello!",
111 "type": "text",
112 "replyToID": null,
113 "timestamp": "2025-01-01T00:00:00Z",
114 "expiresAt": null,
115 "attachmentData": null,
116 "attachmentMimeType": null,
117 "audioDuration": null,
118 "groupInviteData": null,
119 "senderKeyData": null
120}
121```
122
123`type` values: `text` | `image` | `audio` | `groupInvite` | `senderKeyDistribution`
124
125### 4. Delivery Ack
126
127```
128B → A: ack { messageID: "UUID", status: "delivered" }
129```
130
131### 5. Multihop Relay
132
133```
134A → C: relay { RelayEnvelope }
135C → B: relay { RelayEnvelope (ttl-1, hopCount+1) }
136```
137
138**RelayEnvelope**:
139```json
140{
141 "id": "UUID",
142 "targetPeerID": "abc123",
143 "originPeerID": "xyz789",
144 "ttl": 5,
145 "hopCount": 1,
146 "message": { WireMessage }
147}
148```
149
150Max TTL = 6. Nodes drop envelopes with TTL = 0. Deduplication via LRU cache of envelope IDs.
151
152### 6. Sealed Sender
153
154Hides the inner message type and payload from relay nodes:
155
156```
157encryption:
158 EK_s = random Curve25519 keypair
159 sharedSecret = ECDH(EK_s.private, recipient.dhPublicKey)
160 sealingKey = HKDF-SHA256(sharedSecret, info="SophaxChat_SealedSender_v1", len=32)
161 encryptedPayload = ChaCha20-Poly1305(sealingKey, JSON(innerWireMessage))
162```
163
164```json
165sealed {
166 "ephemeralPublicKey": "<base64 32B>",
167 "encryptedPayload": "<base64 nonce(12B) + ciphertext + tag(16B)>"
168}
169```
170
171### 7. Group Messages (Sender Keys v2)
172
173Each group member maintains a KDF chain. Messages use a per-message key derived at the current chain iteration:
174
175```
176message_key = HKDF-SHA256(chain_key, info="SophaxChat_SenderKey_Message_v1", len=32)
177next_chain_key = HKDF-SHA256(chain_key, info="SophaxChat_SenderKey_Chain_v1", len=32)
178```
179
180**GroupWireMessage**:
181```json
182{
183 "groupID": "UUID",
184 "messageID": "UUID",
185 "senderPeerID": "abc123",
186 "senderUsername": "alice",
187 "timestamp": "2025-01-01T00:00:00Z",
188 "ciphertext": "<base64 ChaChaPoly>",
189 "attachmentCiphertext": null,
190 "attachmentMimeType": null,
191 "audioDuration": null,
192 "senderKeyIteration": 42,
193 "expiresAt": null,
194 "replyToID": null
195}
196```
197
198`senderKeyIteration = nil` → v1 shared-key (backward compat).
199
200---
201
202## Store-and-Forward (Offline Delivery)
203
204When target is offline, Alice asks a directly-connected relay peer Bob to hold a sealed message:
205
206```
207A → B: storeAndForward {
208 targetPeerID: "carol_id",
209 messageID: "UUID",
210 sealed: SealedMessage,
211 expiresAt: "2025-01-02T00:00:00Z"
212}
213```
214
215When Carol connects to Bob:
216```
217B → Carol: storeAndForwardDelivery {
218 items: [ { messageID, sealed }, ... ]
219}
220```
221
222---
223
224## PeerID Derivation
225
226```
227peerID = hex(SHA-256(signingKeyPublic || dhIdentityKeyPublic))[0..<16]
228```
229
230Both keys are 32-byte raw representations (no ASN.1 wrapper).
231
232---
233
234## Versioning
235
236The protocol is currently unversioned. Breaking changes will be introduced via a new `WireMessageType` value or a `version` field in the `WireMessage` envelope.