Skip to content

fix(credentials): isolate Android app credentials - #480

Merged
YosefHayim merged 7 commits into
YosefHayim:mainfrom
blankey1337:fix/credentials/android-app-isolation
Nov 9, 2026
Merged

YosefHayim merged 7 commits into
YosefHayim:mainfrom
blankey1337:fix/credentials/android-app-isolation

Conversation

@blankey1337

@blankey1337 blankey1337 commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Android builds could reuse another app's Play account or upload keystore because both were stored globally. Scope Play keys, upload-keystore backups, and their keychain passwords by package name throughout build, store, release, reporting, and readiness workflows.

Existing users can run launch creds adopt --platform android --app <name> to copy legacy credentials into the selected app. It checks Play access and both keystore passwords before copying, retains the originals, and requires --yes to overwrite an existing app slot. Recovery messages name the selected app. An existing but unreadable keystore blocks new-key generation; replacing one through import requires confirmation or --yes.

Credential status lists every configured Android app and surfaces config errors. Snapshot capture retains accessible apps while recording per-app skips, and snapshot diff does not report unavailable apps as deleted. The Android CI example specifies an app. Play and GA4 statistics use scoped keys; discovery uses configured apps, cached portfolio apps, and package identifiers recorded alongside imported keys.

Validation: pnpm check and pnpm test:e2e passed. Tests cover a real temporary JKS with distinct passwords, invalid passwords and Play access, random verification passwords, legacy-keystore non-fallback, replacement protection, partial snapshot capture and persistence, app-specific recovery messages, and status for two apps and a broken config.

I have not run launch creds adopt on a real installation containing legacy unscoped credentials, nor followed it with a real signed Android build. Play access in the adoption tests uses a fake; no production credentials or store APIs were used.

@blankey1337

Copy link
Copy Markdown
Contributor Author

This is an important missing feature for anyone who has multiple Android credentials. The rest of my PRs are strictly related to security.

@blankey1337

Copy link
Copy Markdown
Contributor Author

@YosefHayim any feedback here or my other PRs? I'd like to get these merged so that may local copy is not out of sync with your upstream.

@YosefHayim YosefHayim left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, and sorry for the slow reply. Isolating Play keys and keystores per app is the right fix, and the two-app tests help. The blocker is the upgrade path: today a current Android user's build fails after upgrading, and the message does not say how to recover. Keys Launch generated cannot be re-imported either, because their passwords were never shown. Before we merge, please:

  1. Add an explicit one-time command, for example launch creds adopt --platform android --app <name>. It copies the existing unscoped Play key and upload keystore, with their keychain passwords, into one app's slot. It copies and never deletes, checks that the keystore opens and the Play key can read that package first, and refuses to overwrite an existing app key without --yes.
  2. Print that exact command in the build, store, release and reports errors when a legacy key exists, and add --app to the other "import one with" messages (src/providers/credentials/local.ts:69, metadataCommand.ts:220, googleStoreCommand.ts:48, playReportsCommand.ts:132, releaseTrain/builder.ts:184).
  3. Make packageName required in the credential store functions, and move statsCredentials.ts:47 to per-app keys, so no unscoped read or write is left.
  4. Let launch creds status list every configured Android app instead of failing without --app (command.ts:729).
  5. Never offer to generate a new upload keystore when an older one exists for that app, and ask before an explicit --import replaces an existing app keystore (androidKeystore.ts:264, 369-383).
  6. Add --app to the CI snippet in src/core/config/ciCommand.ts:115-116.
  7. Add tests for the legacy-only build message, keystore non-fallback, status with two apps, and a wrong keystore password.
  8. Small: the two identical branches in local.ts:132-134 and the cache check in storeClients.ts:42-44.

Main moved since you opened this; the conflicts will be in the generated README and llms files, so pnpm docs:gen after a rebase covers them.

@blankey1337
blankey1337 force-pushed the fix/credentials/android-app-isolation branch 2 times, most recently from d1caa82 to 1be9da5 Compare November 9, 2026 22:17

@YosefHayim YosefHayim left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is a big step forward. creds adopt checks the keystore and Play access before copying, never deletes, and needs --yes to overwrite; status lists every app; the keystore is never regenerated next to an old one; and the CI snippet has --app. A few things left before we merge:

Blocker

  1. Snapshot capture: one Android app without a Play key fails the whole Play products and subscriptions source (snapshot/sources/playProducts.ts:146-150, playSubscriptions.ts:184-188), so a user with two apps and one key loses both apps in the snapshot. Skip that app with a reason instead, like restore already does, and drop the presence-only Effect.forEach at playProducts.ts:129-134.

Should fix
2. missingPlayServiceAccountMessage (androidKeystore.ts:270-277) prints a literal <name> and says "legacy Play service account" even when only the keystore passwords exist. Pass the app name in so the exact command prints, and word the message by what was actually found.
3. verifyKeystore (androidKeystore.ts:280-318) writes a PKCS12 copy of the private key protected by a fixed, public password. Use a random password per run (for example randomBytes(32).toString('hex')).
4. The internal helpers still take an optional packageName (androidKeystore.ts:22, 98, 109, 131, 208, 221, 418, 511, 567; paths.ts:83-90), so leaving it out silently reads or writes the unscoped slot. Please make it required, and give adoption and legacy detection their own explicit legacy reads.
5. Missing test: an app with no keystore while a legacy keystore exists must neither use the legacy one nor generate a new one (ensureUploadKeystore). androidIsolation.test.ts:53-66 covers only the Play key.
6. creds status (command.ts:759-762) turns any config error into "no apps". A missing config can mean no apps, but a broken one should fail with its error.

Small: the let keystoreEffect at command.ts:698-701 and let androidApps at command.ts:763 can be consts with early returns.

Please also say whether you ran launch creds adopt once on a real install that had the old unscoped key, followed by a signed build.

Main moved again (#519, #484). The rebase only conflicts in the generated README and llms files, so pnpm docs:gen after it covers them.

@blankey1337
blankey1337 force-pushed the fix/credentials/android-app-isolation branch from 1be9da5 to 6e12343 Compare November 9, 2026 22:28
@YosefHayim

Copy link
Copy Markdown
Owner

Thanks, 591cc54 covers everything I asked. I am pushing one small fixup commit on top (an unreadable Android index or config now fails instead of reading as empty, the stats config uses the working directory, and a few style bits), then I will merge. Please hold further pushes to this branch until then.

@YosefHayim
YosefHayim force-pushed the fix/credentials/android-app-isolation branch from f4c07d4 to c3afe8e Compare November 9, 2026 22:51

@YosefHayim YosefHayim left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working through all of this. I pushed one fixup commit: an unreadable Android index, config, saved portfolio or stored package list now fails or shows as a Play discovery failure instead of reading as empty, the stats config gets the working directory, missingPlayServiceAccountMessage is now describeMissingPlayServiceAccount, and a few lets became consts. I also rebased on main after #520 merged.

@YosefHayim
YosefHayim merged commit 450993a into YosefHayim:main Nov 9, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants