How to verify that app was signed by my certificate? - android

How do I check if the signature of my app matches the signature of the certificate that I used to sign it?
This is how I should be able to get the certificates fingerprint:
public String getCertificateFingerprint() throws NameNotFoundException, CertificateException, NoSuchAlgorithmException {
PackageManager pm = context.getPackageManager();
String packageName =context.getPackageName();
int flags = PackageManager.GET_SIGNATURES;
PackageInfo packageInfo = null;
packageInfo = pm.getPackageInfo(packageName, flags);
Signature[] signatures = packageInfo.signatures;
byte[] cert = signatures[0].toByteArray();
InputStream input = new ByteArrayInputStream(cert);
CertificateFactory cf = null;
cf = CertificateFactory.getInstance("X509");
X509Certificate c = null;
c = (X509Certificate) cf.generateCertificate(input);
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] publicKey = md.digest(c.getPublicKey().getEncoded());
StringBuffer hexString = new StringBuffer();
for (int i = 0; i < publicKey.length; i++) {
String appendString = Integer.toHexString(0xFF & publicKey[i]);
if (appendString.length() == 1)
hexString.append("0");
hexString.append(appendString);
}
return hexString.toString();
}
This is how I should be able to get the fingerprint of my certificate:
keytool -v -list -keystore filenameandpath
My problem is, that these two give back different results.
Could someone point out what I'm screwing up?

You are computing the MD5 hash of the wrong data. The fingerprint of a certificate is a hash (MD5, SHA1, SHA256, etc.) of the raw certificate. I.e., you should be computing the hash of these bytes:
byte[] cert = signatures[0].toByteArray();
E.g., the following computes a SHA1 fingerprint, just change SHA1 to MD5 if you prefer.
public String computeFingerPrint(final byte[] certRaw) {
String strResult = "";
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA1");
md.update(certRaw);
for (byte b : md.digest()) {
strAppend = Integer.toString(b & 0xff, 16);
if (strAppend.length() == 1)
strResult += "0";
strResult += strAppend;
}
strResult = strResult.toUpperCase(DATA_LOCALE);
}
catch (NoSuchAlgorithmException ex) {
ex.printStackTrace();
}
return strResult;
}

You can open the apk as a zip file and filter the ascii text from the binary content of META-INF/CERT.RSA and check there is it you who singed it.
try:
final void initVerify(Certificate certificate)
from: http://developer.android.com/reference/java/security/Signature.html

Use your code for collecting the fingerprint on the device in "test" mode -- meaning you have temporary code to emit that fingerprint to the log (or elsewhere). Be sure to test this using your production signing key, not the debug key!
Once you know from the device's perspective, you can remove the temporary code and elsewhere you can compare to what you've previously determined to be the key.
Be aware though that you're probably doing this to prevent someone from modifying your app and re-signing it with another key, but someone with the ability to do that also has the ability to modify your key checking. This is a problem that can be addressed with additional obfuscation but you'll need to come up with your own solution to minimize the chance of an attacker knowing what to look for.

the code below:
c.getPublicKey().getEncoded()
it should be like this
c.getEncoded()
i think md5 check by keytool is check the certfile,not the publickey

Related

Match Android apk SHA256 with SafetyNet apkCertificateDigestSha256

