Skip to content

Require encryption to write the CCCD of an encrypted characteristic (espressif) - #11236

Merged
tannewt merged 1 commit into
adafruit:mainfrom
dhalbert:espressif-cccd-encryption
Aug 24, 2026
Merged

Require encryption to write the CCCD of an encrypted characteristic (espressif)#11236
tannewt merged 1 commit into
adafruit:mainfrom
dhalbert:espressif-cccd-encryption

Conversation

@dhalbert

@dhalbert dhalbert commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Claude found this bug and wrote the fix. I reworked this explanation for clarity.

(Part of the series of small PRs replacing #11178. Independent of the others.)

The problem

NimBLE (used on Espressif) derives the auto-generated CCCD's permissions only from the BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC/AUTHEN flags. Those flags were not set by us, so the CCCD of an encryption-requiring characteristic was writable on an unencrypted link. This meant an unpaired central could subscribe without having to pair.

By contrast, the nordic port rejects the unencrypted CCCD write with Insufficient Authentication, and that rejection then makes the OS pair.

Since no pairing happened, subsequent web workflow commands, which sent BLE Write Commands, were dropped with no error. This caused the client to just hang waiting for some file transfer results.

(Note that Write Requests can return an error because the server sends a response, but Write Commands get no response. The BLE ATT protocol requires the server to silently ignore those Write Commands when unauthorized.)

The fix

Set BLE_GATT_CHR_F_NOTIFY_INDICATE_ENC (or _AUTHEN) whenever the characteristic's read permission requires encryption (or authentication), so the CCCD write permission follows the characteristic read permission, matching the nordic port.

Note that upstream NimBLE made this inheritance automatic in apache/mynewt-nimble@bfde0dc (July 2026, first released in NimBLE 1.10.0), calling the old behavior a security bypass. But the NimBLE fork pinned by ESP-IDF 6.0.x is 1.6.0, so it doesn't include this fix.

Testing

Tested on a Metro ESP32-S3 with the web editor on Linux, verified with btmon: the CCCD Write Request is now rejected with Insufficient Authentication (0x05), which triggers SMP pairing and encryption, after which the retried write succeeds and file transfer works. Open characteristics still work without pairing, and a bonded reconnect resumes without a new pairing prompt.

@dhalbert
dhalbert requested a review from tannewt August 23, 2026 20:10
…espressif)

NimBLE derives the auto-generated CCCD's permissions only from the
BLE_GATT_CHR_F_NOTIFY_INDICATE_* flags, which were never set, so the CCCD
of an encryption-requiring characteristic was writable on an unencrypted
link. An unpaired central could subscribe, nothing ever demanded pairing,
and its file transfer commands -- Write Commands, which ATT requires the
server to silently ignore when unauthorized -- were dropped with no error,
hanging the client forever.

Now the CCCD write permission follows the characteristic read permission,
as in the nordic port. The central's subscribe attempt gets an ATT
insufficient-authentication error, which is what makes it initiate pairing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dhalbert
dhalbert force-pushed the espressif-cccd-encryption branch from 3c3380d to 14ff34c Compare August 23, 2026 20:39
makermelissa pushed a commit to adafruit/ble-file-transfer-js that referenced this pull request Aug 24, 2026
checkConnection() now reads the transfer characteristic before subscribing.
The characteristic requires encryption, so the read is what makes the browser
pair. Pairing at the subscribe instead fails two ways: espressif firmware
before adafruit/circuitpython#11236 leaves the CCCD writable without
encryption, so nothing ever demands pairing and every request afterwards is
silently dropped (Write Commands have no error response). And on Windows,
pairing during the subscribe makes Chrome deliver each response twice.

It also stops notifications before starting them: on reconnect to a bonded
peripheral, startNotifications() alone can skip the CCCD write, and no
response ever arrives.

Independently, processReadData() rejects a payload longer than its header
declares, and onTransferNotify() reports exceptions as rejections instead of
letting them escape the event handler. Either one turns the doubled-response
case from a permanent hang into a reportable error.

Tested on a Feather nRF52840 and a Metro ESP32-S3, from Chrome on Linux and
Windows 11, including first pairing and reconnect on both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@tannewt tannewt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@tannewt
tannewt merged commit 8863f76 into adafruit:main Aug 24, 2026
208 checks passed
@dhalbert
dhalbert deleted the espressif-cccd-encryption branch August 24, 2026 20:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants