7
7
import android .os .Build ;
8
8
import android .util .Log ;
9
9
import java .security .MessageDigest ;
10
+ import java .security .NoSuchAlgorithmException ;
10
11
11
12
public class SignatureComparison {
12
13
private static final String TAG = "AppSignatureVerifier" ;
13
-
14
+ private static final String HASH_ALGORITHM = String .join ("-" , "SHA" , "256" );
15
+
14
16
public boolean isAppSignatureValid (Context context , String expectedSignatureHash ) {
15
17
try {
16
- PackageManager packageManager = context .getPackageManager ();
17
- String packageName = context .getPackageName ();
18
- android .content .pm .PackageInfo packageInfo ;
19
-
20
- if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .P ) {
21
- packageInfo = packageManager .getPackageInfo (packageName , PackageManager .GET_SIGNING_CERTIFICATES );
22
- } else {
23
- packageInfo = packageManager .getPackageInfo (packageName , PackageManager .GET_SIGNATURES );
24
- }
25
-
26
- Signature [] signatures ;
27
- if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .P ) {
28
- SigningInfo signingInfo = packageInfo .signingInfo ;
29
- if (signingInfo != null ) {
30
- signatures = signingInfo .getApkContentsSigners ();
31
- } else {
32
- signatures = null ;
33
- }
34
- } else {
35
- signatures = packageInfo .signatures ;
18
+ // Validate input parameters
19
+ if (context == null || expectedSignatureHash == null ) {
20
+ throw new IllegalArgumentException ("Null context or expected hash" );
36
21
}
37
22
38
- if (signatures == null || signatures .length == 0 ) {
39
- Log .e (TAG , "No signatures found for the app." );
23
+ final String packageName = context .getPackageName ();
24
+ final Signature signature = getAppSignature (context , packageName );
25
+
26
+ if (signature == null ) {
27
+ Log .e (TAG , "No valid signature found" );
40
28
return false ;
41
29
}
42
30
43
- MessageDigest md = MessageDigest . getInstance ( "SHA-1" );
44
- byte [] originalDigest = md . digest (expectedSignatureHash . getBytes () );
45
- byte [] currentDigest = md . digest ( signatures [ 0 ]. toByteArray () );
31
+ final String currentHash = calculateSignatureHash ( signature );
32
+ final String normalizedExpected = normalizeHash (expectedSignatureHash );
33
+ final String normalizedCurrent = normalizeHash ( currentHash );
46
34
47
- boolean isValid = MessageDigest .isEqual (originalDigest , currentDigest );
48
- if (isValid ) {
49
- Log .d (TAG , "App signature is valid." );
50
- } else {
51
- Log .e (TAG , "App signature is invalid! Possible tampering detected." );
35
+ final boolean isValid = constantTimeEquals (normalizedExpected , normalizedCurrent );
36
+
37
+ if (!isValid ) {
38
+ Log .w (TAG , "Signature mismatch!\n " +
39
+ "Expected: " + normalizedExpected + "\n " +
40
+ "Actual: " + normalizedCurrent );
52
41
}
53
-
42
+
54
43
return isValid ;
55
44
} catch (Exception e ) {
56
- Log .e (TAG , "Error verifying app signature" , e );
45
+ Log .e (TAG , "Signature verification failed: " + e . getMessage () );
57
46
return false ;
58
47
}
59
48
}
60
- }
49
+
50
+ private Signature getAppSignature (Context context , String packageName )
51
+ throws PackageManager .NameNotFoundException {
52
+ final PackageManager pm = context .getPackageManager ();
53
+ final int flags = Build .VERSION .SDK_INT >= Build .VERSION_CODES .P ?
54
+ PackageManager .GET_SIGNING_CERTIFICATES :
55
+ PackageManager .GET_SIGNATURES ;
56
+
57
+ final android .content .pm .PackageInfo packageInfo = pm .getPackageInfo (packageName , flags );
58
+
59
+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .P ) {
60
+ final SigningInfo signingInfo = packageInfo .signingInfo ;
61
+ return (signingInfo != null && signingInfo .hasMultipleSigners ()) ?
62
+ signingInfo .getApkContentsSigners ()[0 ] :
63
+ signingInfo != null ? signingInfo .getSigningCertificateHistory ()[0 ] : null ;
64
+ }
65
+ return packageInfo .signatures != null ? packageInfo .signatures [0 ] : null ;
66
+ }
67
+
68
+ private String calculateSignatureHash (Signature signature )
69
+ throws NoSuchAlgorithmException {
70
+ final MessageDigest md = MessageDigest .getInstance (HASH_ALGORITHM );
71
+ final byte [] hashBytes = md .digest (signature .toByteArray ());
72
+ return bytesToHex (hashBytes );
73
+ }
74
+
75
+ // Constant-time comparison to prevent timing attacks
76
+ private boolean constantTimeEquals (String a , String b ) {
77
+ if (a .length () != b .length ()) {
78
+ return false ;
79
+ }
80
+
81
+ int result = 0 ;
82
+ for (int i = 0 ; i < a .length (); i ++) {
83
+ result |= a .charAt (i ) ^ b .charAt (i );
84
+ }
85
+ return result == 0 ;
86
+ }
87
+
88
+ private String normalizeHash (String hash ) {
89
+ return hash .replaceAll ("[^A-Fa-f0-9]" , "" ).toUpperCase ();
90
+ }
91
+
92
+ private static String bytesToHex (byte [] bytes ) {
93
+ final char [] hexChars = new char [bytes .length * 2 ];
94
+ for (int i = 0 ; i < bytes .length ; i ++) {
95
+ final int v = bytes [i ] & 0xFF ;
96
+ hexChars [i * 2 ] = "0123456789ABCDEF" .charAt (v >>> 4 );
97
+ hexChars [i * 2 + 1 ] = "0123456789ABCDEF" .charAt (v & 0x0F );
98
+ }
99
+ return new String (hexChars );
100
+ }
101
+ }
0 commit comments