From 1f87d6c5713d454e25aa76411c67a395467a321a Mon Sep 17 00:00:00 2001 From: franzap <_@franzap.com> Date: Thu, 9 Apr 2026 21:23:17 -0300 Subject: [PATCH] Handle multiple signature hashes and check on last --- .../plugins/AndroidPackageManagerPlugin.kt | 57 ++++++++++++------- .../android_package_manager.dart | 10 ++-- .../dummy_package_manager.dart | 8 +-- .../installed_packages_snapshot.dart | 8 ++- .../package_manager/package_manager.dart | 6 +- lib/widgets/batch_progress_banner.dart | 9 +-- lib/widgets/install_button.dart | 8 +-- 7 files changed, 64 insertions(+), 42 deletions(-) diff --git a/android/app/src/main/kotlin/dev/zapstore/app/plugins/AndroidPackageManagerPlugin.kt b/android/app/src/main/kotlin/dev/zapstore/app/plugins/AndroidPackageManagerPlugin.kt index 249d17c..3ba6447 100644 --- a/android/app/src/main/kotlin/dev/zapstore/app/plugins/AndroidPackageManagerPlugin.kt +++ b/android/app/src/main/kotlin/dev/zapstore/app/plugins/AndroidPackageManagerPlugin.kt @@ -1023,15 +1023,39 @@ class AndroidPackageManagerPlugin : } } + // ═══════════════════════════════════════════════════════════════════════════════ + // SIGNING CERTIFICATE HELPERS + // ═══════════════════════════════════════════════════════════════════════════════ + + /** + * Returns the current signing certificates from a [android.content.pm.SigningInfo]. + * + * For multiple v1 signers: returns [apkContentsSigners]. + * For single signer (v2/v3, with or without key rotation): returns the + * newest certificate from [signingCertificateHistory] (last element), + * which matches what [apkContentsSigners] returns for APK files. + */ + private fun android.content.pm.SigningInfo.currentSigners(): Array { + return if (hasMultipleSigners()) { + apkContentsSigners ?: emptyArray() + } else { + val history = signingCertificateHistory + if (history.isNullOrEmpty()) emptyArray() else arrayOf(history.last()) + } + } + + private fun android.content.pm.Signature.sha256Hex(): String { + return MessageDigest.getInstance("SHA-256") + .digest(toByteArray()) + .joinToString("") { "%02x".format(it) } + } + /** * Extracts signing certificate hashes from an APK file. * * Returns SHA-256 hashes of the DER-encoded X.509 certificates (lowercase hex), matching the * algorithm used by zsp to produce apk_certificate_hash in NIP-82 SoftwareAsset events. * - * Uses SigningInfo.apkContentsSigners (API 28+) which prefers v3 key-rotation signers over - * v2/v1, consistent with apkverifier.PickBestApkCert used in zsp. - * * Returns null if extraction fails (treat as verification error, not skip). */ private fun extractApkCertHashes(apkPath: String): List? { @@ -1043,15 +1067,10 @@ class AndroidPackageManagerPlugin : ) ?: return null - val signers = packageInfo.signingInfo?.apkContentsSigners ?: return null - if (signers.isEmpty()) return null + val signers = packageInfo.signingInfo?.currentSigners() + if (signers.isNullOrEmpty()) return null - val digest = MessageDigest.getInstance("SHA-256") - signers.map { sig -> - digest.reset() - digest.update(sig.toByteArray()) - digest.digest().joinToString("") { "%02x".format(it) } - } + signers.map { it.sha256Hex() } } catch (e: Exception) { Log.w(TAG, "Failed to extract APK cert hashes from $apkPath: ${e.message}") null @@ -1412,13 +1431,13 @@ class AndroidPackageManagerPlugin : "bundleId" to pkg.packageName, "versionName" to pkg.versionName, "versionCode" to versionCode, - "signatureHash" to getSignatureHash(pkg.packageName), + "signatureHashes" to getSignatureHashes(pkg.packageName), "canInstallSilently" to canInstallSilently(pkg.packageName) ) } } - private fun getSignatureHash(packageName: String): String { + private fun getSignatureHashes(packageName: String): List { return try { val pm = context.packageManager val pkgInfo = @@ -1434,20 +1453,16 @@ class AndroidPackageManagerPlugin : pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES) } - val signatures = + val signers = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - pkgInfo.signingInfo?.apkContentsSigners ?: emptyArray() + pkgInfo.signingInfo?.currentSigners() ?: emptyArray() } else { @Suppress("DEPRECATION") pkgInfo.signatures ?: emptyArray() } - if (signatures.isEmpty()) return "" - - MessageDigest.getInstance("SHA-256").digest(signatures[0].toByteArray()).joinToString( - "" - ) { "%02x".format(it) } + signers.map { it.sha256Hex() } } catch (_: Exception) { - "" + emptyList() } } } diff --git a/lib/services/package_manager/android_package_manager.dart b/lib/services/package_manager/android_package_manager.dart index 4e69cec..6d7a556 100644 --- a/lib/services/package_manager/android_package_manager.dart +++ b/lib/services/package_manager/android_package_manager.dart @@ -453,8 +453,7 @@ final class AndroidPackageManager extends PackageManager { name: existingPkg?.name, version: target.version, versionCode: target.versionCode, - // Keep existing signature hash if available, will be updated by sync - signatureHash: existingPkg?.signatureHash ?? '', + signatureHashes: existingPkg?.signatureHashes ?? const [], installTime: DateTime.now(), canInstallSilently: existingPkg?.canInstallSilently ?? false, ); @@ -709,7 +708,10 @@ final class AndroidPackageManager extends PackageManager { final name = app['name'] as String?; final version = app['versionName'] as String? ?? '0.0.0'; final versionCode = app['versionCode'] as int?; - final signatureHash = app['signatureHash'] as String? ?? ''; + final rawHashes = app['signatureHashes']; + final signatureHashes = rawHashes is List + ? rawHashes.cast().toList() + : []; final canInstallSilently = app['canInstallSilently'] as bool? ?? false; if (canInstallSilently) anyCanInstallSilently = true; @@ -720,7 +722,7 @@ final class AndroidPackageManager extends PackageManager { name: name, version: version, versionCode: versionCode, - signatureHash: signatureHash, + signatureHashes: signatureHashes, installTime: null, canInstallSilently: canInstallSilently, ); diff --git a/lib/services/package_manager/dummy_package_manager.dart b/lib/services/package_manager/dummy_package_manager.dart index da79c87..bc7eaf4 100644 --- a/lib/services/package_manager/dummy_package_manager.dart +++ b/lib/services/package_manager/dummy_package_manager.dart @@ -11,21 +11,21 @@ final class DummyPackageManager extends PackageManager { name: 'Test App', version: '1.0.0', versionCode: 1, - signatureHash: 'dummy_signature_1', + signatureHashes: ['dummy_signature_1'], ), 'dev.zapstore.app': const PackageInfo( appId: 'dev.zapstore.app', name: 'Zapstore', version: '1.0.0', versionCode: 1, - signatureHash: 'dummy_signature_2', + signatureHashes: ['dummy_signature_2'], ), 'com.dummy.browser': const PackageInfo( appId: 'com.dummy.browser', name: 'Dummy Browser', version: '2.1.0', versionCode: 210, - signatureHash: 'dummy_signature_3', + signatureHashes: ['dummy_signature_3'], ), }, operations: const {}, @@ -57,7 +57,7 @@ final class DummyPackageManager extends PackageManager { appId: appId, version: target.version, versionCode: target.versionCode, - signatureHash: 'mock_signature', + signatureHashes: const ['mock_signature'], installTime: DateTime.now(), ); state = state.copyWith(installed: newInstalled); diff --git a/lib/services/package_manager/installed_packages_snapshot.dart b/lib/services/package_manager/installed_packages_snapshot.dart index 043af6f..2366ada 100644 --- a/lib/services/package_manager/installed_packages_snapshot.dart +++ b/lib/services/package_manager/installed_packages_snapshot.dart @@ -25,7 +25,7 @@ class InstalledPackagesSnapshot { 'name': p.name, 'version': p.version, 'versionCode': p.versionCode, - 'signatureHash': p.signatureHash, + 'signatureHashes': p.signatureHashes, 'canInstallSilently': p.canInstallSilently, }, ) @@ -69,12 +69,16 @@ class InstalledPackagesSnapshot { final map = Map.from(item); final appId = map['appId'] as String?; if (appId == null || appId.isEmpty) continue; + final rawHashes = map['signatureHashes']; + final signatureHashes = rawHashes is List + ? rawHashes.cast().toList() + : []; result[appId] = PackageInfo( appId: appId, name: map['name'] as String?, version: (map['version'] as String?) ?? '0.0.0', versionCode: map['versionCode'] as int?, - signatureHash: (map['signatureHash'] as String?) ?? '', + signatureHashes: signatureHashes, installTime: null, canInstallSilently: (map['canInstallSilently'] as bool?) ?? false, ); diff --git a/lib/services/package_manager/package_manager.dart b/lib/services/package_manager/package_manager.dart index 9fd1f62..91b1f12 100644 --- a/lib/services/package_manager/package_manager.dart +++ b/lib/services/package_manager/package_manager.dart @@ -22,7 +22,7 @@ class PackageInfo extends Equatable { required this.appId, required this.version, required this.versionCode, - required this.signatureHash, + this.signatureHashes = const [], this.name, this.installTime, this.canInstallSilently = false, @@ -32,7 +32,7 @@ class PackageInfo extends Equatable { final String? name; final String version; final int? versionCode; - final String signatureHash; + final List signatureHashes; final DateTime? installTime; final bool canInstallSilently; @@ -42,7 +42,7 @@ class PackageInfo extends Equatable { name, version, versionCode, - signatureHash, + signatureHashes, installTime, canInstallSilently, ]; diff --git a/lib/widgets/batch_progress_banner.dart b/lib/widgets/batch_progress_banner.dart index 89f4397..cec5f03 100644 --- a/lib/widgets/batch_progress_banner.dart +++ b/lib/widgets/batch_progress_banner.dart @@ -30,12 +30,13 @@ class UpdateAllRow extends ConsumerWidget { if (target == null) return false; // Skip apps with cert mismatch — they will fail during // install and waste bandwidth. - final installedHash = - pmState.installed[app.identifier]?.signatureHash ?? ''; + final installedHashes = + pmState.installed[app.identifier]?.signatureHashes ?? + const []; final targetHashes = target.certificateHashes; - if (installedHash.isNotEmpty && + if (installedHashes.isNotEmpty && targetHashes.isNotEmpty && - !targetHashes.contains(installedHash)) { + !installedHashes.any(targetHashes.contains)) { return false; } return true; diff --git a/lib/widgets/install_button.dart b/lib/widgets/install_button.dart index 7c6af5b..7294a08 100644 --- a/lib/widgets/install_button.dart +++ b/lib/widgets/install_button.dart @@ -244,12 +244,12 @@ class InstallButton extends ConsumerWidget { isDisabled: true, ); } - final installedHash = installedPkg?.signatureHash ?? ''; + final installedHashes = installedPkg?.signatureHashes ?? const []; final targetHashes = fileMetadata.certificateHashes; final isCertMismatch = - installedHash.isNotEmpty && + installedHashes.isNotEmpty && targetHashes.isNotEmpty && - !targetHashes.contains(installedHash); + !installedHashes.any(targetHashes.contains); if (isCertMismatch) { return _buildSimpleButton( context, @@ -692,7 +692,7 @@ class InstallButton extends ConsumerWidget { Installable fileMetadata, ) async { final currentVersion = installedPkg?.version ?? 'Unknown'; - final currentCertHash = installedPkg?.signatureHash ?? 'Unknown'; + final currentCertHash = installedPkg?.signatureHashes.firstOrNull ?? 'Unknown'; final updateCertHash = fileMetadata.certificateHash ?? 'Unknown'; final updateVersion = fileMetadata.version; final author = app.author.value;