Skip to content

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:

  • status characteristic (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)
  • command characteristic (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) -0x01dongle's public keyHMAC_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_SESSION status along with the donglePublicKey.
  • The app reads the INIT_SESSION payload and verifies its MAC field using the previously stored appAuthKey.
    • In case of mismatch, the app warns the user about dongle impersonation and aborts the connection.
    • If MAC is 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 the sessionKey yet, the command is wrapped into the standard packet structure with seq = 0 and a zero-filled encryption key. That is, START_SESSION command 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 := 0

Now that both peers have a shared sessionKey, the mobile app can send encrypted packets to the dongle.

Packet structure

FieldSizeDescription
seq2 bytesPacket number
encCommand0 – MAX_COMMAND_LENGTH bytesEncrypted command
MAC16 bytesMessage authentication code

where:

  • seq — packet counter, monotonically increasing after every packet. Once it reaches a certain threshold, the dongle reinitiates the session with a new sessionKey.
  • 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_LENGTH is 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 received MAC. In case of mismatch, drops the packet and increases the error counter.
    • Checks that seq matches 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 seq counter. 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_SESSION state pauses processing of all commands except START_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 PAUSE command 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 appAuthKey is already present)
  • When seq exceeds 1000. (To enforce regular rotation of session keys.)

Security checklist

PropertyMechanism
ConfidentialityBLE AES-CCM (link) + AES-CTR (app layer)
IntegrityBLE MIC* (link) + truncated HMAC-SHA256 (app layer)
Replay ProtectionSEQ with window size 1
Key FreshnessEphemeral ECDH per session, regularly rotated
Key ConfidentialityappAuthKey, sessionKey never sent over BLE
Metadata PrivacyActivity obfuscation

* BLE MIC is a 4-byte CCM tag added by the controller


  1. 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. ↩︎