Communication protocol
This document describes the app-level communication protocol between ClickStick dongle and companion app. It works on top of a secure BLE link:
- BLE link-layer authentication and encryption protects against passive RF eavesdropping and classic main-in-the-middle (MitM) attacks.
- App-level protocol blocks out unauthorized apps and adds an independent encryption layer, should BLE keys be compromised.
General concepts
Participants
- User — a person with physical access to the host, dongle and smartphone
- Dongle (also "device" or "peripheral", as appropriate)
- Companion app or smartphone (also "app", "mobile" or "central", as appropriate)
Channels
The dongle starts a custom BLE service with two characteristics, one per direction:
statuscharacteristic (peripheral to central)- Its value reflects the dongle state
- Encrypted and authenticated at BLE link layer
- No app-level encryption, but some states include a MAC
- Permissions:
- Read and notify (push without acknowledgement)
- Security level 4 (accessible to bonded central only)
commandcharacteristic (central to peripheral)- Accepts encrypted command packets to the dongle
- Encrypted and authenticated at BLE link layer
- Encrypted at the app level, too
- Permissions:
- Write with acknowledgement (in case of errors, delivery is retried and central is notified should delivery fail)
- Security level 4 (accessible to bonded central only)
Both characteristics are accessible only when the link is established at security level 4 ("Authenticated LE Secure Connections pairing with encryption"). Connection attempts at lower security levels shall be rejected by BLE stack.
Hardware info service
Several auxiliary BLE characteristcs report dongle's firmware version, manufacturer name — and a signature verifying their authenticity using manufacturer's private key from EPS32 eFuses. These characteristcs are read-only, accessible to bonded devices only, without app-level encryption.
Dongle states
The dongle publishes its current status via the status characteristic. The first byte is the state identifier, followed by optional extra data as described below.
INIT_SESSION
Indicates that the dongle needs to establish or re-establish the session.
| State ID (1 byte) | Protocol version (1 byte) | ECDH public key (32 bytes) | MAC (16 bytes) |
|---|---|---|---|
0x00 (INIT_SESSION) - | 0x01 | dongle's public key | HMAC_appAuthKey(version ‖ donglePublicKey) (first 16 bytes) |
IDLE
Indicates that the dongle awaits commands (and there is an active session).
| State ID (1 byte) |
|---|
0x01 (IDLE) |
BUSY
Indicates that the dongle is busy executing a command (and there is an active session).
| State ID (1 byte) |
|---|
0x02 (BUSY) |
⚠️ About status protection
Dongle states are purely advisory, do not contain sensitive data and thus are published only with link-level protection. Activity analysis by a passive observer is mitigated by activity obfuscation.
Initial setup
The initial setup process is summarized in the sequence diagram and further elaborated below:
BLE bonding
- The dongle configures BLE security mode 1 level 4 ("Authenticated LE Secure Connections pairing with encryption")[1] and won't accept connections with lower security level.
- The dongle disables its BLE Filter Accept List to allow pairing of new devices.
- The dongle starts BLE advertising. If no mobile is paired within 60 seconds, the dongle stops BLE advertising and notifies the user.
- The mobile (central) and dongle (peripheral) bond using BLE Secure Connections with Numeric Comparison:
- Both peers display a 6-digit pairing code
- The user physically confirms the code match on both devices
- BLE stack stores the Long-Term Key (LTK) used for link-layer encryption in future connections.
- The dongle adds the central to the BLE Filter Accept List and re-activates filtering. Connection from non-bonded devices will be ignored by the BLE stack.
App pairing
After successful BLE bonding, the dongle generates a secret 256-bit app pairing key (appAuthKey) and shares it, out-of-band, with the mobile app. The key is the basis of app authentication (SR-AppAuth) and is preserved by both peers in their secure storage (SR-SecureStore).
The app pairing key is generated by dongle's hardware CSRNG and shown on dongle's display as a QR code. The code is then scanned by the app from a close range. Due to the small display size, the QR code is difficult to eavesdrop from afar. The key is never transmitted over the air. The QR code is hidden as soon as peers successfully establish the first session.
Key rotation
The user can regenerate the appAuthKey by performing a factory reset via dongle's UI.
Connection session
Session start
Every connection session starts with establishing a unique sessionKey shared by the peers.
- The dongle initiates the session by generating an ephemeral ECDH keypair (
dongleSecretKey,donglePublicKey) using X25519; each key is 32 bytes. The keypair is generated anew for every session. - The dongle publishes the
INIT_SESSIONstatus along with thedonglePublicKey. - The app reads the
INIT_SESSIONpayload and verifies itsMACfield using the previously storedappAuthKey.- In case of mismatch, the app warns the user about dongle impersonation and aborts the connection.
- If
MACis valid, the app proceeds to the next steps.
- The app checks dongle's protocol
version; aborts the connection if version is unsupported (too old, too new, or vulnerable). - The app generates its own ephemeral ECDH keypair (
mobileSecretKey,mobilePublicKey) - The app sends the START_SESSION command containing the
mobilePublicKey. Since the dongle does not have thesessionKeyyet, the command is wrapped into the standard packet structure withseq = 0and a zero-filled encryption key. That is,START_SESSIONcommand and its payload are transmitted essentially in plaintext. - After sending the command, the app computes:
sharedSecret := ECDH(mobileSecretKey, donglePublicKey)
sessionKey := HKDF(sharedSecret, appAuthKey)
seq := 0- After receiving
mobilePublicKey, the dongle computes:
sharedSecret := ECDH(dongleSecretKey, mobilePublicKey)
sessionKey := HKDF(sharedSecret, appAuthKey)
seq := 0Now that both peers have a shared sessionKey, the mobile app can send encrypted packets to the dongle.
Packet structure
| Field | Size | Description |
|---|---|---|
seq | 2 bytes | Packet number |
encCommand | 0 – MAX_COMMAND_LENGTH bytes | Encrypted command |
MAC | 16 bytes | Message authentication code |
where:
seq— packet counter, monotonically increasing after every packet. Once it reaches a certain threshold, the dongle reinitiates the session with a newsessionKey.encCommand := AES-CTR(command, sessionKey, IV)command— instruction to the dongle (see Commands)IV := SHA256(sessionKey || seq), truncated to first 16 bytes; unique for each packet and session.
MAC := HMAC_sessionKey(seq || encCommand), truncated to first 16 bytes.MAX_COMMAND_LENGTHis 226 bytes. For performance reasons, we want every packet to fit in a single BLE PHY packet; this constraints our encrypted packets to ATT Payload size of 244 bytes (with Bluetooth 4.2 Data Length Extensions).
Packet processing
After receiving a packet, the dongle:
- Sends a link-level acknowledgement of receipt (handled by the BLE stack)
- Verifies and sanity-checks the packet
- Checks packet size, drops too small and too large packets
- Calculates
HMAC_sessionKey(seq || encCommand)and compares it with the receivedMAC. In case of mismatch, drops the packet and increases the error counter. - Checks that
seqmatches the expected value (replay window size is 1). In case of mismatch, drops the packet and increases the error counter. - If error counter > 3, initiates a cool-off period (SR-Cooloff) followed by session restart.
- Decrypts the command and adds it to the internal command queue (sorted by decreasing priority). If the queue is full, drops the last command in the queue (so that high-priorty commands can still be processed).
- Increases the internal
seqcounter. If its value has reached the key rotation threshold, initiates session restart. - Command processing runs in a background queue, when the dongle is idle.
INIT_SESSIONstate pauses processing of all commands exceptSTART_SESSION.
Activity obfuscation (Cover traffic)
In order to blur user's activity and communication patterns from a passive RF observer, the mobile app periodically sends activity-simulating commands. Specifically:
- The mobile sends a
PAUSEcommand with random-length payload, at a random 5 – 15 s interval (jittered). - The dongle responds by publishing the BUSY state for the requested duration.
- To an outside observer, cover activity appears as normal traffic.
Session lifecycle
A new session is triggered in any of these cases:
- After the initial app pairing
- After dongle boot (if
appAuthKeyis already present) - When
seqexceeds 1000. (To enforce regular rotation of session keys.)
Security checklist
| Property | Mechanism |
|---|---|
| Confidentiality | BLE AES-CCM (link) + AES-CTR (app layer) |
| Integrity | BLE MIC* (link) + truncated HMAC-SHA256 (app layer) |
| Replay Protection | SEQ with window size 1 |
| Key Freshness | Ephemeral ECDH per session, regularly rotated |
| Key Confidentiality | appAuthKey, sessionKey never sent over BLE |
| Metadata Privacy | Activity obfuscation |
* BLE MIC is a 4-byte CCM tag added by the controller
BLE security mode 2 supports data signing, but only legacy pairing and no encryption. See Vol 3, Part C, Section 10.2 of BLE v4.2 specification. ↩︎