bastien-mrq/bt-message / PROTOCOL.md
PROTOCOL.md
Code Preview
# btmessage — Protocol Specification
Reference for implementing a compatible Android (or any platform) client.
---
## 1. Transport layer
### iOS — MultipeerConnectivity
iOS uses Apple's **MultipeerConnectivity** framework (Bluetooth LE + WiFi Direct / AWDL).
Service type: `btmsg-pqc` (Bonjour service name).
### Android — required equivalent
Android must use **Wi-Fi Direct** (P2P) or **Bluetooth Classic / BLE** to discover and connect to peers on the same local network or in proximity.
The simplest cross-platform path is to run both sides over a **local Wi-Fi network** using plain TCP sockets or a lightweight protocol (e.g. Bonjour/mDNS for discovery + TCP for data). A dedicated bridging layer would be needed to bridge MultipeerConnectivity ↔ Android transport.
> **Note:** Direct iOS ↔ Android Bluetooth is not natively compatible at the MultipeerConnectivity level. For a fully cross-platform implementation, replace the transport with a shared protocol such as:
> - mDNS discovery + TCP/TLS over Wi-Fi
> - A relay server (TURN-like) over the internet
---
## 2. Peer identity
Each device generates a **UUID v4** string on first launch and persists it as its permanent peer ID.
```
peerID = UUID().uuidString // e.g. "3F2A1B4C-5D6E-7F8A-9B0C-1D2E3F4A5B6C"
```
- The peer ID is the canonical identifier for routing and key lookup.
- It is also used as the `displayName` for MultipeerConnectivity.
- It is stored in the device Keychain alongside the long-term private key.
---
## 3. Wire protocol — MeshEnvelope
Every packet exchanged on the mesh is a **JSON-encoded `MeshEnvelope`**.
### 3.1 Envelope structure
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": 1,
"senderID": "3F2A1B4C-...",
"recipientID": "7A8B9C0D-...",
"ttl": 7,
"payload": "<base64>",
"timestamp": 1712600000.0
}
```
| Field | Type | Description |
|---------------|----------|----------------------------------------------------------|
| `id` | String | UUID — used for deduplication across hops |
| `type` | UInt8 | Message type (see §3.2) |
| `senderID` | String | Originating peer UUID |
| `recipientID` | String | Destination peer UUID, `"*"` for broadcast, or groupID |
| `ttl` | UInt8 | Time-to-live; decremented at each hop, dropped at 0 |
| `payload` | Data | JSON base64 — content depends on `type` |
| `timestamp` | Double | Unix timestamp (seconds since epoch) of origination |
Encoding: `JSONEncoder` / `JSONDecoder` (Swift default). `Data` fields are base64-encoded by `Codable`.
### 3.2 Message types
| Value | Name | Default TTL | Payload |
|-------|------------------|-------------|---------------------------------------|
| `1` | `chat` | 7 | `ChatPayload` (see §3.3) |
| `2` | `keyExchange` | 5 | Raw public key bytes (XWing, 1216b) |
| `3` | `ack` | 5 | Message ID (UTF-8 string) |
| `4` | `discovery` | 5 | Empty |
| `5` | `groupChat` | 7 | `GroupChatPayload` |
| `6` | `groupKeyDistrib`| 5 | `GroupKeyPayload` |
### 3.3 ChatPayload structure (DM)
The `payload` field of a `chat` envelope contains a JSON-encoded `ChatPayload`:
```json
{
"messageID": "uuid-string",
"encryptedMessage": {
"kemCiphertext": "<base64 — ~1120 bytes>",
"aesCiphertext": "<base64 — 12 + N + 16 bytes>"
}
}
```
### 3.4 GroupChatPayload structure
```json
{
"groupID": "uuid-string",
"messageID": "uuid-string",
"aesCiphertext": "<base64 — 12 + N + 16 bytes>"
}
```
### 3.5 GroupKeyPayload structure
Used to distribute group AES keys to members (encrypted with recipient's XWing key):
```json
{
"groupID": "uuid-string",
"groupName": "Group Name",
"memberIDs": ["peer-id-1", "peer-id-2"],
"encryptedGroupKey": {
"kemCiphertext": "<base64>",
"aesCiphertext": "<base64>"
}
}
```
---
## 4. Routing
### Flood with TTL
1. On receipt of any envelope, check `id` against a **deduplication cache** (1000-entry LRU).
If already seen → discard.
2. Insert `id` into the cache.
3. If `recipientID` matches the local peer ID → deliver locally.
4. Otherwise → decrement TTL; if TTL > 0 forward to all connected peers.
### Direct delivery optimisation
If the recipient is a directly connected peer, send only to that peer (skip flood).
### Invite tie-breaking (iOS-specific)
To prevent both peers from simultaneously inviting each other:
only the peer whose `displayName` is **lexicographically smaller** initiates the connection.
---
## 5. Key exchange
### Flow
```
Device A Device B
| |
|--- keyExchange (TTL=5) --------->|
| payload = A's public key |
| |
|<-- keyExchange (TTL=5) ----------|
| payload = B's public key |
```
- On connection, each peer broadcasts a `keyExchange` envelope to all peers.
- `keyExchange` envelopes are also re-broadcast with decremented TTL (mesh propagation).
- When a peer receives a `discovery` envelope, it responds by broadcasting its own public key.
### Public key format (XWingMLKEM768X25519)
The `payload` of a `keyExchange` envelope is the **raw 1216-byte XWing public key** (post-quantum hybrid combining ML-KEM-768 and X25519).
```
payload = xwing_public_key // exactly 1216 bytes
```
> **Note:** Previous versions used 32-byte X25519 keys. The protocol has been upgraded to use XWingMLKEM768X25519 for post-quantum security.
---
## 6. Encryption
### Algorithm (XWing KEM)
**XWingMLKEM768X25519 + HKDF-SHA256 + AES-256-GCM**
XWing is a hybrid post-quantum Key Encapsulation Mechanism combining:
- **ML-KEM-768** (Kyber) — lattice-based, quantum-resistant
- **X25519** — elliptic curve Diffie-Hellman
### Encrypt (sender side)
```
inputs:
plaintext — UTF-8 message bytes
recipient_pub_key — 1216-byte XWing public key
steps:
1. Load recipient XWing public key:
recipient_pub = XWingMLKEM768X25519.PublicKey(rawRepresentation: recipient_pub_key)
2. Encapsulate (generates shared secret + ciphertext):
encap_result = recipient_pub.encapsulate()
shared_secret = encap_result.sharedSecret // SymmetricKey
kem_ciphertext = encap_result.encapsulated // ~1120 bytes
3. HKDF-SHA256:
symmetric_key = HKDF-SHA256(
ikm = shared_secret bytes,
salt = nil (or ephemeral data),
info = "btmessage-v1",
len = 32
)
4. AES-256-GCM encrypt:
nonce = random 12 bytes
ciphertext = AES-256-GCM.encrypt(key=symmetric_key, nonce=nonce, plaintext=plaintext)
// GCM tag is 16 bytes, appended
5. Serialize aesCiphertext:
aesCiphertext = nonce (12 bytes) || ciphertext (N bytes) || tag (16 bytes)
output:
EncryptedMessage {
kemCiphertext: kem_ciphertext // ~1120 bytes
aesCiphertext: aesCiphertext // 28 + N bytes
}
```
### Decrypt (recipient side)
```
inputs:
encrypted_message — EncryptedMessage struct
recipient_priv_key — XWing private key
steps:
1. Decapsulate:
shared_secret = recipient_priv_key.decapsulate(encrypted_message.kemCiphertext)
2. HKDF-SHA256:
symmetric_key = HKDF-SHA256(
ikm = shared_secret bytes,
info = "btmessage-v1",
len = 32
)
3. Deserialize aesCiphertext:
nonce = aesCiphertext[0:12]
tag = aesCiphertext[-16:]
ciphertext = aesCiphertext[12:-16]
4. AES-256-GCM decrypt:
plaintext = AES-256-GCM.decrypt(key=symmetric_key, nonce=nonce,
ciphertext=ciphertext, tag=tag)
```
### Group Encryption
Group messages use **symmetric AES-256-GCM** with a shared group key:
1. Group creator generates random 32-byte AES key
2. Key is distributed to members via `groupKeyDistrib` (encrypted with each member's XWing key)
3. Group messages encrypted directly with AES-256-GCM (no KEM overhead)
### Key sizes summary
| Element | Size |
|--------------------------|------------|
| XWing public key | 1216 bytes |
| XWing private key | 2432 bytes |
| KEM ciphertext | ~1120 bytes|
| AES-256 key (derived) | 32 bytes |
| AES-GCM nonce | 12 bytes |
| AES-GCM tag | 16 bytes |
---
## 7. Security considerations
### Key Storage
| Key Type | Storage Method | Protection Level |
|---------------|----------------|------------------|
| Identity (XWing private) | iOS Keychain | kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly |
| Group keys (AES) | iOS Keychain | kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly |
| Peer public keys | In-memory only | None (ephemeral) |
### Forward Secrecy
Current implementation uses static XWing keys for identity. **Forward secrecy is limited** — if a private key is compromised, past messages may be decrypted. Future versions should implement ephemeral key exchange per session.
### Authentication
The current protocol does not include message signatures. `senderID` in envelopes is not cryptographically verified. A malicious peer could spoof another peer's ID in the mesh. Applications requiring strong authentication should add Ed25519 signatures to envelopes.
---
## 8. Android implementation notes
### Recommended libraries
| Component | Android library |
|-----------------|----------------------------------------------|
| XWing KEM | BouncyCastle 1.78+ (supports ML-KEM + X25519)|
| HKDF-SHA256 | BouncyCastle or standard `javax.crypto` |
| AES-256-GCM | `javax.crypto.Cipher` (standard JCE) |
| JSON | `kotlinx.serialization` or Gson |
### BouncyCastle XWing
```kotlin
// BouncyCastle 1.78+ supports ML-KEM and X25519
// You'll need to implement XWing hybrid construction
val kemParams = MLKEMParameters.ml_kem_768
val x25519Params = X25519KeyGenerationParameters(secureRandom)
// Combine according to XWing specification
```
> **Note:** As of 2024, native XWing support in BouncyCastle is pending. You may need to implement the hybrid construction manually using ML-KEM-768 + X25519.
### Transport for iOS ↔ Android
MultipeerConnectivity is iOS/macOS only. For cross-platform:
1. **Same Wi-Fi LAN**: Use mDNS (Android `NsdManager`) to advertise the service `_btmsg-pqc._tcp`, then connect via TCP. The JSON envelope format remains identical.
2. **Over internet**: Use a lightweight relay (WebSocket server) — peers exchange envelopes via the relay, same JSON format, same crypto.
---
## 9. Versioning
The HKDF `info` string `"btmessage-v1"` acts as a protocol version discriminator.
Future breaking changes must increment this value (e.g. `"btmessage-v2"`).
### Protocol Versions
| Version | Encryption | Status |
|---------|-----------|--------|
| v1 (current) | XWingMLKEM768X25519 | Active |
| v0 (legacy) | X25519 only | Deprecated |
---
## 10. Complete message flow example
```
Alice (iOS) relay/mesh Bob (Android)
| |
|--- keyExchange: Alice_xwing_pub (1216 bytes) ------>|
|<-- keyExchange: Bob_xwing_pub (1216 bytes) ---------|
| |
| Alice types "Hello Bob" |
| → encapsulate with Bob's XWing key |
| → derive AES key via HKDF |
| → AES-256-GCM encrypt |
| |
|--- chat envelope: ChatPayload --------------------- >|
| kemCiphertext: XWing encapsulation (~1120 bytes) |
| aesCiphertext: nonce+ct+tag |
| |
| Bob decapsulates with priv key |
| → derives AES key |
| → decrypts |
| → "Hello Bob" |
| |
|<-- ack envelope: messageID -------------------------|
```
---
## License
This protocol specification is provided under the same license as the btmessage project (Bastien-mrq License v1.0.0).