Account switching: prototype findings and test plan
An Android prototype that saves encrypted sessions and switches accounts without in-game logout. Working code, proposed safeguards, and untested recovery are labeled separately.
Sections marked Proposed describe possible safeguards, not completed prototype functionality.
What I built
Current flow:
static void startAuthorization(Activity activity, Account account)
throws Exception {
String state = randomUrlSafe(12);
String verifier = randomUrlSafe(64);
String challenge = base64Url(
MessageDigest.getInstance("SHA-256")
.digest(verifier.getBytes(US_ASCII)));
boolean saved = pending.edit()
.putString(KEY_ACCOUNT_ID, account.id)
.putString(KEY_STATE, state)
.putString(KEY_VERIFIER, verifier)
.putLong(KEY_CREATED_AT, now())
.commit();
if (!saved) throw new IllegalStateException("pending state");
setCallbackEnabled(activity, true);
activity.startActivity(browserIntent(
authorizationUri(state, challenge)));
}static CallbackResult consumeCallback(Context context, Uri callback) {
String expectedState = pending.getString(KEY_STATE, null);
String returnedState = first(callback, "state");
long createdAt = pending.getLong(KEY_CREATED_AT, 0);
if (now() - createdAt > MAX_PENDING_AGE_MS) {
pending.edit().clear().commit();
return failure("sign-in expired");
}
if (!constantTimeEquals(expectedState, returnedState)) {
return failure("OAuth state mismatch");
}
pending.edit().clear().commit();
return success(accountId, code, verifier);
}void put(String recordId, String plaintext) throws Exception {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, getOrCreateKey());
byte[] ciphertext = cipher.doFinal(plaintext.getBytes(UTF_8));
byte[] iv = cipher.getIV();
ByteBuffer packed = ByteBuffer.allocate(1 + iv.length + ciphertext.length);
packed.put((byte) iv.length).put(iv).put(ciphertext);
preferences.edit()
.putString(recordId, Base64.encodeToString(packed.array(), NO_WRAP))
.commit();
}Result switchAndRelaunch(Account account) {
boolean prepared = false;
String stage = "prepare";
try {
String credential = secureStore.get(recordFor(account));
if (credential == null) return failure("credential-missing");
credential = refreshCredential(credential);
prepared = installer.prepare(credential);
if (!prepared) return failure(installer.getLastError());
stage = "startup-check";
if (!rootBridge.coldStartPokemonGo()) {
installer.rollback();
return failure(stage);
}
installer.commit();
accounts.setSelectedId(account.id);
return success("ready");
} catch (Exception error) {
if (prepared) installer.rollback();
return failure(stage);
}
}Excerpts are shortened; private constants and session paths are omitted. Observed: enrollment and happy-path switching. Not proven: identity or recovery at scale.
Current implementation status
Implemented and observed refer to the prototype. Proposed and not tested do not.
| Capability | Status | What is known |
|---|---|---|
| Browser enrollment with PKCE | Implemented | No PTC password is requested. |
| Separate metadata and session records | Implemented | Aliases never contain credentials. |
| Keystore-encrypted session storage | Implemented | Used by the prototype. |
| Stop the game before replacement | Implemented | Used on every switch. |
| Ordered account cycling | Implemented | Accounts follow repository order. |
| Happy-path switching | Manually observed | Saved accounts loaded successfully. |
| Verify the loaded identity | Proposed | Launch success is not identity proof. |
| Rollback after failure | Not tested | Fault injection is still required. |
| Recover after controller death | Not tested | Not measured yet. |
| Recover after device reboot | Not tested | Not measured yet. |
| Block concurrent switches | Proposed | Use one device-wide switch owner. |
Launching successfully does not prove the correct account loaded
The prototype records the account it attempted to apply, not the account that actually loaded. A stable runtime identity signal still needs validation; aliases, trainer names, and queue order are not proof.
Safety properties
Desired invariants, not proven behavior:
- Pokémon GO is not running while account-session state is being replaced.
- Only one account-switch transaction can operate on a device at a time.
- The previous session remains recoverable until the new account is verified.
- Launching the process is not considered a successful account switch.
- The account manager cannot report success while identity is unknown or mismatched.
- A failed or interrupted switch has a deterministic recovery path.
- Credentials and reusable session data never appear in normal logs.
Proposed safer transaction model
Proposed only:
- Acquire the device switch lock
- Stop Pokémon GO
- Record the current transaction stage
- Preserve and verify the previous session
- Apply and verify the target session
- Launch Pokémon GO
- Wait for runtime readiness
- Read the active account identity
- Compare expected and observed identities
Commit the switch, remove the temporary backup, and report verified success.
Stop the game, restore the previous session, record the result, and keep the switch unresolved.
switchAccount(target):
acquire device switch lock
stop Pokémon GO or abort
persist stage = "stopped"
preserve and verify previous session or abort
apply and verify target session
launch Pokémon GO
wait for runtime readiness
observed = read active account identity
if observed matches expected:
persist stage = "verified"
remove temporary backup
report verified switch success
else:
stop Pokémon GO
restore and verify previous session
record rollback result
keep switch result unresolvedFailure modes to validate
Test targets, not observed incidents:
| Failure | Possible consequence | Detection | Proposed response | Current test status |
|---|---|---|---|---|
| Game does not stop | Concurrent writes | Process still alive | Abort before mutation | Not tested |
| Backup fails | No recovery point | Backup verification fails | Abort before install | Not tested |
| Target session is incomplete | Invalid session state | Staging validation fails | Keep previous session | Not tested |
| Game crashes on launch | Ambiguous result | Exit before verification | Restore and record | Not tested |
| Network is unavailable | Readiness timeout | Bounded timeout | Return unresolved | Not tested |
| Session is expired | Login failure | Runtime rejects session | Re-enroll account | Not tested |
| Wrong account loads | Account mix-up | Identity mismatch | Stop and restore | Not tested |
| Identity is unreadable | Unknown account | No stable identity | Return unresolved | Not tested |
| Controller dies | Partial transaction | Incomplete saved stage | Recover or stop | Not tested |
| Privileged service dies | Partial write | Missing acknowledgement | Stop and verify | Not tested |
| Device reboots | Lost in-memory state | Incomplete saved stage | Recover from saved stage | Not tested |
| Concurrent requests | Overwritten state | Lock already held | Reject or queue | Not tested |
| Storage is full | Incomplete files | Write or sync fails | Abort and preserve | Not tested |
| Game format changes | Invalid installer | Compatibility check fails | Disable switching | Not tested |
| Rollback fails | No confirmed session | Restore verification fails | Manual recovery | Not tested |
Mass-testing plan
Repeat the happy path, then interrupt every destructive stage.
Happy-path repetition
- Enroll one account and load it
- Enroll multiple accounts
- A → B
- A → B → A
- A → B → C
- Cycle through all accounts repeatedly
- Select the same account twice
- Remove and re-enroll an account
Fault injection
- Interrupt before and after stopping the game
- Interrupt during and after backup
- Interrupt during and after target installation
- Interrupt during launch and before identity verification
- Interrupt during rollback and before cleanup
- Kill the controller or privileged service
- Reboot the device during the switch
- Remove, slow, or restore the network
- Use expired, invalid, and intentionally wrong sessions
- Send concurrent switch commands
- Fill storage and force permission restoration failures
- Repeat after a Pokémon GO version change
Results: Not measured yet.
Test-data schema
Hash account IDs. Never log credentials, reusable sessions, or raw account state.
run_id
device_model
android_version
pokemon_go_version
controller_version
source_account_id_hash
target_account_id_hash
expected_identity_hash
observed_identity_hash
transaction_stage
switch_started_at
game_launched_at
identity_checked_at
switch_duration_ms
result
failure_code
rollback_attempted
rollback_succeeded
switch_reported_success
wrong_account_detected
identity_unknown
session_corruption_detectedSummary metrics
- Total switch attempts
- Verified successful switches
- Wrong-account detections
- Unknown-identity results
- Launch timeouts
- Rollback attempts
- Successful rollbacks
- Session-corruption incidents
- Median and 95th-percentile switch duration
- Results grouped by device and Android version
- Results grouped by Pokémon GO version
- Results grouped by session age
Current measurements: Not measured yet.