# Blemesh Protocol Specification (Loxation)
Author: Jonathan Borden <jonathan@loxation.com>
Status: Stable draft  
Spec version: 1.0 (aligned to ProtocolVersion.current = 1 in code)  


This document defines the on‑device Blemesh protocol for decentralized, peer‑to‑peer messaging over Bluetooth LE (BLE), including:
- Binary packet format and message types
- Routing, TTL, deduplication, fragmentation, and reassembly
- 1:1 Noise direct messages (DMs), delivery and read receipts
- MLS group messaging over Blemesh, including attachments
- Version negotiation, presence/identity, privacy, and security considerations
- Interoperability with Nostr (NIP‑17, NIP‑44/EE) for optional relay fallback

Blemesh is dervived from the Bitchat protocol but not binary compatible. It is extended for MLS group messaging and Loxation announcements, queries. Large binary attachments are encrypted end to end and uploaded and downloaded from the loxation REST server.

--------------------------------------------------------------------------------

0. Table of Contents

1. Layering and Architecture  
2. Identifiers and Addressing  
3. Core Packet Format (BlemeshPacket) and Encoding  
4. Message Types and Semantics  
5. Routing, TTL, Fragmentation/Reassembly, Deduplication  
6. Noise 1:1 Messaging over Blemesh  
   6.1 Session model and handshake  
   6.2 Encrypted payload types  
   6.3 Delivery acknowledgments and read receipts  
   6.4 Private attachments (NIP‑XXX-aligned)  
7. MLS over Blemesh  
   7.1 Envelope, processing, and dedupe  
   7.2 Sending policy and Nostr fallback  
   7.3 MLS group attachments (exporter‑derived AEAD)  
8. Version Negotiation  
9. Identity, Presence, and Announcements  
10. Privacy and Security Considerations  
11. Error Handling, ACK/NACK, Status  
12. Worked Examples  
13. Compatibility and Extensibility Notes  
14. Appendix: Binary helpers and utilities


--------------------------------------------------------------------------------

1. Layering and Architecture

Conceptual stack:

App features (chat UI, attachments, groups)
↑
ChatViewModel (routing, threading, UX state)
↑
Blemesh protocol (packets, message types, acks/receipts)
↑
Noise (1:1 E2EE) and MLS (group E2EE)
↑
BLEMeshService (BLE broadcast/relay, TTL, dedupe, fragmentation)
↑
Bluetooth LE

Key roles:
- BLEMeshService: Broadcast/relay BlemeshPacket frames, decrement TTL, de/fragment payloads, and deliver upward. See BLEMeshService+MLS for MLS-over-BLE envelope.
- Noise subsystem: Lazily establishes X25519-HKDF-AESGCM sessions; encrypts 1:1 payloads; supports key wrap for attachment K.
- MLS subsystem: Creates/consumes MLS inner messages; supports exporter for per-attachment keys.
- SwiftMLSIntegratedService: Orchestrates Nostr subscriptions/fallback and “smart” routing decisions for group messages.
- ChatViewModel: UI-level routing for main feed, 1:1 threads, and group threads. Converts protocol events to user-visible messages.


--------------------------------------------------------------------------------

2. Identifiers and Addressing

- PeerID (8 bytes, hex string in code): Ephemeral transport address derived from Noise static public key:  
  PeerID = first 8 bytes of hex(SHA‑256(noise_static_public_key))  
  Ref: PeerIDUtils.derivePeerID(fromPublicKey:), NoisePeerIDFactory.fromHexString.

- DeviceID (opaque, stable): Canonical ID for profiles, favorites, chat threading. LoxationProfileManager maintains mappings DeviceID ⇄ current PeerID.

- Nostr Identity (optional): Public relay identity (npub/hex) for global sync. Mappings stored in profiles for relay fallback and enrichment.


--------------------------------------------------------------------------------

3. Core Packet Format (BlemeshPacket) and Encoding

All BLE traffic is carried in BlemeshPacket:

```swift
struct BlemeshPacket: Codable {
  let version: UInt8            // currently 1
  let type: UInt8               // MessageType (see §4)
  let senderID: Data            // 8-byte PeerID (binary)
  let recipientID: Data?        // 8-byte PeerID, or nil for broadcast
  let timestamp: UInt64         // ms since epoch
  let payload: Data             // binary or JSON depending on type
  let signature: Data?          // optional
  var ttl: UInt8                // hop limit
}
```

