12.7. APK sweep: bulk decompile of the remaining 123 system/product apps

Preinstalled apps (Boox, GMS, AOSP) catalogues the 124 preinstalled APKs by partition without decompiling them; Userspace boot — init, SELinux, A/B updates 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 Userspace boot — init, SELinux, A/B updates or Security and DRM userspace, but two are genuine exported-unguarded-component bugs of the same class as the OnyxOtaService finding, just lower-impact.

12.7.1. 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/<AppName>/:

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

12.7.2. 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.”

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

12.7.4. Real findings

12.7.4.1. 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 Preinstalled apps (Boox, GMS, AOSP) guesses at (“a Boox component”, name-inferred only) — it is the home-screen / content-browser shell app. Its manifest declares:

<provider
    android:name="com.onyx.android.sdk.data.db.OnyxSystemContentProvider_Provider"
    android:exported="true"
    android:authorities="com.onyx.system.database.ContentProvider"/>

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 (SystemConfigDatabaseSystemKeyValueItem, 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 <provider> 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.

12.7.4.2. 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:

<receiver android:name="com.onyx.appmarket.receiver.PreInstallAppReceiver"
          android:exported="true">
    <intent-filter>
        <action android:name="com.onyx.intent.action.DOWNLOAD_APP"/>
    </intent-filter>
</receiver>
<service android:name="com.onyx.appmarket.service.AppCheckUpdateService"
         android:exported="true">
    <intent-filter>
        <action android:name="com.onyx.appmarket_ACTION_APP_CHECK_UPDATE"/>
    </intent-filter>
</service>

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.

12.7.4.3. A dead, unobfuscated AES key in the shared Onyx SDK

com.onyx.android.sdk.utils.AESEncryptUtils (present verbatim in all ~20 Onyx apps) declares:

public static final String key = "8chLv9pat4WlE5IzyUwF9lOEtRYaPo9d1v0EYQnIak8iJDHLdIkJQW994iQZzLIoMhErq190IU8NEYACVnkrxtr6uOXTqkWuiClOC5ACbIWB0zUk5hALBaGWEvXDhudK";

Unlike the OnyxOtaService AES key (XOR-obfuscated in native .rodata, Userspace boot — init, SELinux, A/B updates), 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.

12.7.4.4. 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 <service>; 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.

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

12.7.5. 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 Userspace boot — init, SELinux, A/B updates (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 AVB enforcement code path (LoadImageAndAuth / libavb), 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.

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

12.7.7. What was checked and came up empty

  • No other exported, unguarded <service>/<receiver>/<provider> 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 (Userspace boot — init, SELinux, A/B updates).

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

12.7.8. 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.”

12.7.9. 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/<AppName>/{sources,resources}. Per-APK decompile logs kept at artifacts/re_apk/<AppName>/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:

Preinstalled apps (Boox, GMS, AOSP) (the un-decompiled full app catalogue this page follows up on — note the kcb-release purpose correction above), Userspace boot — init, SELinux, A/B updates (the one APK decompiled and audited before this sweep, OnyxOtaService, and the AVB/A/B chain this sweep’s OEM-unlock finding relates to), Security and DRM userspace (keystore2/FBE context for the device’s overall trust model, unaffected by anything in this sweep).