From c2fad7a9e58bd2aa9536a912e499f7b797f41231 Mon Sep 17 00:00:00 2001 From: Om Narayan Date: Wed, 22 Apr 2026 10:24:29 +0530 Subject: [PATCH] =?UTF-8?q?Wire=20clearKeychain=20on=20iOS=20=E2=80=94=20s?= =?UTF-8?q?tandalone=20step=20+=20launchApp=20flag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported by @ross-aker in #57 — thank you for the clear repro. ClearKeychainStep had no handler in the WDA driver's Execute() dispatch, so a standalone `clearKeychain` step errored with "Step type '*flow.ClearKeychainStep' is not supported on iOS". The launchApp struct has a ClearKeychain field, but launchApp() never read it — so `launchApp: clearKeychain: true` was silently a no-op: the app relaunched with the previous user still logged in. Fix: - Add clearKeychain(step) handler that calls resetKeychain(), a shared helper that runs `xcrun simctl keychain reset` on simulators. - Wire *flow.ClearKeychainStep into driver.Execute() so the standalone step works. - In launchApp(), if step.ClearKeychain is set, invoke resetKeychain() after clearState and before launch. Failures log at Warn and do not abort the launch. Real-device behavior: returns a clear "unsupported on real iOS devices" result with a pointer to use `clearState` instead (reinstall drops the app's keychain entries). The iOS keychain on real devices is sandboxed per-app and cannot be reset via public API without jailbreak. Verified on iPhone 16 Pro simulator (iOS 18.6): standalone clearKeychain passes, launchApp with clearKeychain: true no longer silently no-ops, existing auth flows still pass. Fixes #57 --- pkg/driver/wda/commands.go | 36 ++++++++++++++++++++++++++++++++++++ pkg/driver/wda/driver.go | 4 ++++ 2 files changed, 40 insertions(+) diff --git a/pkg/driver/wda/commands.go b/pkg/driver/wda/commands.go index 65aecf1..742f0ba 100644 --- a/pkg/driver/wda/commands.go +++ b/pkg/driver/wda/commands.go @@ -726,6 +726,15 @@ func (d *Driver) launchApp(step *flow.LaunchAppStep) *core.CommandResult { wg.Wait() } + // Reset simulator keychain if requested — after clearState so a reinstall + // can't race, before launch so the app starts with a clean keychain. + // No-op with a warning on real devices (keychain can't be reset there). + if step.ClearKeychain { + if r := d.resetKeychain(); !r.Success { + logger.Warn("launchApp: clearKeychain skipped: %s", r.Message) + } + } + if d.udid != "" { d.alertAction = resolveAlertAction(permissions) } @@ -901,6 +910,33 @@ func (d *Driver) clearAppStateSimulator(bundleID string) *core.CommandResult { return successResult(fmt.Sprintf("Cleared state for %s (uninstall+reinstall)", bundleID), nil) } +// clearKeychain handles the standalone clearKeychain step. Unsupported on +// real devices (iOS keychain is sandboxed and can't be reset via public API). +func (d *Driver) clearKeychain(_ *flow.ClearKeychainStep) *core.CommandResult { + return d.resetKeychain() +} + +// resetKeychain runs `xcrun simctl keychain reset` on the simulator. +// Shared by the standalone step and the launchApp clearKeychain: true option. +func (d *Driver) resetKeychain() *core.CommandResult { + if !d.info.IsSimulator { + return &core.CommandResult{ + Success: false, + Error: fmt.Errorf("clearKeychain is not supported on real iOS devices"), + Message: "clearKeychain requires an iOS Simulator — the iOS keychain is sandboxed on real devices and cannot be reset programmatically. Use clearState to reinstall the app, which drops its keychain entries.", + } + } + if d.udid == "" { + return errorResult(fmt.Errorf("simulator UDID required"), "clearKeychain requires a booted simulator") + } + cmd := exec.Command("xcrun", "simctl", "keychain", d.udid, "reset") + if output, err := cmd.CombinedOutput(); err != nil { + return errorResult(fmt.Errorf("simctl keychain reset failed: %w: %s", err, string(output)), + "Failed to reset simulator keychain") + } + return successResult("Simulator keychain reset", nil) +} + func (d *Driver) clearAppStateDevice(bundleID string) *core.CommandResult { // Uninstall via xcrun devicectl (uses remoted, doesn't disrupt usbmuxd port forwarding) cmd := exec.Command("xcrun", "devicectl", "device", "uninstall", "app", diff --git a/pkg/driver/wda/driver.go b/pkg/driver/wda/driver.go index 76281e9..9a1f001 100644 --- a/pkg/driver/wda/driver.go +++ b/pkg/driver/wda/driver.go @@ -233,6 +233,10 @@ func (d *Driver) Execute(step flow.Step) *core.CommandResult { case *flow.SetPermissionsStep: result = d.setPermissions(s) + // Keychain + case *flow.ClearKeychainStep: + result = d.clearKeychain(s) + default: result = &core.CommandResult{ Success: false,