mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 20:48:24 +00:00
Handle multiple signature hashes and check on last
This commit is contained in:
+36
-21
@@ -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<android.content.pm.Signature> {
|
||||
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<String>? {
|
||||
@@ -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<String> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String>().toList()
|
||||
: <String>[];
|
||||
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,
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<String, dynamic>.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<String>().toList()
|
||||
: <String>[];
|
||||
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,
|
||||
);
|
||||
|
||||
@@ -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<String> signatureHashes;
|
||||
final DateTime? installTime;
|
||||
final bool canInstallSilently;
|
||||
|
||||
@@ -42,7 +42,7 @@ class PackageInfo extends Equatable {
|
||||
name,
|
||||
version,
|
||||
versionCode,
|
||||
signatureHash,
|
||||
signatureHashes,
|
||||
installTime,
|
||||
canInstallSilently,
|
||||
];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user