Encoding/decoding:
- BinaryProtocol.encode(decode) produces the on-wire frame. Over BLE we pass padding=false to reduce fragmentation pressure.
- Multi-byte integers are big-endian.
- Packets exceeding BLE MTU (~512 bytes) are fragmented/reassembled transparently by the transport layer; the application payload format remains unchanged.


--------------------------------------------------------------------------------

4. Message Types and Semantics

Defined in BlemeshProtocol.swift:

User/system and transport:
- 0x01 announce
- 0x03 leave
- 0x04 message (public chat, may also be used for some UI/system display)
- 0x05 fragment (internal fragmentation framing, handled below transport)

Delivery semantics:
- 0x0A deliveryAck (DeliveryAck)
- 0x0B deliveryStatusRequest
- 0x0C readReceipt (ReadReceipt)

Noise (1:1 E2EE):
- 0x10 noiseHandshake (initial & response frames)
- 0x12 noiseEncrypted (data after session established)
- 0x13 noiseIdentityAnnounce (static key announce)

Version negotiation:
- 0x20 versionHello
- 0x21 versionAck

Protocol-level ack/ping:
- 0x22 protocolAck
- 0x23 protocolNack
- 0x24 systemValidation
- 0x25 handshakeRequest (request a handshake for pending messages)

Social:
- 0x30 favorited
- 0x31 unfavorited

Loxation extensions and location:
- 0x40 loxationAnnounce
- 0x41 loxationQuery
- 0x42 loxationChunk
- 0x43 loxationComplete
- 0x44 locationUpdate
- 0x45 uwbRanging
- 0x46 proximityAlert
- 0x47 beaconContext

MLS over BLE:
- 0x48 mlsMessage (MLS inner ciphertext envelope)

WebRTC signaling:
- 0x50 webrtcSDP
- 0x51 webrtcICE


--------------------------------------------------------------------------------

5. Routing, TTL, Fragmentation/Reassembly, Deduplication

- TTL: Each broadcast hop decrements ttl. When ttl reaches 0 the packet is dropped. Typical defaults: 5 for MLS group transmissions over BLE (see BLEMeshService+MLS).
- RecipientID: If nil, packet is broadcast. Unicast frames set recipientID.
- Fragmentation: BLEMeshService handles fragmentation and reassembly below the application level. Applications write/read full BlemeshPacket payloads.
- Deduplication:
  - MLS-over-BLE dedupe is content-based to avoid replays across relays:  
    key = groupId "|" bytes.count "|" hex(prefix(8, SHA256(ciphertext)))  
    A fixed-size set is used; repeated keys are dropped.
  - Nostr relay paths maintain a relay event-id set to avoid duplicates across relays (SwiftMLSIntegratedService.seenEvents).


--------------------------------------------------------------------------------

6. Noise 1:1 Messaging over Blemesh

6.1 Session model and handshake

- Each peer advertises a Noise static public key (Curve25519) via announce/identity frames. PeerID is derived from this key.
- Sessions are established lazily when the user opens a private chat or attempts to send a DM or attachment.
- High-level state: `LazyHandshakeState { none, handshakeQueued, handshaking, established, failed }`.
- Handshake frames use MessageType.noiseHandshake (0x10). Once established, traffic uses .noiseEncrypted (0x12).

6.2 Encrypted payload types

Within the .noiseEncrypted channel, the payload type is `NoisePayloadType`:
- 0x01 privateMessage: Chat payload (BlemeshMessage serialized; app-level)
- 0x02 delivered: DeliveryAck (encrypted)
- 0x03 readReceipt: ReadReceipt (encrypted)
- 0x04 binaryAttachment: Encrypted attachment metadata path (see below)
- 0x05 mlsWelcome: Per-recipient MLS Welcome envelope (Noise 1:1 bootstrap)

The Noise transport uses AEAD (AES‑GCM) derived from X25519 ephemeral-static handshakes (HKDF‑SHA256). Session state and rekeying are managed by the Noise subsystem.

6.3 Delivery acknowledgments and read receipts

