Prototype status

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:

01Enroll account in the system browser
02Save local metadata
03Encrypt the session record
04Select a saved account
05Stop Pokémon GO
06Apply stored account state
07Launch Pokémon GO
08Wait until usable
Start PKCE enrollmentJava · current excerpt
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)));
}
Validate the OAuth callbackJava · current excerpt
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);
}
Encrypt the credential recordJava · current excerpt
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();
}
Refresh, install, launch, or roll backJava · current excerpt
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.

CapabilityStatusWhat is known
Browser enrollment with PKCEImplementedNo PTC password is requested.
Separate metadata and session recordsImplementedAliases never contain credentials.
Keystore-encrypted session storageImplementedUsed by the prototype.
Stop the game before replacementImplementedUsed on every switch.
Ordered account cyclingImplementedAccounts follow repository order.
Happy-path switchingManually observedSaved accounts loaded successfully.
Verify the loaded identityProposedLaunch success is not identity proof.
Rollback after failureNot testedFault injection is still required.
Recover after controller deathNot testedNot measured yet.
Recover after device rebootNot testedNot measured yet.
Block concurrent switchesProposedUse 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:

  1. Pokémon GO is not running while account-session state is being replaced.
  2. Only one account-switch transaction can operate on a device at a time.
  3. The previous session remains recoverable until the new account is verified.
  4. Launching the process is not considered a successful account switch.
  5. The account manager cannot report success while identity is unknown or mismatched.
  6. A failed or interrupted switch has a deterministic recovery path.
  7. Credentials and reusable session data never appear in normal logs.

Proposed safer transaction model

Proposed only:

Controller
  1. Acquire the device switch lock
  2. Stop Pokémon GO
  3. Record the current transaction stage
  4. Preserve and verify the previous session
  5. Apply and verify the target session
  6. Launch Pokémon GO
  7. Wait for runtime readiness
  8. Read the active account identity
  9. Compare expected and observed identities
Identity matches

Commit the switch, remove the temporary backup, and report verified success.

Mismatch, timeout, crash, or unknown result

Stop the game, restore the previous session, record the result, and keep the switch unresolved.

Java-like pseudocode showing the proposed transaction boundariesPseudocode · proposed
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 unresolved

Failure modes to validate

Test targets, not observed incidents:

FailurePossible consequenceDetectionProposed responseCurrent test status
Game does not stopConcurrent writesProcess still aliveAbort before mutationNot tested
Backup failsNo recovery pointBackup verification failsAbort before installNot tested
Target session is incompleteInvalid session stateStaging validation failsKeep previous sessionNot tested
Game crashes on launchAmbiguous resultExit before verificationRestore and recordNot tested
Network is unavailableReadiness timeoutBounded timeoutReturn unresolvedNot tested
Session is expiredLogin failureRuntime rejects sessionRe-enroll accountNot tested
Wrong account loadsAccount mix-upIdentity mismatchStop and restoreNot tested
Identity is unreadableUnknown accountNo stable identityReturn unresolvedNot tested
Controller diesPartial transactionIncomplete saved stageRecover or stopNot tested
Privileged service diesPartial writeMissing acknowledgementStop and verifyNot tested
Device rebootsLost in-memory stateIncomplete saved stageRecover from saved stageNot tested
Concurrent requestsOverwritten stateLock already heldReject or queueNot tested
Storage is fullIncomplete filesWrite or sync failsAbort and preserveNot tested
Game format changesInvalid installerCompatibility check failsDisable switchingNot tested
Rollback failsNo confirmed sessionRestore verification failsManual recoveryNot 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.

Fields to collect for every switch attemptSchema outline
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_detected

Summary 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.