Fix logout credential resurrection + crash on keychain entitlement drift#25658
Open
jkmassel wants to merge 1 commit into
Open
Fix logout credential resurrection + crash on keychain entitlement drift#25658jkmassel wants to merge 1 commit into
jkmassel wants to merge 1 commit into
Conversation
…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.
Contributor
|
| App Name | WordPress | |
| Configuration | Release-Alpha | |
| Build Number | 32644 | |
| Version | PR #25658 | |
| Bundle ID | org.wordpress.alpha | |
| Commit | 72488b5 | |
| Installation URL | 589eh9eq9ctvo |
Contributor
|
| App Name | Jetpack | |
| Configuration | Release-Alpha | |
| Build Number | 32644 | |
| Version | PR #25658 | |
| Bundle ID | com.jetpack.alpha | |
| Commit | 72488b5 | |
| Installation URL | 2tjgbc9umhavo |
Contributor
🤖 Build Failure AnalysisThis build has failures. Claude has analyzed them - check the build annotations for details. |
Contributor
Author
On-device smoke test ✅Built both apps (Debug, dev-signed) and installed on an iPad via
Results:
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). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Note
Draft companion to #25654, surfaced while reviewing it. Targets
task/keychain-repairso it can be folded into that PR (squash or cherry-pick as you prefer).Summary
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 nextgetPasswordmissed 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 existingdeleteClearsSharedGroupEvenWhenPrivateDeleteFails, 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/BuildSettingsbut missing from the target'skeychain-access-groupsentitlement makes every operation against it fail witherrSecMissingEntitlement: 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 andfatalErrors onerrSecMissingEntitlement. 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 intoWordPressAppDelegate.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-34018signal, so lock-state/not-found can't falsely brick a correct build.Test plan
WordPressSharedunit tests pass on the Simulator — 45 tests, including the new regression.AppKeychain, green with the fix.WordPressSharedcompiles for the device arch (verifies the non-Simulator crash path).Not included
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).AuthTokenServiceNamesdoc comment.