I am using SafetyNet to verify the integrity of the android app.
This is the flow as of now.
I generate a nonce value in the server and send it to the SafetyNet service to get the response.
I get the response from the server. Now I want to verify the result on the server.
I get a base64 string. I decode it and get the response as below.
{
"evaluationType": "BASIC",
"ctsProfileMatch": false,
"apkPackageName": "com.test.safetynetproject",
"apkDigestSha256": "CbU9JzwRzQneYqnEXewB56ZzPm1DgQ4LGUK0eGlWmyM=",
"nonce": "U2FnYXI=",
"apkCertificateDigestSha256": [
"AJRBzWCfJIY7QD2cp4sv9t0cCGMRGdxuID9VdPLV1H4="
],
"timestampMs": 1624099377557,
"basicIntegrity": false
}
Now i want to verify the apkCertificateDigestSha256. The sha256 created from my system using cmd is -
C:\Program Files\Java\jdk-11.0.11\bin>keytool -list -v -alias androiddebugkey -keystore C:\Users\.android\debug.keystore
Enter keystore password:
Alias name: androiddebugkey
Creation date: May 25, 2021
Entry type: PrivateKeyEntry
Certificate chain length: 1
Certificate[1]:
Owner: C=US, O=Android, CN=Android Debug
Issuer: C=US, O=Android, CN=Android Debug
Serial number: 1
Valid from: Tue May 25 11:48:00 IST 2021 until: Thu May 18 11:48:00 IST 2051
Certificate fingerprints:
SHA1: 43:16:E2:63:DB:2A:53:7C:7D:BB:E9:80:7B:05:1C:74:7C:84:66:A2
SHA256: 00:94:41:CD:60:9F:24:86:3B:40:3D:9C:A7:8B:2F:F6:DD:1C:08:63:11:19:DC:6E:20:3F:55:74:F2:D5:D4:7E
Signature algorithm name: SHA1withRSA (weak)
Subject Public Key Algorithm: 2048-bit RSA key
Version: 1
Warning:
The certificate uses the SHA1withRSA signature algorithm which is considered a security risk. This algorithm will be disabled in a future update.
The SHA256
00:94:41:CD:60:9F:24:86:3B:40:3D:9C:A7:8B:2F:F6:DD:1C:08:63:11:19:DC:6E:20:3F:55:74:F2:D5:D4:7E
Question -
I want to verify if the apkCertificateDigestSha256 is the same as the app certificate. Bt unable to find any way to do it.
Tries-
I tried to base64 decode the AJRBzWCfJIY7QD2cp4sv9t0cCGMRGdxuID9VdPLV1H4= and got a random byte array that does not match with the sha256 created in cmd.
Code -
val decode =
String(
Base64.decode(
responseJws!!.apkCertificateDigestSha256!![0],
Base64.DEFAULT
),
StandardCharsets.UTF_8
)
The output -
���A�`�$�;#=���/��c�n ?Ut���~
This is not matching 43:16:E2:63:DB:2A:53:7C:7D:BB:E9:80:7B:05:1C:74:7C:84:66:A2.
Update-
Found some ref but dont really know how to achieve this.
Ref1
How do I do the matching?
I have used SafetyNet API for accessing device's runtime env. I have kept signing certificate of app on server to verify its sha256 against what we get in the SafetyNet response. Below are the steps you can refer if applies to you too.
Get SHA256 fingerprint of signing X509Certificate
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] der = cert.getEncoded();
md.update(der);
byte[] sha256 = md.digest();
Encode sha256 to base64 string
String checksum = Base64.getEncoder().encodeToString(sha256)
Match checksum with apkCertificateDigestSha256 of SafetyNet response
I think this can help you
1.Find AttestationStatement file in GG example. and add this function:
public String bytesToHex(byte[] bytes) {
StringBuffer result = new StringBuffer();
for (byte b : bytes) result.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1));
return result.toString();
}
2.Find getApkCertificateDigestSha256 function and edit like this:
public byte[][] getApkCertificateDigestSha256() {
byte[][] certs = new byte[apkCertificateDigestSha256.length][];
for (int i = 0; i < apkCertificateDigestSha256.length; i++) {
certs[i] = Base64.decodeBase64(apkCertificateDigestSha256[i]);
System.out.println(bytesToHex(certs[i]));
}
return certs;
}
3.Find process() function in OnlineVerrify and add like this:
if (stmt.getApkPackageName() != null && stmt.getApkDigestSha256() != null) {
System.out.println("APK package name: " + stmt.getApkPackageName());
System.out.println("APK digest SHA256: " + Arrays.toString(stmt.getApkDigestSha256()));
System.out.println("APK certificate digest SHA256: " +
Arrays.deepToString(stmt.getApkCertificateDigestSha256()));
}
Now, run and you'll see the SHA-256 and let compare.
Not: there is no ":" charactor bettwen sha-256 generated cause i'm lazy. ^^.
Check the code here as reference on how to do the validations: https://github.com/Gralls/SafetyNetSample/blob/master/Server/src/main/java/pl/patryk/springer/safetynet/Main.kt
I just found it while searching for the same thing, and all credit goes to the person that owns the repo.
public class Starter {
static String keystore_location = "C:\\Users\\<your_user>\\.android\\debug.keystore";
private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
public static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
}
return new String(hexChars);
}
public static void main(String[] args) {
try {
File file = new File(keystore_location);
InputStream is = new FileInputStream(file);
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
String password = "android"; // This is the default password
keystore.load(is, password.toCharArray());
Enumeration<String> enumeration = keystore.aliases();
while(enumeration.hasMoreElements()) {
String alias = enumeration.nextElement();
System.out.println("alias name: " + alias);
Certificate certificate = keystore.getCertificate(alias);
System.out.println(certificate.toString());
System.out.println(certificate.getEncoded());
final MessageDigest md = MessageDigest.getInstance("SHA-1");
final MessageDigest md2 = MessageDigest.getInstance("SHA-256");
final byte[] der = certificate.getEncoded();
md.update(der);
md2.update(der);
final byte[] digest = md.digest();
final byte[] digest2 = md2.digest();
System.out.println(bytesToHex(digest));
System.out.println(bytesToHex(digest2));
byte[] encoded = Base64.getEncoder().encode(digest2);
System.out.println(encoded);
String checksum = Base64.getEncoder().encodeToString(digest2);
System.out.println(checksum); // This should match apkCertificateDigestSha256
}
} catch (java.security.cert.CertificateException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
Now Google depreciated SafetyAPI and Introduced PlayIntegrity API for attestation. PlayIntegrity Service provides the response as follows.
{
"tokenPayloadExternal": {
"accountDetails": {
"appLicensingVerdict": "LICENSED"
},
"appIntegrity": {
"appRecognitionVerdict": "PLAY_RECOGNIZED",
"certificateSha256Digest": ["pnpa8e8eCArtvmaf49bJE1f5iG5-XLSU6w1U9ZvI96g"],
"packageName": "com.test.android.safetynetsample",
"versionCode": "4"
},
"deviceIntegrity": {
"deviceRecognitionVerdict": ["MEETS_DEVICE_INTEGRITY"]
},
"requestDetails": {
"nonce": "SafetyNetSample1654058651834",
"requestPackageName": "com.test.android.safetynetsample",
"timestampMillis": "1654058657132"
}
}}
Response contains only certificateSha256Digest of the app (The sha256 digest of app certificates) instead of having apkDigestSha256 and apkCertificateDigestSha256.
How do we validate the received certificateSha256Digest at server?
If the app is deployed in Google PlayStore then follow the below steps
Download the App signing key certificate from Google Play Console (If you are using managed signing key) otherwise download Upload key certificate and then find checksum of the certificate.
public static Certificate getCertificate(String certificatePath)throws Exception {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X509");
FileInputStream in = new FileInputStream(certificatePath);
Certificate certificate = certificateFactory.generateCertificate(in);
in.close();
return certificate;
}
Generate checksum of the certificate
Certificate x509Cert = getCertificate("<Path of file>/deployment_cert.der");
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] x509Der = x509Cert.getEncoded();
md.update(x509Der);
byte[] sha256 = md.digest();
String checksum = Base64.getEncoder().encodeToString(sha256);
Then compare checksum with received certificateSha256Digest
String digest = jwsResponse.tokenPayloadExternal.appIntegrity.certificateSha256Digest;
if(checksum.contains(digest)){
//
}

How to add a owner AES key to AndroidKeyStore?

I want to store my AES-256 key to AndroidKeyStore, this AES-256 key is raw key (a random 32 byte). I try some code like this.
public foo () {
SecureRandom sr = new SecureRandom();
byte[] key = new byte[32];
sr.nextBytes(key);
try {
KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
ks.load(null);
SecretKeySpec sks = new SecretKeySpec(key, "AES");
SecretKeyFactory skf = SecretKeyFactory.getInstance("AES");
SecretKey sk = skf.generateSecret(sks);
ks.setEntry("key", new KeyStore.SecretKeyEntry(sk), new
KeyProtection.Builder(KeyProperties.PURPOSE_ENCRYPT).build());
KeyStore.SecretKeyEntry entry = (KeyStore.SecretKeyEntry) ks.getEntry("key", null);
SecretKey skLoad = (SecretKey) ks.getKey("key", null);
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, skLoad);
Log.i(TAG, Arrays.toString(cipher.doFinal(plainBytes)));
} catch (Exception e) {
e.printStackTrace();
}
}
I get the exception at line SecretKey sk = skf.generateSecret(sks);
java.security.spec.InvalidKeySpecException: To generate secret key in Android Keystore, use KeyGenerator initialized with android.security.keystore.KeyGenParameterSpec
I know we can save key with using KeyGenerator with KeyGenParameterSpec, but I have some reason to use owner key and KeyGenParameter seem can't import my owner key. So have any idea for this problem, thank all!
Generally you should not import keys from outside the key store, as they are insecure before they enter. So adding them later has limited benefits.
However, you can do a little trick: create a wrapping key in the key store and use it to wrap your symmetric key, and store the result. Then you can simply reverse the process when the key is needed again.
Unfortunately the best methods for storing keys such as (GCM-)SIV mode is generally not implemented, but hey, now you've at least heard about it.

How to decode SHA384 hash?

I am creating a SHA384 hash. I want to decode that hash. Is there any possible way to do this? Please help
Following is the code to get hash
public String getHash(String message) {
String algorithm = "SHA384";
String hex = "";
try {
byte[] buffer = message.getBytes();
MessageDigest md = MessageDigest.getInstance(algorithm);
md.update(buffer);
byte[] digest = md.digest();
for(int i = 0 ; i < digest.length ; i++) {
int b = digest[i] & 0xff;
if (Integer.toHexString(b).length() == 1) hex = hex + "0";
hex = hex + Integer.toHexString(b);
}
return hex;
} catch(NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
}
}
A cryptographically secure hashing function is a function such that a given arbitrary length input is processed into a fixed length output in such a way that is not reversible (computationally infeasible). Such functions include MD5 and the SHA (Secure Hash Algorithm) family (1, 224, 256, 384, 512, etc).
Once you take the hash of the input there is no going back to the original input. This property can be used for verification of message integrity as hashing the identical message produces a identical hash.
The website you visited simply stores hashes and their inputs side by side and does a database lookup for your hash to attempt to find a possible input (if it was previously added to the database).

Android and Python different SHA1 checksum of the same file

The scenario is the next:
I want to upload image to the server. But before uploading the file I have to send the SHA1 checksum of that file so the server could check if the file is already uploaded so I don't upload it again.
The problem is that for the same file I don't get the same SHA1 checksum in my app and on the server side.
Here is the code in my Android app:
public static String getSHA1FromFileContent(String filename)
throws NoSuchAlgorithmException, IOException {
final MessageDigest messageDigest = MessageDigest.getInstance("SHA-1");
InputStream is = new BufferedInputStream(new FileInputStream(filename));
final byte[] buffer = new byte[1024];
for (int read = 0; (read = is.read(buffer)) != -1;) {
messageDigest.update(buffer, 0, read);
}
is.close();
// Convert the byte to hex format
Formatter formatter = new Formatter();
for (final byte b : messageDigest.digest()) {
formatter.format("%02x", b);
}
String res = formatter.toString();
formatter.close();
return res;
}
And here is the code on the server side:
def hashFile(f):
sha1 = hashlib.sha1()
if hasattr(f, 'multiple_chunks') and f.multiple_chunks():
for c in f.chunks():
sha1.update(c)
else:
try:
sha1.update(f.read())
finally:
f.close()
return sha1.hexdigest()
What is the problem and why do I get different SHA1 checksums?
Turned out there was some server side image editing before generating the sha1 sum that wasn't meant to be done in this scenario. They made changes on the server side and now this is working perfectly.

Java Android - Decrypted byte array 255 bytes long instead of "Hello World!".length

I'm reading a lot since some weeks to implement an encrypt/decrypt algoritm for my Android application. I'm implementing a license key that is downloaded from my website and stored in the external storage of my Android device. the application read the content of the file and decrypt it using the server public key (yes i know that i should with client private key but it's ok for my purpose). The problem is that the final string has a lot of black square with question mark inside. i've read a lot of other posts here on stackoverflow, but i think that the "only" problem is that, even if there should be 10 chars in the string, the string is long 255 bytes (with 2048 bit RSA key) and the remaining chars are filled with black "". Why the newPlainText var is not long as "Hello World!" ? Here below my code... Many thanks in advance!
public boolean licenseValid() throws IOException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException{
java.io.File file = new java.io.File(Environment.getExternalStorageDirectory().toString() ,
"/folder/file.lic");
byte[] fileBArray = new byte[(int)file.length()];
FileInputStream fis = new FileInputStream(file);
// Read in the bytes
int offset = 0;
int numRead = 0;
while (offset < fileBArray.length
&& (numRead=fis.read(fileBArray, offset, fileBArray.length-offset)) >= 0) {
offset += numRead;
}
// Ensure all the bytes have been read in
if (offset < fileBArray.length) {
throw new IOException("Could not completely read file "+file.getName());
}
fis.close();
// Decrypt the ciphertext using the public key
PublicKey pubKey = readKeyFromFile();
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, pubKey);
byte[] newPlainText = cipher.doFinal(fileBArray);
// THE FOLLOWING TOAST PRINTS MANY <?> AND THAN THE DECRYPTED MESSAGE. THE TOTAL NUMBER OF CHARACTERS IS 255, EVEN IF I CHANGE ENCRYPTED TEXT!
toast(String.valueOf(cipher.doFinal(fileBArray).length));
if (new String(newPlainText, "utf-8").compareTo("Hello World!") == 0)
return true;
else
return false;
}
PublicKey readKeyFromFile() throws IOException {
Resources myResources = getResources();
//public key filename "pub.lic"
InputStream is = myResources.openRawResource(R.raw.pub);
ObjectInputStream oin =
new ObjectInputStream(new BufferedInputStream(is));
try {
BigInteger m = (BigInteger) oin.readObject();
BigInteger e = (BigInteger) oin.readObject();
RSAPublicKeySpec keySpec = new RSAPublicKeySpec(m, e);
KeyFactory fact = KeyFactory.getInstance("RSA");
PublicKey pubKey = fact.generatePublic(keySpec);
return pubKey;
} catch (Exception e) {
throw new RuntimeException("Spurious serialisation error", e);
} finally {
oin.close();
}
}
If you encrypt with RSA the input and output are always the same length as the key. In your case, that should be 256 bytes (=2048 bits), so first check your code, you are missing a byte.
When the input is shorter, you need to apply a padding, and it looks like your server and client are using a different one. Cipher.getInstance("RSA") will use the platform default, which is probably different for Android and Java SE. You need to specify the padding explicitly in both programs for this to work. Something like this:
Cipher cipher = Cipher.getInstance("RSA/None/PKCS1Padding");
BTW, you really don't want to distribute the private key with your app, so using the public key is the right thing to do. (Whether your whole encryption scheme is secure is another matter though).

Categories

Resources