Skip to content

feat(bundle): App Bundle (AAB) bundle-mode integrity + relicense to CC BY-ND 4.0 - #5

Merged
iamjosephmj merged 13 commits into
mainfrom
feat/app-bundle-integrity
Jun 29, 2026
Merged

feat(bundle): App Bundle (AAB) bundle-mode integrity + relicense to CC BY-ND 4.0#5
iamjosephmj merged 13 commits into
mainfrom
feat/app-bundle-integrity

Conversation

@iamjosephmj

Copy link
Copy Markdown
Owner

App Bundle (AAB) bundle-mode integrity

Adds a parallel bundle-mode integrity path so the build-baked tamper detection works when an app ships as an Android App Bundle through Google Play. Previously the integrity layer was APK-only: an AAB build never injected the fingerprint, pinned the upload key (Play re-signs with the app-signing key), and hashed compressed bytes (Play re-encodes) — so it silently broke / false-positived under Play delivery.

How it works

  • DSL: deviceIntelligence { appBundle { enabled = true; playSigningCertSha256("AB:CD:…") } } (opt-in; default off — APK consumers unaffected).
  • Fingerprint v3 (additive): new bundleMode + bundleEntryHashes; MIN_SUPPORTED_FORMAT_VERSION stays 1, so v1/v2 blobs still decode and the APK path is byte-for-byte unchanged.
  • Build: BundleIntegrityTask (wired to SingleArtifact.BUNDLE) bakes the fingerprint into the .aab and re-signs it (JDK JarSigner; no bundletool dependency).
  • What's pinned: signer allow-set (upload cert ∪ Play app-signing cert pins) checked by membership, and decompressed SHA-256 of classes*.dex + lib/<abi>/*.so (stable across Play's re-encode), keyed by installed-entry name.
  • Native: NDK-zlib streaming raw-inflate (hash_entry_decompressed) + a apkEntryDecompressedHash JNI — bounds-checked, fail-closed, 64 KiB peak memory.
  • Runtime: a bundle branch checks signer membership + decompressed entry hashes across base.apk + splitSourceDirs; mismatch → apk_entry_modified (CRITICAL), dex absent → apk_entry_removed (HIGH). An absent ABI-split .so is treated as conditionally-delivered (not flagged).

Validation

  • Unit tests: codec v3 round-trip + v1/v2 back-compat, AabHasher normalization + decompressed-hash, BundleFingerprintBuilder encrypt/decrypt, a native host C++ suite (STORED/DEFLATED/garbage), and the runtime decision logic (membership + modified/removed/skip).
  • On-device (Pixel 6 Pro, real bundletool split delivery — base.apk + split_config.arm64_v8a.apk): a clean install reports integrity.apk ok with zero false positives; a tampered device .so is caught as apk_entry_modified CRITICAL.

Also in this PR

  • Relicense to CC BY-ND 4.0 (Creative Commons Attribution-NoDerivatives 4.0 International), replacing Apache-2.0 — LICENSE, README badge + section, and the Maven POM license metadata in both modules.

Spec + plan under docs/superpowers/.

Adds two v3 fields to both the plugin and runtime Fingerprint data class
(bundleMode: Boolean, bundleEntryHashes: Map<String,String>) and wires
them into the binary codec. v1/v2 blobs remain decodable; MIN_SUPPORTED_FORMAT_VERSION
stays at 1. Includes three round-trip JUnit 5 tests (TDD: RED → GREEN).
…ompressed

I-1: Replace single-shot vector inflate + one-shot sha256 with fixed 64 KiB
output buffer streaming inflate loop + incremental Sha256Ctx (init/update/final)
added to sha256.{h,cpp}; existing one-shot sha256() routed through it.
Peak memory O(64 KiB) independent of entry size. inflateEnd called on every path.

I-2: Reject comp > kMaxInflateBytes before handing to zlib (symmetric with
existing uncomp cap).

M-2: Explicit body_off narrowing guard — reject if body_off > apk.size() or
body_off + comp > apk.size() before any size_t cast, preventing silent 32-bit
truncation on armeabi-v7a.

M-1: Post-inflateEnd total_out read dropped; completeness tracked via
accumulated total_inflated inside the loop.

M-3: New test6 — valid-shaped DEFLATED ZIP entry with garbage body bytes;
asserts hash_entry_decompressed returns false and does not crash.
…, guards, temp cleanup, const

T1: replace inline FQCNs in DeviceIntelligenceExtension with proper imports.
T2a: add rawV2BlobDecodesWithNoBundleFields test proving formatVersion>=3 guard handles real v2 blobs.
T2b: add readNonNegative guard for bundleEntryCount in plugin FingerprintCodec v3 decode block.
T3a: add key.size==32 require in BundleFingerprintBuilder.build (symmetric with task-level check).
T3b: wrap AabSigner sign+rename in try/catch to delete stale temp on failure.
T4b: promote BUNDLE_ASSET_PATH to const val with literal value; drop unused Fingerprint import.
…pk_entry_removed

Play delivers only the device-ABI config split; other ABIs baked into
the fingerprint are legitimately absent and must not emit apk_entry_removed.
A present-but-patched .so still fires apk_entry_modified (tamper detection
preserved). Non-ABI entries (e.g. classes*.dex) continue to flag removal.
Replace Apache-2.0 with Creative Commons Attribution-NoDerivatives 4.0
International (CC BY-ND 4.0), matching the hydra project. Updates LICENSE,
the README badge + license section, and the Maven POM license metadata in
both the library and Gradle-plugin modules.
@iamjosephmj
iamjosephmj merged commit a5e4fe0 into main Jun 29, 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.

1 participant