@@ -465,97 +465,7 @@ class ApkDetailsActivity : AppCompatActivity() {
465465 }
466466 val auditedLibs = libsToAudit.mapNotNull { pair -> auditNativeLibrary(pair.first, pair.second) }
467467
468- // Cryptographic Developer Signature and Schemes Auditing
469- val hasV1 = entriesList.any { it.second.startsWith(" META-INF/" ) && (it.second.endsWith(" .SF" ) || it.second.endsWith(" .DSA" ) || it.second.endsWith(" .RSA" ) || it.second.endsWith(" .EC" )) }
470- val baseApkPath = appInfo.sourceDir
471- val blockResults = parseApkSigningBlock(baseApkPath)
472- val hasV2 = blockResults.first
473- val hasV3 = blockResults.second
474- val hasV31 = blockResults.third
475- val hasV4 = java.io.File (" $baseApkPath .idsig" ).exists()
476-
477- var issuerName = " Unknown"
478- var subjectName = " Unknown"
479- var sigAlg = " Unknown"
480- var validityStr = " Unknown"
481- var sha256Hex = " Unknown"
482- var sha1Hex = " Unknown"
483-
484- val warnings = ArrayList <String >()
485- val bestPractices = ArrayList <String >()
486468
487- try {
488- val signatures = if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .P ) {
489- val packageInfoCert = if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .TIRAMISU ) {
490- packageManager.getPackageInfo(packageName, PackageManager .PackageInfoFlags .of(PackageManager .GET_SIGNING_CERTIFICATES .toLong()))
491- } else {
492- @Suppress(" DEPRECATION" )
493- packageManager.getPackageInfo(packageName, PackageManager .GET_SIGNING_CERTIFICATES )
494- }
495- packageInfoCert.signingInfo?.let { sInfo ->
496- if (sInfo.hasMultipleSigners()) sInfo.apkContentsSigners else sInfo.signingCertificateHistory
497- }
498- } else {
499- @Suppress(" DEPRECATION" )
500- val packageInfoCert = packageManager.getPackageInfo(packageName, PackageManager .GET_SIGNATURES )
501- @Suppress(" DEPRECATION" )
502- packageInfoCert.signatures
503- }
504-
505- if (! signatures.isNullOrEmpty()) {
506- val firstSig = signatures[0 ]
507- val certFactory = java.security.cert.CertificateFactory .getInstance(" X.509" )
508- val cert = certFactory.generateCertificate(java.io.ByteArrayInputStream (firstSig.toByteArray())) as java.security.cert.X509Certificate
509-
510- issuerName = cert.issuerDN.name
511- subjectName = cert.subjectDN.name
512- sigAlg = cert.sigAlgName
513-
514- val sdf = java.text.SimpleDateFormat (" yyyy-MM-dd HH:mm:ss" , java.util.Locale .getDefault())
515- validityStr = " From ${sdf.format(cert.notBefore)} to ${sdf.format(cert.notAfter)} "
516-
517- val md256 = java.security.MessageDigest .getInstance(" SHA-256" )
518- sha256Hex = md256.digest(firstSig.toByteArray()).joinToString(" :" ) { " %02X" .format(it) }
519-
520- val md1 = java.security.MessageDigest .getInstance(" SHA-1" )
521- sha1Hex = md1.digest(firstSig.toByteArray()).joinToString(" :" ) { " %02X" .format(it) }
522-
523- val sigAlgLower = sigAlg.lowercase(java.util.Locale .getDefault())
524- if (sigAlgLower.contains(" md5" ) || sigAlgLower.contains(" sha1" )) {
525- warnings.add(" 🔴 CRITICAL: Cryptographically weak signature algorithm ($sigAlg ). This makes the signature susceptible to collision and spoofing attacks." )
526- } else {
527- bestPractices.add(" 🟢 COMPLIANT: Secure signature algorithm ($sigAlg )." )
528- }
529-
530- if (issuerName == subjectName) {
531- bestPractices.add(" 🔵 INFO: Certificate is self-signed. This is standard for Android development, but ensure the private key is stored in a secure Keystore/HSM." )
532- }
533- }
534- } catch (e: Exception ) {
535- e.printStackTrace()
536- }
537-
538- val targetSdkVersion = appInfo.targetSdkVersion
539-
540- if (hasV1 && ! hasV2 && ! hasV3) {
541- warnings.add(" 🔴 WARNING: Only V1 (JAR) signature is present. The app is vulnerable to the Janus Vulnerability (CVE-2017-13156) on Android 5.0 to 8.0, allowing attackers to inject malicious DEX classes directly into the APK zip container without invalidating the cryptographic signature." )
542- } else if (! hasV1 && (hasV2 || hasV3)) {
543- bestPractices.add(" 🔵 INFO: JAR signing (V1) is disabled, but whole-file signing (V2/V3) is active. The app will only install on Android 7.0 (API 24) and higher." )
544- } else {
545- bestPractices.add(" 🟢 COMPLIANT: JAR signing (V1) and whole-file signing (V2/V3) are both active. Protected against Janus vulnerability." )
546- }
547-
548- if (! hasV2 && ! hasV3) {
549- warnings.add(" 🔴 WARNING: Missing modern whole-file Signature Schemes (V2/V3). Whole-file signing is mandatory for modern Android releases and guarantees faster on-device verification and ZIP structural integrity protection." )
550- }
551-
552- if (targetSdkVersion >= 30 ) {
553- if (! hasV4) {
554- bestPractices.add(" 🟡 RECOMMENDED: Target SDK is 30+ but APK Signature Scheme v4 is missing. Implementing V4 enables seamless incremental installations (Play Feature Delivery)." )
555- } else {
556- bestPractices.add(" 🟢 COMPLIANT: APK Signature Scheme v4 is active for incremental installs." )
557- }
558- }
559469
560470 // Update UI on main thread
561471 runOnUiThread {
@@ -567,55 +477,7 @@ class ApkDetailsActivity : AppCompatActivity() {
567477 tvObfuscationScore.text = " $scoreText$techText "
568478 tvObfuscationScore.setTextColor(if (obfuscationScore > 50 ) android.graphics.Color .parseColor(" #FF5722" ) else if (obfuscationScore > 15 ) android.graphics.Color .parseColor(" #FFEB3B" ) else android.graphics.Color .WHITE )
569479
570- // Bind dynamic Signature Schemes UI
571- val tvSigSchemes = findViewById<TextView >(R .id.tvSigSchemes)
572- val tvSigIssuer = findViewById<TextView >(R .id.tvSigIssuer)
573- val tvSigSubject = findViewById<TextView >(R .id.tvSigSubject)
574- val tvSigAlgorithm = findViewById<TextView >(R .id.tvSigAlgorithm)
575- val tvSigValidity = findViewById<TextView >(R .id.tvSigValidity)
576- val tvSigSHA256 = findViewById<TextView >(R .id.tvSigSHA256)
577- val tvSigSHA1 = findViewById<TextView >(R .id.tvSigSHA1)
578- val llSigFindings = findViewById<LinearLayout >(R .id.llSigFindings)
579-
580- llSigFindings.removeAllViews()
581-
582- val schemesBuilder = StringBuilder (" Signature Schemes Used:\n " )
583- schemesBuilder.append(" • V1 (JAR Signing): " ).append(if (hasV1) " ✓ YES" else " ✗ NO" ).append(" \n " )
584- schemesBuilder.append(" • V2 (APK v2): " ).append(if (hasV2) " ✓ YES" else " ✗ NO" ).append(" \n " )
585- schemesBuilder.append(" • V3 (APK v3): " ).append(if (hasV3) " ✓ YES" else " ✗ NO" ).append(" \n " )
586- if (hasV31) {
587- schemesBuilder.append(" • V3.1 (APK v3.1): ✓ YES\n " )
588- }
589- schemesBuilder.append(" • V4 (Incremental): " ).append(if (hasV4) " ✓ YES" else " ✗ NO" )
590-
591- tvSigSchemes.text = schemesBuilder.toString()
592- tvSigIssuer.text = " Issuer: $issuerName "
593- tvSigSubject.text = " Subject: $subjectName "
594- tvSigAlgorithm.text = " Signature Algorithm: $sigAlg "
595- tvSigValidity.text = " Validity: $validityStr "
596- tvSigSHA256.text = " SHA-256 Hash:\n $sha256Hex "
597- tvSigSHA1.text = " SHA-1 Hash:\n $sha1Hex "
598-
599- for (warning in warnings) {
600- llSigFindings.addView(TextView (this @ApkDetailsActivity).apply {
601- text = warning
602- setTextColor(android.graphics.Color .parseColor(" #FF5555" ))
603- textSize = 12f
604- setPadding(0 , 4 , 0 , 4 )
605- })
606- }
607480
608- for (bp in bestPractices) {
609- val isInfo = bp.contains(" INFO:" )
610- val isRecommended = bp.contains(" RECOMMENDED:" )
611- val colorHex = if (isInfo) " #8888FF" else if (isRecommended) " #FFFF55" else " #55FF55"
612- llSigFindings.addView(TextView (this @ApkDetailsActivity).apply {
613- text = bp
614- setTextColor(android.graphics.Color .parseColor(colorHex))
615- textSize = 12f
616- setPadding(0 , 4 , 0 , 4 )
617- })
618- }
619481
620482 if (auditedLibs.isNotEmpty()) {
621483 findViewById< androidx.cardview.widget.CardView > (R .id.cardNativeLibs).visibility = android.view.View .VISIBLE
@@ -649,100 +511,7 @@ class ApkDetailsActivity : AppCompatActivity() {
649511 return sdf.format(java.util.Date (timeMs))
650512 }
651513
652- private fun parseApkSigningBlock (apkPath : String ): Triple <Boolean , Boolean , Boolean > {
653- var hasV2 = false
654- var hasV3 = false
655- var hasV31 = false
656-
657- try {
658- java.io.RandomAccessFile (apkPath, " r" ).use { file ->
659- val length = file.length()
660- if (length < 22 ) return Triple (false , false , false )
661-
662- var eocdOffset = - 1L
663- val scanStart = maxOf(0L , length - 1024 )
664- val searchBytes = byteArrayOf(0x50 , 0x4b , 0x05 , 0x06 ) // EOCD signature (Little Endian)
665-
666- val buffer = ByteArray (1024 )
667- file.seek(scanStart)
668- val bytesRead = file.read(buffer)
669-
670- for (i in bytesRead - 4 downTo 0 ) {
671- if (buffer[i] == searchBytes[0 ] && buffer[i+ 1 ] == searchBytes[1 ] &&
672- buffer[i+ 2 ] == searchBytes[2 ] && buffer[i+ 3 ] == searchBytes[3 ]) {
673- eocdOffset = scanStart + i
674- break
675- }
676- }
677-
678- if (eocdOffset == - 1L ) return Triple (false , false , false )
679-
680- file.seek(eocdOffset + 16 )
681- val cdOffset = readIntLE(file)
682-
683- if (cdOffset.toLong() >= length || cdOffset < 32 ) return Triple (false , false , false )
684-
685- file.seek(cdOffset.toLong() - 16 )
686- val magicBytes = ByteArray (16 )
687- file.readFully(magicBytes)
688- val magicString = String (magicBytes, Charsets .US_ASCII )
689-
690- if (magicString == " APK Sig Block 42" ) {
691- file.seek(cdOffset.toLong() - 24 )
692- val blockLength = readLongLE(file)
693- val startOffset = cdOffset.toLong() - (blockLength + 8 )
694-
695- if (startOffset >= 0 ) {
696- file.seek(startOffset)
697- val totalPairsLength = blockLength - 24
698- var bytesParsed = 0L
699-
700- while (bytesParsed < totalPairsLength) {
701- val pairLength = readLongLE(file)
702- if (pairLength < 4 || pairLength > totalPairsLength - bytesParsed) break
703-
704- val pairId = readIntLE(file).toLong() and 0xFFFFFFFFL
705-
706- when (pairId) {
707- 0x7109871aL -> hasV2 = true
708- 0xf05368c0L -> hasV3 = true
709- 0x1b93ad61L -> hasV31 = true
710- }
711-
712- file.skipBytes((pairLength - 4 ).toInt())
713- bytesParsed + = pairLength + 8
714- }
715- }
716- }
717- }
718- } catch (e: Exception ) {
719- e.printStackTrace()
720- }
721-
722- return Triple (hasV2, hasV3, hasV31)
723- }
724-
725- private fun readIntLE (file : java.io.RandomAccessFile ): Int {
726- val b = ByteArray (4 )
727- file.readFully(b)
728- return (b[0 ].toInt() and 0xFF ) or
729- ((b[1 ].toInt() and 0xFF ) shl 8 ) or
730- ((b[2 ].toInt() and 0xFF ) shl 16 ) or
731- ((b[3 ].toInt() and 0xFF ) shl 24 )
732- }
733514
734- private fun readLongLE (file : java.io.RandomAccessFile ): Long {
735- val b = ByteArray (8 )
736- file.readFully(b)
737- return (b[0 ].toLong() and 0xFFL ) or
738- ((b[1 ].toLong() and 0xFFL ) shl 8 ) or
739- ((b[2 ].toLong() and 0xFFL ) shl 16 ) or
740- ((b[3 ].toLong() and 0xFFL ) shl 24 ) or
741- ((b[4 ].toLong() and 0xFFL ) shl 32 ) or
742- ((b[5 ].toLong() and 0xFFL ) shl 40 ) or
743- ((b[6 ].toLong() and 0xFFL ) shl 48 ) or
744- ((b[7 ].toLong() and 0xFFL ) shl 56 )
745- }
746515
747516 private fun copyToClipboard (label : String , text : String ) {
748517 val clipboard = getSystemService(android.content.Context .CLIPBOARD_SERVICE ) as android.content.ClipboardManager
0 commit comments