apk_inspector 0.1.0
apk_inspector: ^0.1.0 copied to clipboard
Inspect installed Android apps and APK files - signing certificates, permissions, exported components, flags and installer - with a permission catalog.
apk_inspector #
Inspect installed Android apps and APK files on disk: signing certificates (with key rotation), requested permissions with grant state and protection level, exported components, manifest flags and the installer. Comes with a plain-language permission catalog and a transparent risk score.
Android only; iOS not supported. On other platforms every call throws
ApkInspectorException with ApkInspectorErrorCode.unavailable.
Features #
- Listing:
installedApps(),installedAppsPage()andinstalledAppsStream()return lightweightAppSummaryobjects, sorted by package name and fetched in pages so large devices do not block the UI or hit binder limits. - Icons: PNG bytes drawn from the app's Drawable at the size you ask for,
including adaptive icons (drawn with the device mask). Load them lazily
with
iconOf(packageName), or inline withincludeIcons: true. appDetails(packageName)returnsAppDetails:- version name, long version code, min/target/compile SDK;
- first install and last update time;
- installer, initiating package and update owner through
getInstallSourceInfoon API 30+,getInstallerPackageNamebelow; - flags: system, updatedSystem, debuggable, testOnly, allowBackup, usesCleartextTraffic, largeHeap, hasCode;
- sharedUserId, dataDir, sourceDir, split APK paths, total APK size;
- requested permissions with granted state and protection level;
- activities, services, receivers and providers with exported, permission and effective enabled state;
- signing:
GET_SIGNING_CERTIFICATESon API 28+ (current signers, past signers from key rotation, multiple signers) andGET_SIGNATURESbelow. Each certificate reports SHA-256, SHA-1, subject, issuer, validity, serial, signature and key algorithm.
inspectApkFile(path): the same details for an APK on disk throughgetPackageArchiveInfo, withsourceDir/publicSourceDirset so the label and icon resolve. It also reports which signature schemes (v1, v2, v3, v3.1) the file carries, read from the ZIP central directory and the APK Signing Block.- Pure-Dart helpers:
PermissionCatalog,RiskScore,CertificateFingerprint,ApkSignatureSchemes. - The plugin manifest declares nothing: no permissions, no
<queries>.
Install #
dependencies:
apk_inspector: ^0.1.0
The plugin's minSdk is 21.
Package visibility (Android 11+) #
From Android 11 (API 30), an app only sees the packages it declares. Without
a declaration, installedApps() returns little more than system packages
and your own app, and appDetails() fails with notFound for anything
else. This plugin declares nothing on purpose. Your app chooses one of:
-
Narrow
<queries>(preferred): list the packages or intents you need.<manifest ...> <queries> <package android:name="com.example.partner" /> <intent> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent> </queries> </manifest> -
QUERY_ALL_PACKAGES: sees everything.<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />Google Play restricts this permission. It is allowed only when seeing all installed apps is part of the app's core purpose (for example device search, antivirus or app management tools), and you must justify it in the Play Console permissions declaration. Apps that do not qualify are rejected or removed. Use
<queries>if you can. The example app declaresQUERY_ALL_PACKAGESbecause it is an app inspector.
inspectApkFile() needs no visibility declaration, but your app must be able
to read the file path (for example a file in your app's own storage).
Usage #
import 'package:apk_inspector/apk_inspector.dart';
final inspector = ApkInspector();
Future<void> listApps() async {
// Streams page by page; icons are loaded separately.
await for (final app in inspector.installedAppsStream(pageSize: 50)) {
print('${app.label} ${app.packageName} ${app.versionName}');
}
final icon = await inspector.iconOf('com.android.chrome', size: 96);
print('icon bytes: ${icon?.length}');
}
Future<void> details(String packageName) async {
try {
final d = await inspector.appDetails(packageName);
print('target ${d.targetSdk}, installer ${d.installSource?.installer}');
print('debuggable ${d.flags.debuggable}');
for (final cert in d.signing?.currentSigners ?? const <CertificateInfo>[]) {
print('${cert.subject} ${cert.formattedSha256}');
}
for (final p in d.permissions) {
final entry = PermissionCatalog.describe(p.name,
protectionLevel: p.protectionLevel?.raw);
print('${p.name} granted=${p.granted} ${p.risk.name}: '
'${entry.description}');
}
for (final c in d.exportedComponents.where((c) => !c.isProtected)) {
print('open ${c.kind.name}: ${c.name}');
}
final risk = d.riskScore;
print('risk ${risk.score}/100');
risk.factors.forEach(print);
} on ApkInspectorException catch (e) {
if (e.code == ApkInspectorErrorCode.notFound) {
print('Not installed or not visible to this app.');
}
}
}
Future<void> apkFile(String path) async {
final d = await inspector.inspectApkFile(path);
print('${d.label} ${d.versionName} schemes: ${d.signingSchemes}');
const pinned =
'6973d591ad0002d66bcc0d4a30b080bbaec4627dbeee67e59d9c91378176723f';
final ok = d.signing?.currentSigners
.any((c) => CertificateFingerprint.matches(c.sha256, pinned)) ??
false;
print('signed by the expected key: $ok');
}
Permission risk buckets #
PermissionRisk.of(level) looks only at the protection level:
| Protection level | Bucket |
|---|---|
normal (any flags) |
low |
dangerous |
high |
signature, signatureOrSystem or internal with appop |
specialAccess |
signature, signatureOrSystem or internal without appop |
systemOnly |
| unknown / not reported | unknown |
RequestedPermission.risk uses the level the device reports, falling back to
the AOSP level in PermissionCatalog when the device reports none.
Risk score formula #
RiskScore.of(details) (or details.riskScore) is a heuristic for sorting
and triage, not a malware verdict. Every point shows up in
RiskReport.factors. The score is the sum below, clamped to 0..100:
| Factor | Points |
|---|---|
Each requested permission: high 6, specialAccess 10, systemOnly 2, unknown 1, low 0 |
capped at 50 |
debuggable |
15 |
testOnly |
5 |
usesCleartextTraffic |
5 |
allowBackup |
3 |
targetSdk below 23 (runtime permissions granted at install) |
10 |
| Each exported component without a permission (providers need both read and write permissions) | 1, capped at 15 |
sharedUserId set |
2 |
The factors add up to at most 105 before clamping. Requested permissions count whether or not they are granted, so APK files and installed apps score the same way.
Privacy #
The list of installed apps is personal data: it can reveal health
conditions, religion, finances or dating habits. This plugin reads package
data only on the device and sends nothing anywhere. If your app uploads
anything it collects with this plugin, tell users, get consent where the law
requires it, and complete the Google Play Data safety form. Google Play
treats the installed-apps list as sensitive, and QUERY_ALL_PACKAGES is
itself subject to the policy above.
Limitations #
- Package visibility applies: without
<queries>orQUERY_ALL_PACKAGES, Android 11+ hides most packages. exportedis the value PackageManager resolved. The plugin does not parse the binary manifest, so it cannot tell an explicitandroid:exportedfrom a default, and intent filters are not reported.- A permission defined by a package your app cannot see has no protection level on the device; the catalog fallback covers platform permissions only.
originatingPackageis only revealed to callers withINSTALL_PACKAGES.minSdkneeds Android 7.0+ andcompileSdkneeds Android 12+ on the device running the plugin.- For APK files, grant states, install times, installer and dataDir are null. Rotation history for APK files depends on the device's parser.
- Signature scheme detection reports which schemes are present. It does not
verify signatures (PackageManager's certificate data is what the plugin
reports for that). ZIP64 archives and v4
.idsigfiles are not handled. usesCleartextTrafficis the manifest flag; a network security config can refine it per domain.- The example app has an emulator integration test (
example/integration_test/, run withdart run tool/run_on_device.dart <applicationId>fromexample/). It compares the installed-app list withpm list packages, and this app's details and the copied debug APK withapksigner verify --print-certsandaapt2 dump badgingrun on the host against the installed APK. It also checkscom.android.settingsand the icons.
Tested on: Android Emulator, Pixel 10 system image (sdk_gphone16k_x86_64), API 37. Not yet
tested on a physical phone, OEM system apps or split APKs.
Data sources #
- Protection levels for the 1016 permissions in
PermissionCatalogare generated bytool/generate_permission_levels.dartfrom the AOSP platform manifestframeworks/base/core/res/AndroidManifest.xml, commit1cdfff555f4a21f71ccc978290e2e212e2f8b168(https://android.googlesource.com/platform/frameworks/base/+/1cdfff555f4a21f71ccc978290e2e212e2f8b168/core/res/AndroidManifest.xml). Copyright The Android Open Source Project, Apache License 2.0. - Protection flag names and values in the Kotlin layer come from the
protectionLevelattribute in AOSPframeworks/base/core/res/res/values/attrs_manifest.xmlat the same commit (Apache License 2.0). - APK Signing Block IDs (v2
0x7109871a, v30xf05368c0, v3.10x1b93ad61) are the constants in AOSPplatform/tools/apksig(V2SchemeConstants.java,V3SchemeConstants.java, commit184702d9d18877edf9e5296c4e191cf0aa2b5fbb, Apache License 2.0). The block layout follows https://source.android.com/docs/security/features/apksigning/v2. - The permission descriptions and groups were written for this package.
License #
MIT. See LICENSE. The generated protection-level table carries the Apache-2.0 notice of its AOSP source in its file header.
Made by Abdeldjalil Chougui.