# WHN-MESH v1 — Offline Distress Relay Protocol

**Status:** Draft for public review · **Version:** 1.0 · **Date:** 2026-08-05
**Editor:** Dünya İnsanlık Ağı Derneği (World Humanity Network)
**Contact:** info@worldhumanitynetwork.org

This document specifies WHN-MESH, a Bluetooth Low Energy protocol that lets a
distress call travel from phone to phone when cellular and internet
infrastructure is unavailable, until some device with connectivity delivers it
to a server.

The protocol is published openly so that other humanitarian organisations,
disaster management agencies and application vendors can implement it. A
distress network is only as useful as it is dense; a format that only one
application speaks is worth less than one many applications speak.

**Implementation status:** WHN-MESH v1 is implemented and field-verified in the
World Humanity Network Android application. Single-device transmission, relay
scheduling, storage and server-side verification are verified; multi-device
propagation testing is in progress.

---

## 1. Terminology

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted
as described in RFC 2119.

- **Originator** — the device whose user raised the distress call.
- **Relay** — a device that receives a packet over BLE and re-broadcasts it.
- **Uploader** — a device that delivers a stored packet to a server.
- **Registration authority** — the service that issues device identities and
  holds the corresponding secret keys.

A single device usually performs all three roles.

---

## 2. Design constraints

The entire design follows from one hard limit.

A BLE 4.x legacy advertisement carries **31 bytes** total. After the mandatory
structures, the usable payload is 20 bytes:

```
  Flags AD structure                        3 bytes
  16-bit service UUID list                  4 bytes   (required for scan filtering)
  Service Data AD header + UUID             4 bytes
  ------------------------------------------------
  Remaining payload                        20 bytes
                                           --------
                                           31 bytes
```

BLE 5 extended advertising offers more room but depends on the chipset and is
absent from the inexpensive and older handsets that are common in disaster
areas. **The base layer MUST fit in 20 bytes.** Implementations MAY define
extended profiles, but MUST remain able to originate and relay v1 packets.

Consequences that implementers frequently get wrong:

- The advertisement MUST NOT include the device name or the TX power level.
  Either one overflows the budget and `startAdvertising` fails silently with a
  data-too-large error.
- Scanning MUST use a hardware service-UUID filter. On Android 8.1 and later an
  unfiltered scan returns **no results at all** while the screen is off — which
  is precisely the state of a phone in someone's pocket during a disaster.

---

## 3. Wire format

A packet is exactly **20 bytes**. All multi-byte integers are big-endian.

```
 byte  0  1  2  3 │  4  5  6 │  7  8  9 │ 10 11 │ 12 13 │ 14 .. 19
       senderId   │ latitude │ longitude│ minute│ flags │ authentication tag
```

The 16-bit flags field is packed as:

```
 bit  15 14 13 │ 12 11 10  9 │  8  7  6  5 │  4  3  2  1  0
      category │   battery   │     hop     │    sequence
```

### 3.1 Fields

| Field | Size | Encoding |
|---|---|---|
| `senderId` | 4 bytes | Opaque device identity issued by the registration authority (§5). |
| `latitude` | 24 bits | `round((lat + 90) / 180 × 16777215)`, `lat` clamped to [-90, 90]. Resolution ≈ 1.2 m. |
| `longitude` | 24 bits | `round((lng + 180) / 360 × 16777215)`, `lng` clamped to [-180, 180]. Resolution ≈ 2.4 m at the equator. |
| `minute` | 16 bits | Minutes since the Unix epoch, modulo 65536. Gives a ~45-day unambiguous window; the server resolves the wrap using its own clock. |
| `category` | 3 bits | 0 general · 1 injured · 2 trapped · 3 lost · 4 unreachable. Values 5–7 reserved. |
| `battery` | 4 bits | `round(percent × 15 / 100)`. Decoded as `round(raw × 100 / 15)`. |
| `hop` | 4 bits | Number of relays traversed. Originator sets 0. |
| `sequence` | 5 bits | Distinguishes consecutive calls from the same originator. Wraps at 32. |
| tag | 6 bytes | Authentication tag (§4). |

### 3.2 Rounding is normative

The battery conversion MUST round, not truncate. With truncation the mapping is
not a fixed point — 47 % encodes to 7, decodes to 46 %, re-encodes to 6 — so a
relay that decoded and re-encoded a packet would alter the bytes and invalidate
the authentication tag. With rounding, 47 → 7 → 47 → 7.

This is one reason for the rule in §6.2: relays MUST NOT re-encode.

---

## 4. Authentication

A distress network that anyone can write to is worse than no network: false
calls send rescue teams to empty addresses. Every packet therefore carries a
tag that only the originator and the registration authority can produce.

### 4.1 Computation

```
tag = HMAC-SHA256(device_key, signed_prefix)[0..5]      // first 6 bytes
```

`signed_prefix` is bytes 0–13 of the packet **with the four `hop` bits set to
zero**. `device_key` is a 32-byte secret shared between the device and the
registration authority at registration time.

### 4.2 Why `hop` is masked and nothing else is

`hop` changes at every relay. If it were covered by the tag, a call would fail
verification after its first hop and the network would never work at all.

Category, battery and sequence share the same two bytes but **remain inside the
signed prefix**. A relay therefore cannot alter the nature of a call it is
carrying — changing "trapped" to "general", or renumbering it — without the
server rejecting the packet.

### 4.3 Tag length

48 bits is what the 20-byte budget leaves. It gives roughly a 1-in-281-trillion
forgery chance per attempt, which combined with server-side rate limiting is
sufficient. Asymmetric signatures were considered and rejected: an Ed25519
signature is 64 bytes, more than three times the whole packet.