- DeliveryAck and ReadReceipt are first-class binary structures with explicit codecs that can be sent either via Noise (as encrypted NoisePayloadType frames) or mapped at the Blemesh level when needed.
- ChatViewModel uses DeliveryTracker and observes ACKs/receipts to update delivery states: sending → sent → delivered → read.
- Read receipts are emitted when a private chat is visible. Routing prefers mesh if the peer is connected, else falls back to Nostr when a mapping exists.

6.4 Private attachments (NIP‑XXX aligned)

Two Noise-compatible paths are supported:

A) Mesh/Noise key-wrap path (encMode "dm" with wrapped key; Blemesh):
- BinaryAttachmentService.uploadDM:
  1) Generates random K (32 bytes).
  2) AES‑GCM encrypts the plaintext with random IV → ciphertext+tag.
  3) Uploads ciphertext to storage via presigned URL (application/octet-stream).
  4) Wraps K using X25519 ECDH with recipient’s static key, HKDF derive wrapKey, AES‑GCM wrap (ek), carries: ek, epk, wrapNonce, wrapSalt.
  5) Metadata: { encMode: "dm", iv, tag, ek, epk, wrapAlg: "X25519-HKDF-AESGCM" } (+ url/size/ct/fn/checksum).
  6) This metadata is delivered inside the Noise 1:1 channel (NoisePayloadType.binaryAttachment) as JSON or via a small typed envelope. Receiver unwraps with their static secret and decrypts the blob.

B) NIP‑17 DM path (Nostr fallback) (encMode "dm" with plaintext K inside encrypted DM content; non-Blemesh):
- BinaryAttachmentService.uploadForNIP17() returns (metadata, keyB64).
- The DM plaintext JSON (inside NIP-17 E2EE envelope) embeds:
  ```
  {
    "type":"message",
    "attachments":[
      {
        "url":"https://.../blob",
        "ct":"image/jpeg",
        "size":12345,
        "sha256":"<hex of ciphertext>",
        "fn":"photo.jpg",
        "enc": { "mode":"dm", "algo":"A256GCM", "k":"<base64>", "iv":"<b64>", "t":"<b64>" }
      }
    ]
  }
  ```
- On receipt, the client downloads ciphertext and decrypts using k/iv/t provided in the DM content, verifying optional sha256 over ciphertext.


--------------------------------------------------------------------------------

7. MLS over Blemesh

7.1 Envelope, processing, and dedupe

MLS group messages over BLE use MessageType.mlsMessage (0x48) with a compact JSON envelope sent in BlemeshPacket.payload:

```json
{
  "groupId": "<hex group id>",
  "epoch": 42,             // optional hint (UInt64)
  "message": "<raw-bytes>" // MLS framed bytes (ciphertext)
}
```

- BLEMeshService+MLS.swift defines MLSWireEnvelope and encodes/decodes as JSON.
- Dedupe uses content fingerprint as defined in §5.
- Receiver calls MLSEncryptionService.processMessage(groupId, receiverId, messageB64) to process proposals/commits or to return plaintext for application messages.
- ChatViewModel routes messages per groupId and renders with “<@sender> …” formatting. Sender mapping comes from BLE senderPeerID or from Nostr pubkey on the Nostr path.

7.2 Sending policy and Nostr fallback

Smart policy for group sends (SwiftMLSIntegratedService.sendGroupMessageSmart):
- If all other group members are currently reachable on BLE mesh (peer present and connected), send via BLE only (fast, local).
- Otherwise publish via Nostr (kind 445), avoiding dual-publish to reduce dupes. SwiftMLSIntegratedService also manages subscriptions.
- BLE path: BLEMeshService.sendMLSApplication creates MLS application (base64) via MLSEncryptionService and broadcasts MLSWireEnvelope with ttl=5.

7.3 MLS group attachments (exporter‑derived AEAD)

- Upload path (BinaryAttachmentService.uploadMLS):
  1) MLS exporter derives key and nonce (A256GCM) using:
     - label "attachment" (key) and "attachment-nonce" (nonce)
     - context: "\(epoch)|\(filename)" (sender-provided epoch)
  2) AES‑GCM encrypts content with derived key/nonce.
  3) Upload encrypted bytes to storage; return metadata:
     ```
     {
       "encMode":"mls",
       "iv":"<b64>",
       "tag":"<b64>",
       "mlsGroupId":"<groupId>",
       "mlsEpoch": <uint64>,
       // plus: url/blobId/ct/size/filename/checksum
     }
     ```
- Group message payload (application JSON inside MLS content, NIP‑XXX aligned):
  ```
  {
    "type":"message",
    "attachments":[
      {
        "url":"https://.../blob",
        "ct":"image/jpeg",
        "size":12345,
        "sha256":"<hex>",
        "fn":"photo.jpg",
        "enc": { "mode":"mls", "algo":"A256GCM", "iv":"<b64>", "t":"<b64>" },
        "mls": { "group_id":"<gid>", "epoch": <uint64>, "ctx":"<blobId>" }
      }
    ]
  }
  ```
- Download/decrypt path (BinaryAttachmentService.downloadAndDecryptAttachment):
  - If encMode == "mls", derive key via MLS exporter `exportSecret(groupId, label:"attachment", context:"<epoch>|<blobId or messageId>", length:32)`, and decrypt with iv/tag from metadata.

Notes:
- We include iv/tag in the metadata for MLS to avoid placing key material in metadata. Receiver uses exporter locally to reproduce K (no ek/epk).
- The UI renders “paperclip” rows with filename/type/size and an “Open” button when the local file is ready.


--------------------------------------------------------------------------------

8. Version Negotiation

- Peers can optionally exchange VersionHello (0x20) and VersionAck (0x21) to agree on a protocol version.
- Current implementation supports only v1 (ProtocolVersion.supportedVersions = {1}).
- VersionHello carries supported versions, preferredVersion, clientVersion, platform, optional capabilities[].
- VersionAck returns agreedVersion and optional capabilities/reason if rejected.

Negotiation strategy: Agree on highest common version. If none, reject with reason.


--------------------------------------------------------------------------------

9. Identity, Presence, and Announcements

9.1 NoiseIdentityAnnouncement (JSON + binary)
- Binds current PeerID to Noise static public key and Ed25519 signing key, with nickname and timestamp.
- Supports identity rotation through previousPeerID chaining.
- Binary format uses a flags byte to indicate presence of optional fields.

9.2 LoxationAnnouncement (JSON + v2/v3 binary)
- Binds DeviceID to current PeerID; includes timestamp, signature, and optionally:
  - locationId, uwbToken, profilePhotoAssetId
  - MC discovery info, essential profile (nickname/emojis)
  - signingPublicKey, Nostr npub (v3 ext)
- Privacy: In initial BLE announce TLVs, DeviceID is omitted to avoid cleartext device tracking; the signed LoxationAnnouncement conveys it at the application layer.

PeerID derivation: PeerIDUtils.derivePeerID(fromPublicKey:). Mappings DeviceID ⇄ PeerID maintained by profile manager.


--------------------------------------------------------------------------------

10. Privacy and Security Considerations

- Padding (MessagePadding): PKCS#7-style padding with random bytes to obscure plaintext length; for large pads, an extended trailer is used. Over BLE, BinaryProtocol padding is disabled to reduce fragmentation pressure.
- No persistent identifiers in low-level headers; DeviceID avoided in cleartext BLE TLVs.
- MLS over BLE uses content-based dedupe to mitigate replay floods across relays.
- Read receipts are only sent when appropriate UX indicates content is visible.
- Blocking: Messages from blocked users are dropped at the ViewModel boundary.
- Signature fields exist in certain announcements; integrity of control traffic should be verified where signatures are present.
- TTL enforces loop bounds and limits propagation.
- Key handling:
  - Noise DMs: Random per-attachment K, wrapped via X25519-HKDF-AESGCM (or plaintext K inside NIP‑17 DM content for Nostr fallback).
  - MLS groups: Exporter-derived K/nonce; never included in metadata.


--------------------------------------------------------------------------------

11. Error Handling, ACK/NACK, Status

- DeliveryAck acknowledges 1:1 message receipt (hopCount for diagnostics). ReadReceipt acknowledges display/read.
- ProtocolAck/ProtocolNack at the protocol level can be used for integrity or routing failures:
  - ProtocolNack includes reason and errorCode (checksumFailed, decryptionFailed, malformedPacket, unsupportedVersion, resourceExhausted, routingFailed, sessionExpired).
- MLS processing (`MLSEncryptionService.processMessage`) distinguishes proposal/commit vs application. Proposals/commits generally do not surface in UI; application returns plaintext for chat display.
- Dedupe caches (for MLS and Nostr events) bound memory and drop repeats.


