Skip to content

fix: infinite sync after MLS got enabled while logged out - WPB-24543#4532

Open
David-Henner wants to merge 5 commits intorelease/cycle-4.16from
fix/infinite-sync-after-MLS-enabled
Open

fix: infinite sync after MLS got enabled while logged out - WPB-24543#4532
David-Henner wants to merge 5 commits intorelease/cycle-4.16from
fix/infinite-sync-after-MLS-enabled

Conversation

@David-Henner
Copy link
Copy Markdown
Contributor

@David-Henner David-Henner commented Apr 2, 2026

BugWPB-24543 [iOS] Infinite sync re-logging in after MLS is enabled

Issue

After logging out and back in on a team where MLS was enabled while the user was logged out, the incremental sync enters an infinite sync loop because of an error:

failedToFetchStoredEvents(
   WireDomain.EAREncryptionHelper.CryptoError.decryptionFailed(
      Error Domain=NSOSStatusErrorDomain Code=-50 "ECIES: Failed to aes-gcm decrypt data (err -69)"
      UserInfo={numberOfErrorsDeep=0, NSDescription=ECIES: Failed to aes-gcm decrypt data (err -69)}
   )
)

The sync eventually stops thanks to the BackoffRetrier and an alert is shown with the error above. Afterwards we also observe a few EAR decryption errors.

Causes

A — Zombie websocket stores events with stale EAR keys.
ZMUserSession.tearDown() set syncAgent = nil without calling suspend() first. This left the websocket alive after logout. When the user logged back in, a live user.clientAdd event arrived on this zombie connection and was encrypted into the event store using EAR public keys that had been fetched at the start of the previous session's IncrementalSync.

B — EAR keys were not deleted on logout.
Account.deleteKeychainItems() removed the app lock passcode and CoreCrypto keys but not EAR keys. On re-login, clientRegistrationDidSucceed called enableEncryptionAtRest, which deleted the old EAR keys and generated new ones. Any events already stored with the old public key became permanently undecryptable.

In short: the zombie websocket stores a freshly arrived event using old EAR keys, then key rotation destroys those keys, leaving the stored event orphaned.

Solutions

  • Close the websocket on logout by calling suspend() on the sync agent before teardown, ensuring the push channel and its captured EAR public keys are released.
  • Delete EAR keys on account cleanup by adding RemoveEARKeysUseCase to Account.deleteKeychainItems(), ensuring no orphaned EAR keys survive in the keychain after logout.

Changes Made

  • SyncAgent: Added tearDown() method that nils the delegate and fires a task to call suspend(), which cancels the ongoing sync and closes the push channel.
  • ZMUserSession.tearDown(): Replaced syncAgent?.delegate = nil with syncAgent?.tearDown().
  • RemoveEARKeysUseCase (new): Use case that deletes all five EAR keychain entries (primary/secondary public, primary/secondary private, database key) for a given account ID.
  • Account.deleteKeychainItems(): Added RemoveEARKeysUseCase().invoke(accountID:) alongside the existing CoreCrypto key cleanup.
  • Tests: Added RemoveEARKeysUseCaseTests, EARKeyRepositoryTests, extended AccountTests and SyncAgentTests.

Testing

  • login with proteus user on iOS and any other client
  • create some groups and / or 1:1s
  • logout the iOS user
  • enable MLS for the team
  • login the iOS user
  • Try to send a message in any conversation or
  • try to add the user to a group

Checklist

  • Title contains a reference JIRA issue number like [WPB-XXX].
  • Description is filled and free of optional paragraphs.
  • Adds/updates automated tests.

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 2, 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

Test Results

    7 files    982 suites   11m 6s ⏱️
7 109 tests 7 081 ✅ 28 💤 0 ❌
7 110 runs  7 082 ✅ 28 💤 0 ❌

Results for commit c393eca.

Summary: workflow run #23907886957
Allure report (download zip): html-report-29020-fix_infinite-sync-after-MLS-enabled

Copy link
Copy Markdown
Collaborator

@netbe netbe left a comment

Choose a reason for hiding this comment

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

LGTM:)

I just wonder if this for 4.16.1 or 4.17.0?

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