The cost of symmetric authentication is that **relays cannot verify anything**.
That is acceptable, and by design: a relay only carries; the server decides.

---

## 5. Identity

`senderId` MUST be issued by the registration authority and MUST NOT be chosen
by the client.

If a client could choose its own identity it could register a key under another
person's identity and thereby make that person's genuine distress calls
unverifiable, or fabricate calls in their name.

The reference implementation derives it as the first 4 bytes of
`HMAC-SHA256(server_salt, user_id)`. The salt never leaves the server, so a
`senderId` observed over the air cannot be reversed to a user identity.

### 5.1 Collisions are expected

Four bytes is not enough to be collision-free. Measured: about 2 collisions per
120,000 users. The registration authority MUST therefore store a **list** of
keys per `senderId` and try each of them during verification. Storing one key
per identity would let a second user silently overwrite the first user's key,
after which the first user's calls could never be verified again.

---

## 6. Relay behaviour

### 6.1 Decision rules

A receiving device MUST apply, in order:

1. **Duplicate suppression.** If `(senderId, sequence)` has been seen before,
   do not re-broadcast. Implementations SHOULD remember keys for 24 hours and
   MUST persist this table across restarts. Without persistence a device that
   restarts — common on low battery during a disaster — re-broadcasts every
   call it has ever seen, and the network floods itself.
2. **Hop limit.** If `hop >= 15`, do not re-broadcast.
3. **Random delay.** Wait a uniformly random 0–3 s before re-broadcasting.
   Without it, every device in range answers simultaneously and the
   advertising channel is unusable.

### 6.2 Relays MUST NOT re-encode

A relay MUST increment the `hop` field **in place, on the received bytes**. It
MUST NOT decode the packet to a structure and re-encode it.

Re-encoding passes through lossy fields (§3.2) and can change bytes the tag
covers. Only the 4 `hop` bits in byte 13 may change; the other 19 bytes and the
tag MUST be forwarded exactly as the originator emitted them.

### 6.3 Storage is independent of relaying

A device SHOULD store every packet it receives, including packets it decides
not to relay. A call that has reached the hop limit is still worth delivering:
the receiving device may be the only one that will ever reach the internet.

Stored packets MUST be delivered when connectivity returns, and MUST NOT be
discarded for age while still undelivered.

---

## 7. BLE transport

| Parameter | Value |
|---|---|
| Service UUID | 16-bit, currently `0xFFD1` (see §9) |
| Payload location | Service Data for the service UUID |
| Advertising duration per packet | 30 s |
| Scan mode | Low latency, all matches, hardware filter on the service UUID |
| Device name in advertisement | MUST be disabled |
| TX power in advertisement | MUST be disabled |

Implementations SHOULD duty-cycle scanning when no crisis is declared. The
reference implementation scans 10 s every 5 minutes in its standby mode and
continuously in crisis mode.

---

## 8. Test vector

An implementation is conformant only if it reproduces this vector exactly. It
is pinned in the reference implementation's Kotlin and TypeScript test suites
for precisely this purpose — the two sides diverging is a silent failure, in
which the server accepts nothing and no one is told.

```
device_key   = 00 01 02 ... 1F        (32 bytes, key[i] = i)
senderId     = 0x1A2B3C4D
latitude     = 39.9042
longitude    = 41.2658
minute       = 12345
category     = 2 (trapped)
battery      = 47 %
hop          = 0
sequence     = 7

packet       = 1A2B3C4D B8C0AC 9D5835 3039 4E07 58E540491501
```

Byte by byte: `1A2B3C4D` identity · `B8C0AC` latitude · `9D5835` longitude ·
`3039` minute (12345) · `4E07` flags (category 2, battery 7/15, hop 0,
sequence 7) · `58E540491501` tag.

---

## 9. Open issues

**The service UUID is not allocated.** `0xFFD1` is inside the range commonly
used for vendor and experimental purposes and is **not** assigned by the
Bluetooth SIG. It may therefore collide with unrelated products. A collision is
not a security problem — a foreign advertisement either is not 20 bytes and
fails to decode, or fails authentication — but it wastes scanning effort. An
allocated UUID MUST be obtained before large-scale deployment, and this
specification updated.

---

## 10. Versioning and compatibility

The v1 wire format is **frozen**. During a disaster it is not possible to ask
everyone to update first, so a packet emitted by an old handset must remain
decodable by a new one.

- Fields MUST NOT be resized, reordered or reinterpreted.
- Category values MUST NOT be reassigned; new categories may only be appended
  into the reserved range 5–7.
- Any change that alters the meaning of existing bytes MUST use a **different
  service UUID** and a version byte, so that the two protocols coexist on air
  rather than corrupt each other.

---

## 11. Privacy considerations

- No raw user identifier is transmitted. The over-the-air identity is 4 bytes
  of a salted hash and cannot be reversed without the server-side salt.
- A relay learns that *a* distress call exists near it; it does not learn whose.
- Location is transmitted only when the user themselves raises a call.
- Participation as a relay MUST be under the user's control and MUST be
  disclosed clearly. Carrying other people's calls consumes the user's battery;
  doing so without saying so is not acceptable.
- Implementations MUST NOT use the protocol to derive presence or movement of
  identified individuals.

---

## 12. Licence and participation

This specification is published for open implementation. Implementers,
reviewers and agencies wishing to interoperate — or to run their own
registration authority for their own user base — are invited to contact the
editor.

The reference implementation's design rationale, including the failures that
shaped these rules, is documented in `docs/kesintisiz-ag.md` (Turkish).
