Monday, August 5, 2013

Android - APK Signature check programmatically in runtime

I've been at this for some time... I've tried to make my application distinct between debug version, release version, in some cases staging version, free, paid premium... and the list goes on and on...

So... I've asked the pro's, it was hard for me to describe the question, but I think it is right. I got no answer so I've went and searched a bit, and found an article explaining how this things work, I'm not going to go by all the explanation, I'll just lay it down.

First we define the certificates:

private enum SignedCertificate {
 Debug("...", null, true, true),
 Staging("...", Environments.Staging, false, true),
 Production("...", Environments.Production, false, false),
 Unknown("There is no such a certificate", Environments.Production, false, false), ;

 private final String md5;

 private final Environments environment;

 private final boolean debugMode;

 private final boolean showsLogs;

 private SignedCertificate(String md5, Environments environment, boolean debugMode, boolean showsLogs) {
  this.md5 = md5;
  this.environment = environment;
  this.debugMode = debugMode;
  this.showsLogs = showsLogs;
 }

 public static final SignedCertificate getCertificate(String md5) {
  md5 = md5.toLowerCase();
  for (SignedCertificate certificate : values()) {
   if (certificate.md5.toLowerCase().equals(md5))
    return certificate;
  }
  return Unknown;
 
}

The MD5 for each certificate can be copied from the export dialog in the final step of exporting a signed application in Eclipse, for debug md5 sign it with you debug keystore.

In order to retrieve the current certificate in runtime:

private void checkCertificate() {
 try {
  PackageManager pm = application.getPackageManager();
  Signature sig = pm.getPackageInfo(application.getPackageName(), PackageManager.GET_SIGNATURES).signatures[0];
  String md5Fingerprint = doFingerprint(sig.toByteArray(), "MD5");
  certificate = SignedCertificate.getCertificate(md5Fingerprint);
 } catch (Exception e) {
  logInfo("Error getting certificate, assuming release version...", e);
  certificate = SignedCertificate.Production;
 } finally {
  logInfo("Found Certificate: " + certificate.name());
  Log.setShowLogs(certificate.showsLogs);
 }
}

protected static String doFingerprint(byte[] certificateBytes, String algorithm)
  throws Exception {
 MessageDigest md = MessageDigest.getInstance(algorithm);
 md.update(certificateBytes);
 byte[] digest = md.digest();
 
 String toRet = "";
 for (int i = 0; i < digest.length; i++) {
  if (i != 0)
   toRet += ":";
  int b = digest[i] & 0xff;
  String hex = Integer.toHexString(b);
  if (hex.length() == 1)
   toRet += "0";
  toRet += hex;
 }
 return toRet;
}

After retrieving the certificate, implement any logic you may want according to the certificate, I've used the enum to define logs or debug mode, working environment and so on...

The nice thing about this implementation, is that it does not care about the application package name Google uses... if you have shared code between free and paid version of the application, and you would like to distinct them programmatically, I think this is a very nice solution...

Leave your comments below... :)

-- UPDATE --

I've released Cyborg not too long ago, and this Certificate Validation is build in and optimized further in terms of API and how much code you need to write to get it to work, You can find it here.

3 comments:

  1. brother what is Environments here.
    Environments cannot be resolved to a variable.

    ReplyDelete
    Replies
    1. Environment is an object you manage that hold the configuration for your environments, server address ports, protocol... etc.

      Delete