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