===================================================================== APK sweep: bulk decompile of the remaining 123 system/product apps ===================================================================== :doc:`app-inventory` catalogues the **124** preinstalled APKs by partition without decompiling them; :doc:`platform-boot` decompiled exactly one, ``OnyxOtaService.apk``, and found a hardcoded AES key protecting (weakly) its OTA-payload decryption step. This page covers the other **123**: every remaining APK under ``system``, ``product`` and ``system_ext`` was bulk decompiled with ``jadx`` and grepped for anything bootloader/security relevant. The result mostly confirms stock AOSP/GMS/Qualcomm code and one large shared Onyx SDK copy-pasted into every Onyx-branded app, plus a **small number of concrete, Onyx-custom findings** detailed below — none of them change the trust picture in :doc:`platform-boot` or :doc:`security`, but two are genuine exported-unguarded-component bugs of the same *class* as the ``OnyxOtaService`` finding, just lower-impact. Method and coverage ==================== All 123 non-``OnyxOtaService`` APKs found under ``artifacts/system_a``, ``artifacts/product_a`` and ``artifacts/system_ext_a`` were decompiled with ``jadx`` 1.5.1 (``--show-bad-code``, 90 s timeout per APK) into ``artifacts/re_apk//``: - **75** decompiled cleanly (exit 0). - **47** exited non-zero (jadx's own "finished with errors, count: N" — it still emits sources for every class it *could* decompile; only a minority of individual methods/classes inside these APKs came out as bad-code stubs). Spot-checked several of these (``NetworkStack``, ``knote2-release``) and confirmed full, usable source trees underneath — this is normal jadx behaviour on large, ProGuard/R8-shrunk, Kotlin-heavy real-world APKs, not a sign the APK is unusual. - **1** (``GmsCore.apk``, 119 K decompiled files) hit the 90 s timeout before finishing but had already written the bulk of its sources; treated as partial-but-usable, not retried. - **20** of the 124 produced only ``R.java`` (no real code): these are resource-only APKs — five ``DisplayCutoutEmulation*`` RROs, three more RRO overlays (``NavigationBarMode3Button``, ``NavigationBarModeGestural``, ``TransparentNavigationBar``), the two ``*__tablet_nosdcard__auto_ generated_characteristics_rro`` framework RROs, ``FontNotoSerifSource Overlay``, ``GmsConfigOverlayMdm``, ``FrameworksResCommon_Sys``, ``WifiResCommon_Sys``, ``WigigTetheringRRO``, ``framework-res.apk`` itself, ``ModuleMetadata`` (GMS module-version metadata), ``dcf`` (Qualcomm DRM-content-format resource shim), ``NotesRoleEnabledOverlay`` (package ``com.android.role.notes.enabled``, a role-grant RRO) and ``chromium_TrichromeLibrary`` (the shared native WebView/Chrome library APK — its code lives in the native ``.so``, not in classes.dex). None of these had anything to grep. Total: **124/124** attempted APKs produced a usable decompiled source tree under ``artifacts/re_apk/`` (left in place, not deleted). ``711,698`` `.java` files across all trees, most of it duplicated third-party/AOSP/GMS/framework boilerplate — see below for why a raw grep count is meaningless without filtering. The shared Onyx SDK dominates the corpus ========================================= Roughly twenty of the APKs are Onyx's own apps (``knote2-release``, ``kreader2-release``, ``ksync-release``, ``kmail-release``, ``kcb-release``, ``app-market-release``, ``ai-assistant-release``, ``dict-release``, ``Gallery-release``, ``Music-release``, ``Clock-release``, ``calculator-release``, ``OnyxLatinIME-release``, ``floatingbutton-release``, ``igetshop-release``, ``Kime-release``, ``EasyTransfer``, ``VoiceRecorder-release``, ``tscalibration-release``, ``ProductionTest-release``). All twenty statically link the **same** ``com.onyx.android.sdk.*`` library, so the same handful of SDK classes (``ShellUtils``, ``AESEncryptUtils``, ``OnyxEncryptUtils``, ``SecurePreferences``, ``DPMUtils``, ``FileSecurityV1``, ...) show up identically in every one of them. Grepping the whole tree without accounting for this produces a large number of duplicate hits; every finding below was checked against **where the class is actually registered as a live component** (the merged ``AndroidManifest.xml`` in each APK's own ``resources/``), not just "the source file exists somewhere in the tree." Grep sweep: false positives worth naming (so they don't get re-flagged) ========================================================================== - **"BOOTLOADER" in Apache Commons Logging** (``org/apache/commons/logging/ LogFactory.java``, present in ~15 apps via a bundled logging dep): this is the library's own Java-``ClassLoader``-hierarchy diagnostic string (``"BOOTLOADER"`` / ``"SYSTEM"`` / ``"CONTEXT"`` describing which classloader tier loaded a class) — nothing to do with the Android bootloader. - **``Build.BOOTLOADER`` reads** (``igetshop-release``'s bundled ``EasyDeviceMod`` device-info library, and an iFlytek SDK property-name table): standard Android `Build.BOOTLOADER` device-info string reporting, used the same way in thousands of apps for crash-report device fingerprints. - **``avb``/``avc``/``avg`` etc. as bare identifiers**: these are R8/ProGuard short obfuscated class names inside ``GmsCore``, ``Phonesky``, ``AndroidPlatformServices`` and ``GoogleRestore`` (package ``defpackage``, classes named ``ava.java``, ``avb.java``, ``avc.java``...) — coincidental 3-letter collisions with "AVB", unrelated to Android Verified Boot. - **``ShellUtils.COMMAND_SH`` inside ``Caverphone1``/``Caverphone2``** (phonetic-matching algorithms in Apache Commons Codec, bundled in most Onyx apps): an R8 constant-pooling artifact — the literal string ``"sh"`` happens to be shared between the phonetic algorithm's own ``.replaceAll("sh", ...)`` call and the unrelated ``ShellUtils.COMMAND_SH`` constant, so jadx's static-import resolution prints the SDK's constant name in a file that has nothing to do with shelling out. - **``-----BEGIN ... PRIVATE KEY-----`` blocks**: every hit resolves to a well-known public library test fixture — Google API Client's ``TestCertificates.java`` (a published, documented test key shipped with ``google-api-java-client`` upstream) and Netty's ``PemPrivateKey``/ ``SelfSignedCertificate`` self-signed-cert-generation utility code (Netty generates these at runtime for TLS testing; the "key" in source is generation *code*, not a fixed secret). None are device- or Onyx-specific. - No ``com/onyx/*`` source file anywhere in the sweep contains a ``-----BEGIN`` block of any kind. Real findings ============== Exported, unguarded content provider in the Boox shell app (``kcb-release``) ------------------------------------------------------------------------------ ``kcb-release`` (package ``com.onyx``, application class ``com.onyx.ContentBrowserApplication``, main activity ``com.onyx.tablet.main.ui.TabletMainActivity``) is not the minor component :doc:`app-inventory` guesses at ("a Boox component", name-inferred only) — it is the **home-screen / content-browser shell app**. Its manifest declares: .. code-block:: xml No ``android:permission``, and no other guard. This is a `DBFlow `_-generated ``ContentProvider`` (``OnyxSystemContentProvider_Provider.java``) that implements full ``query``/``insert``/``update``/``delete``/``bulkInsert`` directly against a SQLite table (``SystemConfigDatabase`` → ``SystemKeyValueItem``, a generic ``(key, value)`` store) with the caller-supplied ``selection`` string passed straight through to ``SQLiteDatabase.query/update/delete``. Since the provider is exported with no permission, **any app installed on the device can read, insert, update or delete rows in this table via `content://com.onyx.system.database.ContentProvider/SystemKeyValueItem` without any permission grant**. One confirmed key stored there, ``"sys.app_preference"`` (JSON blob, written/read by ``AppPreferenceSaveRequest``/``AppPreferenceLoadRequest`` in the same SDK), holds the shell app's own preferences — so the concrete impact found is local tamper/denial-of-service against the Boox shell's preferences, not secret disclosure: no credentials or tokens were found stored under this authority in static analysis, though only this one class's reads/writes were traced, not the full key/value space the provider exposes. This is the same *shape* of bug as ``OnyxOtaService`` (exported + unguarded + Onyx-custom) but lower severity, since the reachable data is app-preference key/value pairs rather than a flashable OTA payload. Only ``kcb-release`` registers this provider in its manifest — the SDK class is compiled into the other ~19 Onyx apps too, but the ```` entry itself only appears in ``kcb-release``'s merged manifest, so only that app's process actually exposes the authority. The same manifest declares a **second** exported, unguarded provider, ``com.onyx.android.sdk.scribble.data.contentprovider.NoteDataStatisticsDatabaseContentProvider`` (authority ``com.onyx.android.sdk.note.statistics.ContentProvider.cb``, no ``android:permission``). Its implementation is the same shape as the one above — full ``query``/``insert``/``update``/``delete``/``bulkInsert`` against a single table, here a Room database (``NoteStatisticsRoomDatabaseManager``) holding a ``NoteStatisticsModel`` table — so any local app can also read or write the device's note-taking usage-statistics rows with no permission grant. Lower severity again (usage-stats data, not credentials or a flashable payload), but it is a second, independent instance of the exact same exported-unguarded-DBFlow/Room-provider pattern in the same APK, which makes this look like a house convention in the Onyx SDK rather than a one-off oversight. The same provider *class* turns up a third time, in ``knote2-release`` (package ``com.onyx.android.note``, Onyx's flagship note-taking app — not a companion/shell app). Its manifest declares ``com.onyx.android.sdk.scribble.data.contentprovider.NoteDataStatisticsDatabaseContentProvider`` exported, with no ``android:permission``, under a different authority, ``com.onyx.android.sdk.note.statistics.ContentProvider`` (no ``.cb`` suffix). Same bug, same class, same lack of a guard — but reachable via the main note app rather than the shell, which arguably makes this the more impactful of the two instances. Exported, unguarded download-trigger receiver/service in the App Market -------------------------------------------------------------------------- ``app-market-release`` (package ``com.onyx.appmarket``, the Boox app store) declares two exported, unguarded components: .. code-block:: xml ``PreInstallAppReceiver.onReceive()`` reads a caller-supplied string extra (``args_app_package_name``) straight off the intent, uses it to fetch that package's listing from the Onyx App Market backend (``MarketAppFetchAction``), and if found, immediately kicks off ``DownloadAppManagerAction`` — which acquires a wake lock, downloads the APK over the network to local storage, MD5-checks it against the store's metadata, and marks it ``AppStatus.INSTALL`` on completion. In other words: **any app on the device, with no permission, can make the Boox App Market silently fetch and stage for install any package that exists in Onyx's store catalogue**, by broadcasting one unguarded intent. Whether that final step then installs silently or just prompts the user is an open question (see *Honest limits*). ``AppCheckUpdateService`` is lower-impact: its ``Messenger``-bound handler only starts the store's own update-check routine (gated by ``AppMarketMMKVHelper.isUpdateRemainderEnable()``), which any app could already trigger implicitly just by being present. A dead, unobfuscated AES key in the shared Onyx SDK ------------------------------------------------------- ``com.onyx.android.sdk.utils.AESEncryptUtils`` (present verbatim in all ~20 Onyx apps) declares: .. code-block:: java public static final String key = "8chLv9pat4WlE5IzyUwF9lOEtRYaPo9d1v0EYQnIak8iJDHLdIkJQW994iQZzLIoMhErq190IU8NEYACVnkrxtr6uOXTqkWuiClOC5ACbIWB0zUk5hALBaGWEvXDhudK"; Unlike the ``OnyxOtaService`` AES key (XOR-obfuscated in native ``.rodata``, :doc:`platform-boot`), this one sits in plain ASCII in a public static final field — but tracing every call site of ``AESEncryptUtils.encrypt()``/ ``decrypt()`` across the SDK (``NoteMediaUtils`` for per-note media attachments, ``GetCertificatesRequest`` for a cloud-fetched cert blob) shows each call passing its **own derived key** (from note-shape metadata, or a server-supplied ``certificates.key``) — never this static field. It appears to be dead/vestigial code: present, unused, and not gating anything reachable as of this dump; no call site currently uses the static key, though a future OS/app update could change that. Factory test app (``ProductionTest-release``) — broad exported-activity surface, no privileged exec found --------------------------------------------------------------------------------------------------------------- This is a genuinely Onyx-custom, non-generic app (package ``com.onyx.android.production.test``) with an unusually large permission set (``REBOOT``, ``MASTER_CLEAR``, ``WRITE_SECURE_SETTINGS``, ``MOUNT_UNMOUNT_FILESYSTEMS``, ...) and several activities marked ``android:exported="true"`` with no ``intent-filter`` and no permission — ``DramTestActivity``, ``SensorTestActivity``, ``SIMTestActivity``, ``EpdcTestActivity``, ``WaveFormActivity``, ``OTGCopyTestActivity``, plus ``DeviceBindingInfoActivity`` (exported *with* an intent-filter, ``com.onyx.action.device.binding.INFO``). Any local app could launch these by component name — but they are all factory hardware-test UI screens (DRAM stress test, sensor readout, SIM diagnostics, e-ink waveform test), not shell/root primitives. The app does call ``Runtime.getRuntime().exec()`` and ``ShellUtils.execCommand()`` in a few places (``CTPCalibration``: ``setprop ctl.start ``; ``StorageUtils``: ``df``/``cat /proc/partitions``), but every argument traced back to a hardcoded string or a local file path, never to intent/network input — no command-injection or unguarded-root path was found. ``ResetFactoryActivity`` / ``ResetFactoryMainActivity`` (the ``MASTER_CLEAR``-adjacent screens) are *not* exported (no ``android:exported`` and no intent-filter → defaults to non-exported even at this app's ``targetSdkVersion=29``), so they are not reachable from other apps. Net assessment: broad attack surface *on paper* (permissions, exported test activities) but nothing that resolves to a usable privilege-escalation or bootloader-relevant primitive was found. A dedicated touch-calibration app that shells out to sysfs (``tscalibration-release``) ------------------------------------------------------------------------------------------ ``CalibrationManager.doExec()`` wraps a bare ``Runtime.getRuntime().exec(str)`` (no shell, so no metacharacter injection is possible — ``Runtime.exec(String)`` tokenizes on whitespace and never invokes ``/bin/sh``), called only as ``doExec("touch " + str)`` and ``doExec("rm " + CALIBRATE_FILE)``, where ``str``/``CALIBRATE_FILE`` are hardcoded device calibration-node paths (e.g. Wacom touch-raw-data nodes), not attacker- or network-supplied. This is a system app creating/removing files it has permission to touch on sysfs/procfs calibration nodes — normal factory-calibration behaviour, not a finding. Confirmed present, unmodified: the stock AOSP OEM-unlock toggle ==================================================================== ``artifacts/re_apk/Settings`` (``system_ext``'s ``Settings.apk``, ``com.android.settings``) contains ``OemUnlockPreferenceController.java``, byte-for-byte consistent with upstream AOSP: it gates the Developer-Options "OEM unlocking" switch on ``SystemProperties.get("ro.oem_unlock_supported")``, talks to ``OemLockManager`` (``isOemUnlockAllowed`` / ``setOemUnlockAllowedByUser`` / ``isDeviceOemUnlocked``), and additionally checks carrier-lock (``getAllowedCarriers``) and the ``no_factory_reset`` user restriction before allowing the toggle. No Onyx or vendor customization was found in this controller. This doesn't change anything in :doc:`platform-boot` (which already establishes the device is AVB 2.0, test-key signed, secure-boot-fuse-unfused) — it just confirms the standard Android-side UI/permission path for OEM unlock is present and unaltered, so the fastboot-side unlock story is governed entirely by :doc:`/abl/verified-boot`, not by anything in this app layer. Also present and equally stock: ``Enable16KOemUnlockDialog`` and ``EnableOemUnlockSettingWarningDialog`` (both standard AOSP 16 KB-page-size / OEM-unlock warning dialogs) and ``com.android.dynsystem.InstallationAsyncTask`` in ``DynamicSystemInstallationService.apk`` — the stock AOSP Dynamic System Updates (DSU/GSI) installer, which references ``AvbPublicKey`` and refuses to DSU-install over ``vbmeta``/``boot``/``dtbo``/``super_empty``/ ``system_other``/``scratch``/``userdata`` partitions (``UNSUPPORTED_PARTITIONS``) — again unmodified upstream AOSP code, not an Onyx addition. Qualcomm vendor apps: names checked, nothing custom found ============================================================== A batch of oddly-named ``system_ext_a/priv-app`` entries — ``Xpan``, ``xrcbservice``, ``xrvdservice``, ``xrwifiservice``, ``com.qualcomm.qti.services.systemhelper``, ``DynamicDDSService``, ``NtnSatApp``, ``WigigTetheringRRO``, ``dcf``, ``aptxui`` — all resolve to stock Qualcomm reference-platform packages by their manifest ``package=`` attribute (``vendor.qti.bluetooth.xpan``, ``com.qualcomm.qti.xrcb``, ``com.qualcomm.qti.xrvd.service``, ``com.qualcomm.qti.xrwifi``, ``com.qualcomm.qti.dynamicddsservice``, ``vendor.qti.data.ntnsatapp``, ``com.qualcomm.qti.server.wigig.tethering.rro``, ``com.qti.dcf``, ``com.qualcomm.qtil.aptxui``). These are carried over unmodified from the Qualcomm SM6350 BSP for hardware this device mostly doesn't use (XR controller-bridge, wigig tethering, satellite non-terrestrial-network data, aptX UI) — normal for a QCOM reference-platform-derived build, not Onyx-specific, and none showed anything relevant in the grep sweep beyond what the platform BSP apps generically contain. What was checked and came up empty ====================================== - No other exported, unguarded ````/````/```` with ``ota``/``update``/``recovery``/``factory``/``debug``/``admin``/ ``root``/``backdoor`` in its name was found across all 124 manifests beyond ``OnyxOtaService`` (already documented) and the two above (``kcb-release``'s content provider, ``app-market-release``'s receiver/ service). The many other exported-unguarded hits from the grep sweep (``ConfigUpdater``'s dozen receivers, GMS's Chimera services, Phonesky's install services, SystemUI's standard framework services) are stock AOSP/ GMS components whose components rely on protected-broadcast actions or Chimera's own internal caller checks — normal for these subsystems, not flagged further. - No ``vbmeta``/``avb``/``fastboot``/``dm-verity``/``slot_suffix`` string outside of the two legitimate AOSP hits already covered (``OemUnlockPreferenceController``, ``DynamicSystemInstallationService``). - No embedded private key, certificate, or high-entropy secret specific to this device or to Onyx was found beyond the one dead AES key constant above and the already-documented ``OnyxOtaService`` key (:doc:`platform-boot`). - No ``su``/root-escalation path reachable from network or IPC input; the only ``su``-capable code (``ShellUtils.checkRootPermission()``/ ``execCommand(..., true, ...)``) is the shared SDK's generic root-detection helper — grepping every call site across all 20 Onyx apps found no caller that actually passes ``su=true``; every real call in this sweep uses the non-``su`` ``sh`` path with hardcoded arguments. Honest limits ================ - The final step of ``PreInstallAppReceiver``'s download flow — whether ``AppStatus.INSTALL`` leads to an automatic ``PackageInstaller`` session or only a user-facing "tap to install" prompt — was not traced past ``DownloadAppManagerAction``. This matters for severity (silent local install-of-attacker's-choice vs. silent download + still requires a user tap). Similarly, whether anything actually calls ``AESEncryptUtils.encrypt/decrypt`` with the static ``key`` field reflectively, dynamically, or from a class this static sweep's call-site grep missed, was not exhaustively proven — "no static call site found" is not the same as "provably unreachable." - 47 of the 123 APKs decompiled with some internal jadx errors (partial bad-code on a minority of classes within each, above). The grep sweep still covers whatever jadx *did* emit for those classes; anything jadx failed to render at all (rare, and typically synthetic/obfuscated inner classes in heavily R8-shrunk apps like GMS/Phonesky) is not covered, including ``GmsCore.apk``'s un-decompiled tail (above). - Given the volume (124 apps, ~712K decompiled files), the grep sweep is necessarily keyword-driven rather than an exhaustive line-by-line read; the "what was checked and came up empty" section lists what was specifically searched for, not a claim that every method in every app was read. - The manifest regex pass covers every exported, unguarded service/receiver/provider by definition — that part of the sweep is complete. What it doesn't cover is anything that isn't an exported component: unsafe deserialization, path traversal, SSRF-shaped backend calls, logic bugs in non-exported code reachable only through a chain of intents, and so on. Those bug classes were never searched for here, so "came up empty" above means "nothing of the searched-for shape was found," not "these 124 apps have no other issues." Provenance ============ :Source: all APKs under ``artifacts/system_a``, ``artifacts/product_a`` and ``artifacts/system_ext_a`` (``find ... -iname '*.apk'``, 123 files plus the already-decompiled ``OnyxOtaService.apk``), decompiled with ``tools/jadx/bin/jadx`` 1.5.1 (``--show-bad-code``, 90 s timeout per APK via a manual bash watchdog — the ``timeout``/``gtimeout`` coreutils binary is not present on a stock macOS host) into ``artifacts/re_apk//{sources,resources}``. Per-APK decompile logs kept at ``artifacts/re_apk//jadx.log``. :Method: bulk decompile via a one-off shell loop (not checked in); grep sweep across every ``artifacts/re_apk/*/sources`` tree for the term groups listed above (bootloader/AVB, crypto/secrets, exec/privilege, exported-component name patterns), each hit manually read in context to separate real findings from bundled-library/obfuscation-name coincidence noise (documented above); manifest analysis via a small Python/regex pass over each APK's ``resources/AndroidManifest.xml`` to enumerate ``exported="true"`` services/receivers/providers lacking ``android:permission``. No code was executed; no device was touched. :Cross-refs: :doc:`app-inventory` (the un-decompiled full app catalogue this page follows up on — note the ``kcb-release`` purpose correction above), :doc:`platform-boot` (the one APK decompiled and audited before this sweep, ``OnyxOtaService``, and the AVB/A/B chain this sweep's OEM-unlock finding relates to), :doc:`security` (keystore2/FBE context for the device's overall trust model, unaffected by anything in this sweep).