--------------------------------------------------------------------------------

12. Worked Examples

12.1 Private DM over Blemesh (Noise)

Flow:
1) User opens private chat → handshake may be queued if no session (`LazyHandshakeState.handshakeQueued`).
2) Noise handshake frames exchanged (`noiseHandshake`), session becomes established.
3) User sends DM:
   - ChatViewModel builds BlemeshMessage (private, with mentions).
   - BLEMeshService sends encrypted Noise payload (`noiseEncrypted`, type=privateMessage).
   - Receiver decrypts, constructs BlemeshMessage with senderPeerID, stores under DeviceID thread; emits DeliveryAck.
4) Sender receives DeliveryAck, UI shows “delivered”.

12.2 Private attachment over Blemesh (Noise key-wrap path)

Sender:
- BinaryAttachmentService.uploadDM(data, filename, ct, recipientDeviceId):
  - Generates K; AES‑GCM encrypts; uploads ciphertext.
  - Builds metadata {encMode:"dm", iv, tag, ek, epk, wrapNonce, wrapSalt, url/size/ct/fn/checksum}.
  - Sends metadata via Noise (NoisePayloadType.binaryAttachment).
Receiver:
- Unwraps K using their static secret; downloads ciphertext; decrypts with iv/tag; persists to Documents/attachments/<messageId>/<filename>.
- UI renders “paperclip” entry and “Open” once file is ready.

12.3 MLS group message over BLE

Sender:
- MLSEncryptionService.createApplicationMessage → base64 MLS framed bytes.
- BLEMeshService sends MLSWireEnvelope JSON as MessageType.mlsMessage with ttl=5.  
Receiver:
- BLEMeshService+MLS decodes envelope; dedupe by (groupId|len|sha256_16prefix); passes to MLSEncryptionService.processMessage.
- If application: ChatViewModel adds to group thread; sender nickname resolved via senderPeerID (BLE) or event pubkey (Nostr path).

12.4 MLS group attachment

Sender:
- Determine current epoch (e.g., via `createNostrMLSMessageWithNip44(...).epoch`, discarded content).
- BinaryAttachmentService.uploadMLS derives K/nonce via exporter(label:"attachment"/"attachment-nonce", context:"epoch|filename"), encrypts, uploads.
- Build application JSON in MLS message:
  ```
  { "type":"message", "attachments":[
      { "url":..., "ct":"image/jpeg", "size":..., "sha256":..., "fn":"photo.jpg",
        "enc":{"mode":"mls","algo":"A256GCM","iv":"<b64>","t":"<b64>"},
        "mls":{"group_id":"<gid>","epoch":<uint64>,"ctx":"<blobId>"} }
  ]}
  ```
- Send via smart policy (BLE-only when all on mesh else Nostr).
Receiver:
- Parses attachments JSON; constructs metadata with encMode:"mls"; calls BinaryAttachmentService.downloadAndDecryptAttachment which derives K via exporter with context "epoch|ctx" and decrypts; UI marks attachment ready.


--------------------------------------------------------------------------------

13. Compatibility and Extensibility Notes

- Reserved ranges: Protocol includes numerous extensible TLVs and flags to enable forward compatibility (e.g., LoxationAnnouncement v2/v3 flags, NoiseIdentityAnnouncement flags).
- Decoders are tolerant to unknown TLVs/fields and should skip them.
- Subject to BLE constraints, large payloads should be minimized; attachments are always offloaded to a storage service with ciphertext-at-rest and presigned download.

Interoperability with Nostr:
- For groups, Nostr (kind 445) is used when mesh reachability is incomplete. The MLS inner remains identical; only the outer transport differs.
- For DMs, when mesh is not available but the peer is a mutual favorite with known npub, NIP‑17 is used with identical attachment payload schema except that the attachment K is embedded in the DM content instead of wrapped in metadata.


--------------------------------------------------------------------------------


Security notes summary:
- AEAD everywhere (Noise/MLS).
- No DeviceID in cleartext BLE TLVs.
- TTL + dedupe for replay/loop mitigation.
- Exporter-derived keys for MLS attachments (no ek/epk), iv/tag carried in metadata.
- X25519 wrap for DM attachment K in the pure Noise path; plaintext K only inside NIP‑17 E2EE content for relay fallback.

End of document.
