Skip to content

Fix logout credential resurrection + crash on keychain entitlement drift#25658

Open
jkmassel wants to merge 1 commit into
task/keychain-repairfrom
jkmassel/keychain-25654-resurrection-test
Open

Fix logout credential resurrection + crash on keychain entitlement drift#25658
jkmassel wants to merge 1 commit into
task/keychain-repairfrom
jkmassel/keychain-25654-resurrection-test

Conversation

@jkmassel

Copy link
Copy Markdown
Contributor

Note

Draft companion to #25654, surfaced while reviewing it. Targets task/keychain-repair so it can be folded into that PR (squash or cherry-pick as you prefer).

Summary

  • Fix a credential resurrection on logout when the shared-group keychain delete fails.
  • Crash at launch on keychain access-group entitlement drift, so a misconfigured build can't get past beta.

1. Logout credential resurrection

AppKeychain.setPassword(for:to:nil) deleted the shared group, caught any failure, then deleted the private group regardless. If the shared delete threw a real error (e.g. errSecMissingEntitlement) while the private delete succeeded, the result was the resurrectable "private empty, shared present" state: the next getPassword missed the private group, fell back to the shared copy, returned the supposedly-logged-out token, and read-repaired it back into the private group — re-exposing it in the cross-app group.

The shared-first ordering only defended against an interruption between the two deletes, not a thrown shared-delete error. Fix: require the shared delete to succeed before touching the private copy. Worst case on a real failure is now "both copies remain" (logout didn't complete, the caller retries), never the resurrectable state.

Adds sharedDeleteFailureMustNotEmptyPrivateGroup — the mirror of the existing deleteClearsSharedGroupEvenWhenPrivateDeleteFails, which only covered the reverse direction. It is red on the pre-fix code and green with the fix.

Trigger / severity

This needs an asymmetric delete failure (shared fails, private succeeds). The common failure — a locked device — is symmetric and fails both deletes, so it doesn't trigger this. The realistic trigger is an entitlement misconfiguration (next section), which makes every shared-group op fail. Low-probability in normal operation, but a logout silently leaving a token in the cross-app group is exactly the leak this work is meant to eliminate.

2. Crash on entitlement drift

A keychain access group declared in Info.plist/BuildSettings but missing from the target's keychain-access-groups entitlement makes every operation against it fail with errSecMissingEntitlement: login can't persist (the token write is swallowed and the next read returns nothing) and logout can leak the token cross-app. That's a blatant build misconfiguration, and keychain groups aren't provisioning-profile-gated, so a TestFlight build and the App Store build share the same entitlements.

AppKeychain.requireDeclaredAccessGroupsAreEntitled() probes each declared group at launch and fatalErrors on errSecMissingEntitlement. It crashes in release/beta, not just debug — so the crash surfaces on first launch of any affected build and blocks promotion to production (the identical build would crash in TestFlight first; it can't slip through). Wired into WordPressAppDelegate.willFinishLaunching, after crash logging starts and before any keychain access — shared by WordPress, Jetpack, and Reader. No-op on the Simulator (which doesn't enforce access-group entitlements); crashes only on the precise -34018 signal, so lock-state/not-found can't falsely brick a correct build.

Test plan

  • WordPressShared unit tests pass on the Simulator — 45 tests, including the new regression.
  • New test is red on the pre-fix AppKeychain, green with the fix.
  • WordPressShared compiles for the device arch (verifies the non-Simulator crash path).
  • Existing signed-in WordPress/Jetpack users stay signed in after upgrading.
  • A build with a deliberately broken keychain entitlement crashes at launch on device.

Not included

  • A static CI consistency check for Info.plist.entitlements ↔ xcconfig drift — the unsigned, CI-runnable counterpart to the runtime crash (the simulator can't enforce entitlements, so the crash only fires on device).
  • The stale "group sweep" wording in the AuthTokenServiceNames doc comment.

…drift

Two keychain hardening changes surfaced while reviewing this PR.

AppKeychain.setPassword(nil) deleted the shared group, caught any
failure, then deleted the private group regardless. A real shared-delete
failure with a successful private delete left the credential "private
empty, shared present": the next read fell back to the shared copy and
read-repaired it into the private group, resurrecting a logged-out token
and re-exposing it in the cross-app group. The shared-first ordering only
guarded against an interruption between the two deletes, not a thrown
shared-delete error. Require the shared delete to succeed before touching
the private copy. Add the mirror test; the suite only covered the reverse
direction (private fails, shared succeeds).

A keychain access group declared in Info.plist/BuildSettings but missing
from the entitlement fails every operation with errSecMissingEntitlement,
blocking login and leaking tokens cross-app. requireDeclaredAccessGroupsAreEntitled()
probes each declared group at launch and crashes on that signal in
release and beta too, so a misconfigured build cannot ship past
TestFlight. Wired into WordPressAppDelegate.willFinishLaunching, shared by
WordPress, Jetpack, and Reader. No-op on the Simulator; crashes only on
the precise errSecMissingEntitlement code so a correct build is never
falsely bricked.
@wpmobilebot

Copy link
Copy Markdown
Contributor
App Icon📲 You can test the changes from this Pull Request in WordPress by scanning the QR code below to install the corresponding build.
App NameWordPress
ConfigurationRelease-Alpha
Build Number32644
VersionPR #25658
Bundle IDorg.wordpress.alpha
Commit72488b5
Installation URL589eh9eq9ctvo
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@wpmobilebot

Copy link
Copy Markdown
Contributor
App Icon📲 You can test the changes from this Pull Request in Jetpack by scanning the QR code below to install the corresponding build.
App NameJetpack
ConfigurationRelease-Alpha
Build Number32644
VersionPR #25658
Bundle IDcom.jetpack.alpha
Commit72488b5
Installation URL2tjgbc9umhavo
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@wpmobilebot

Copy link
Copy Markdown
Contributor

🤖 Build Failure Analysis

This build has failures. Claude has analyzed them - check the build annotations for details.

@jkmassel jkmassel marked this pull request as ready for review June 15, 2026 18:17
@jkmassel

Copy link
Copy Markdown
Contributor Author

On-device smoke test ✅

Built both apps (Debug, dev-signed) and installed on an iPad via devicectl, over the existing installs. Confirmed each signed build's effective keychain-access-groups grant both declared groups (declared ⊆ granted), so the launch assertion passes:

  • WordPress: [3TMU3BH3NK.org.wordpress.wordpress, 3TMU3BH3NK.org.wordpress]
  • Jetpack: [3TMU3BH3NK.org.wordpress.jetpack, 3TMU3BH3NK.org.wordpress]

Results:

  • ✅ Both apps launch — no entitlement-assertion crash.
  • ✅ Both signed in, into different accounts at the same time on the same device — the cross-app isolation this work is for (CMM-2097).
  • ✅ Force-quit + cold relaunch: both stay signed into their respective accounts (private-group read; existing-install upgrade path via shared→private fallback + read-repair).
  • ✅ Logout + re-login: clean — exercises the real dual-group delete against the actual keychain, which the Simulator can't test (it collapses access groups).

Not exercised on device: a deliberately-broken entitlement crashing at launch (the pass case is confirmed here; the crash branch is the runtime guard itself).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants