Skip to content

Read the channel's data encoder live when encoding annotations - #2231

Draft
SimonWoolf wants to merge 1 commit into
mainfrom
annotation-cipher-setoptions
Draft

Read the channel's data encoder live when encoding annotations#2231
SimonWoolf wants to merge 1 commit into
mainfrom
annotation-cipher-setoptions

Conversation

@SimonWoolf

@SimonWoolf SimonWoolf commented Jul 29, 2026

Copy link
Copy Markdown
Member

ARTRestAnnotationsInternal and ARTRealtimeAnnotationsInternal each captured _channel.dataEncoder into an ivar at construction time. But ARTChannel replaces its _dataEncoder with a brand-new instance whenever setOptions is called (recreateDataEncoderWith:, ARTChannel.m:63), and there is no hook to refresh the annotations objects' copies.

So after a setOptions that added or changed cipher params, annotation data was encoded and decoded with the encoder built at channel-creation time — typically one with no cipher at all — while messages correctly picked up the new cipher, because encodeMessageIfNeeded: reads the dataEncoder property live (ARTChannel.m:252). ARTRealtimePresence's publish path also reads it live (ARTRealtimePresence.m:596), deliberately bypassing its own cached ivar.

Fix: read _channel.dataEncoder at each encode/decode site rather than caching it. Four sites; the REST decode path (ARTRestAnnotations.m:268) already read it live, which was itself an inconsistency within that file.

Scope

This is not the same bug as the one being fixed in ably-js (ably-js#2277), where annotation data was never encrypted at all. ably-cocoa is correct on the normal path: a channel constructed with cipher options encrypts annotation data properly, because construction order guarantees the captured encoder is already cipher-bearing.

This is the narrower latent case — a channel reconfigured via setOptions after creation. It also fails symmetrically (annotations stay internally consistent, just encrypted under the wrong policy — in practice not encrypted), so unlike the ably-js bug it wouldn't corrupt a round trip within a single client. It surfaces as annotation data sitting in plaintext on the service, and as garbling against a client that constructed its channel with the cipher up front.

Found while auditing the other SDKs for the ably-js bug. Java is unaffected; Python has the ably-js bug and worse (its decode path is broken too).

Test

test__annotation_data_uses_a_cipher_added_by_setOptions_after_channel_creation in RealtimeAnnotationsTests.swift creates a channel with no cipher, adds one via setOptions, then publishes and receives an annotation with a data payload and asserts it round-trips.

Before the fix the annotation is published as plaintext and arrives as ciphertext, so the assertion fails.

Worth noting there was no existing test coverage for annotations on encrypted channels at all — neither RealtimeAnnotationsTests.swift nor RestAnnotationsTests.swift contained a single reference to cipher or encrypt. That gap is what let this sit unnoticed.

CI status

The new test passes on all three platforms (macOS, iOS, tvOS), and the SDK compiles cleanly everywhere. EditorConfig passes.

The three check jobs are nonetheless red, on a pre-existing flaky failure unrelated to this change: test__annotations_subscribe_should_implicitly_attach_the_channel_if_options_attachOnSubscribe_is_true, failing with Error 80017 - connection broken before receiving publishing acknowledgment. That test never publishes an annotation and never configures a cipher — it only exercises attach/detach lifecycle — so it cannot be affected by this change. It also passed on an earlier run of this same branch. tvOS additionally failed a presence test (test__050__Presence__PresenceMap__...).

For reference, the last three Integration Test runs on main each failed a different test (Presence.test__038, Presence.test__039, RestClientStats.test__002) — none repeating, which is the signature of sandbox/network flakiness rather than a regression.

Note the REST path has no equivalent test; the realtime one covers the shared mechanism.

An earlier revision of this test used reaction:distinct.v1 and failed with Error 40000 - Aggregation method not allowed for anonymous client publish; allowed methods are: [multiple.v1 total.v1]. Now uses reaction:multiple.v1, matching the other tests in the file.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: befd29b0-d4ec-4648-9809-6533a25f5a47

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions
github-actions Bot temporarily deployed to staging/pull/2231/features July 29, 2026 10:08 Inactive
@github-actions
github-actions Bot temporarily deployed to staging/pull/2231/jazzydoc July 29, 2026 10:13 Inactive
@github-actions
github-actions Bot temporarily deployed to staging/pull/2231/markdown-api-reference July 29, 2026 10:13 Inactive
@SimonWoolf
SimonWoolf requested a review from maratal July 29, 2026 10:20
ARTRestAnnotationsInternal and ARTRealtimeAnnotationsInternal each captured
_channel.dataEncoder into an ivar at construction. ARTChannel replaces its
_dataEncoder with a new instance whenever setOptions is called
(recreateDataEncoderWith:), and there is no hook to refresh the annotations
objects' copies, so after a setOptions that added or changed cipher params the
annotations objects kept using the encoder built at channel-creation time.

Annotation data would then be published and decoded with the stale cipher
config (typically none at all) while messages, which read the channel's
dataEncoder property live, correctly picked up the new cipher.

Read _channel.dataEncoder at each encode/decode site instead of caching it,
matching ARTChannel's encodeMessageIfNeeded: and ARTRealtimePresence's publish
path. The REST decode path already read it live.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant