I have an app available in Pro (paid) & Free (add supported) versions. Both versions are signed with the same key store, but each one with its own key alias.
Know, I'd like to develop a plugin compatible with both versions, offering data through a content provider. The data is sensitive, so I need my content provider to be accessible only from my app (both Pro & Free versions).
Using a permission with the android:protectionLevel="signature" is not working, because the Free & Pro versions do not have the same signature :/ . I guess I should have signed my two versions with the same key, but I thought each app on the Play Store needed to be signed with its own key.
So, does someone knows a solution ? Is there a way to gently ask Google to change the keys I'm using (I can prove my identity, as I did not loose my keys), or am I stuck??
EDIT : I could choose to un-publish the Pro version (as I have very few downloads at the moment) and re-upload it with the same certificate than the one used for the free version. If I do so, will I need to change its package?
Thanks in advance
signature
-level permissions are great, but fairly inflexible: the apps have to be signed by the same signing key. There are many cases in which we will want to check to see if another app is signed by the expected key, but that key is not our key. In your case, you have two keys. In other cases, one might be checking the signature of some partner app — confirming, for example, that the PayPal app you are about to send the user to is really the PayPal app and not malware that has replaced the PayPal app.
To validate the signature of another app, you can use PackageManager
. For example, here is the now-current edition of my SignatureUtils
class from my CWAC-Security library:
/***
Copyright (c) 2014 CommonsWare, LLC
Licensed under the Apache License, Version 2.0 (the "License"); you may
not use this file except in compliance with the License. You may obtain
a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package com.commonsware.cwac.security;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class SignatureUtils {
public static String getOwnSignatureHash(Context ctxt)
throws NameNotFoundException,
NoSuchAlgorithmException {
return(getSignatureHash(ctxt, ctxt.getPackageName()));
}
public static String getSignatureHash(Context ctxt, String packageName)
throws NameNotFoundException,
NoSuchAlgorithmException {
MessageDigest md=MessageDigest.getInstance("SHA-256");
Signature sig=
ctxt.getPackageManager()
.getPackageInfo(packageName, PackageManager.GET_SIGNATURES).signatures[0];
return(toHexStringWithColons(md.digest(sig.toByteArray())));
}
// based on https://stackoverflow.com/a/2197650/115145
public static String toHexStringWithColons(byte[] bytes) {
char[] hexArray=
{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B',
'C', 'D', 'E', 'F' };
char[] hexChars=new char[(bytes.length * 3) - 1];
int v;
for (int j=0; j < bytes.length; j++) {
v=bytes[j] & 0xFF;
hexChars[j * 3]=hexArray[v / 16];
hexChars[j * 3 + 1]=hexArray[v % 16];
if (j < bytes.length - 1) {
hexChars[j * 3 + 2]=':';
}
}
return new String(hexChars);
}
}
I use PackageManager
to get the Signature
for the given package, given its application ID. Despite the name, Signature
is really the public key of the keypair used to sign the app. The output of getSignatureHash()
is a colon-delimited set of hex character pairs, representing the SHA256 hash of the public key... the same value you get from using the Java 7+ keytool
command.
In your case, you are trying to determine, on the fly, whether an incoming operation is from the desired app, and if that desired app is the right one (versus repackaged malware, for example). In your case, Binder.getCallingUid()
will give you the Linux UID of the app that triggered the IPC that triggered your code. PackageManager
can give you the application ID of the app for that UID (ignoring android:sharedUserId
scenarios). You would then see if that application ID is the expected value, and if it is, check to see if the hashed signing key is the expected value. If either test fails, in the words of an arm-flailing robot, "Danger, Will Robinson! Danger!".
One significant caveat is that some developers will publish apps through channels where those apps get re-signed. Most notable here is the Amazon AppStore for Android, where Amazon intentionally wraps your app in their own quasi-DRM and signs that app using a key that they generate on your behalf. That's not the same key that you would necessarily be using elsewhere, so there may be multiple valid signature hashes that you would need to compare.