I am building an Android application that communicates with an online webservice. I plan on releasing the application's source code on GitHub. For my production version, which will utilize my personal webservice I want to allow only my digitally signed apk to connect.
Is is possible to request the APK's keystore and confirm the username/password from that keystore?
If this is not possible how else can I produce this functionality?
Edit:
I have read into the class Certificate It looks like I might be able to user public/private keys to confirm an identity. But I am still unsure of an implementation
I use this --
static public String getPackageFingerPrint( Context ctx ) {
PackageManager pm = ctx.getPackageManager();
String packageName = ctx.getPackageName();
int flags = PackageManager.GET_SIGNATURES;
PackageInfo packageInfo = null;
try {
packageInfo = pm.getPackageInfo(packageName, flags);
} catch (NameNotFoundException e) {
return "";
}
Signature[] signatures = packageInfo.signatures;
byte[] cert = signatures[0].toByteArray();
InputStream input = new ByteArrayInputStream(cert);
CertificateFactory cf = null;
try {
cf = CertificateFactory.getInstance("X509");
} catch (CertificateException e) {
return "";
}
X509Certificate c = null;
try {
c = (X509Certificate) cf.generateCertificate(input);
} catch (CertificateException e) {
return "";
}
try {
MessageDigest md = MessageDigest.getInstance("SHA1");
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();
} catch (NoSuchAlgorithmException e1) {
return "";
}
}
The problem I see with your approach is that anyone could determine the package fingerprint or your package and send it to your web-service. A better possibility would be to use a challenge-response mechanism: Your web-service sends you a unique session-token, which your app encrypts or digests using a shared algorithm, and then sends this encrypted token back to your service for verification. Of course, you wouldn't want to publish that algorithm to github.
You cannot restrict a web service to apks signed by a specific key.
The signature of your apk gets validated by the Android OS, and is not directly shared with web services the device connects to. Even if you read your signature from the keystore and send it along with requests, an attacker could just send the same signature (e.g. the same byte stream) without having access to your private key. He would just need to grab your signed apk, read the bytes from the keystore (or listen to a legitimate request) and spoil the data.
You would need to sign individual requests to have a level of security. But if you keep a private key in release versions (the key not distributed on gitHub), and sign requests using that key you are not safe as the private key is distributed as part of your apk and thus can get extracted easily.
In any way your API could get accessed by other apks.
However, there might be another way to restrict your API, for example by using license tokens, etc. In that case you would probably not care if the user builds the apk by himself, as long as he has a valid license token. If a license token is exploited and distributed, you could react to a high amount of traffic on that license token and for example block it from further requests. As I don't use Google Play I'm not sure in how far they can get validated from your server, but maybe the application licensing portal is a good starting point to search for suitable tokens.
Related
I'm trying to implement Google Cloud's Translation API using only a key as authentication in my Android app. I don't have a server or anything to improve security, so I wanted to restrict the API key to only be used by my Android app by specifying the app package name and SHA-1 hash. Here are the settings I used in the Cloud API & Services page:
To ensure the information was correct, I have the Gradle App ID set to the specified package name:
I ran the provided keytool command in windows for the SHA-1 hash:
Gradle's signing report tool also returns the same hash:
When I try to call a simple GET request for supported languages, the response is always a 403 Forbidden error. However, when I remove the restriction of only Android apps and the package/hash setting, the key works. Am I clearly doing something wrong here or forgetting something? When I even log BuildConfig.getPackage(), it returns the same package name. Both ways for getting the SHA-1 hash returned the same hash. I'm not sure what is going wrong.
Finally figured it out a couple days ago. Just sending the API request from the app package does not mean it's encoded with that information somewhere for the API endpoint to know it's from an authorized accessor.
The requests need two header properties to specify the source package and signature, in the form of:
{
"X-Android-Package": "package.name.here",
"X-Android-Cert": "debug:or:release:signature:here"
}
This is nowhere in Google documentation. I have no idea why, and it's frustrating that this is apparently something everyone needs to know when using pure REST instead of Google's client libraries.
One thing this means is that it's not a good idea to hard code these values in, since someone could decompile the apk and get the authorized acccessor credentials for these headers. But you can make it more difficult by using several available functions to get this information.
Getting the package name is simple:
context.getPackageName()
Signature requires some work
public String getSignature(Context context) {
PackageInfo info = null;
try {
info = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNING_CERTIFICATES);
Signature[] sigHistory = info.signingInfo.getSigningCertificateHistory();
byte[] signature = sigHistory[0].toByteArray();
MessageDigest md = MessageDigest.getInstance("SHA1");
byte[] digest = md.digest(signature);
StringBuilder sha1Builder = new StringBuilder();
for (byte b : digest) sha1Builder.append(String.format("%02x", b));
return sha1Builder.toString();
} catch (PackageManager.NameNotFoundException | NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
Make sure to read the SigningInfo documentation to use it correctly.
From there, set the request headers. I have been using Apache's Http Clients:
HttpGet getReq = new HttpGet(uri);
getReq.setHeader("X-Android-Package", context.getPackageName());
getReq.addHeader("X-Android-Cert", getSignature(context));
I received a very disturbing email from google:
We reviewed app name, with package name, and found that your app
uses software that contains security vulnerabilities for users. Apps
with these vulnerabilities can expose user information or damage a
user's device, and may be considered to be in violation of our
Malicious Behavior policy.
Below is the list of issues and the corresponding APK versions that
were detected in your recent submission. Please migrate your apps to
use the updated software as soon as possible and increment the version
number of the upgraded APK.
Vulnerability APK Version(s) Deadline to fix TrustManager You can find
more information about TrustManager in this Google Help Center
article.
486 September 14, 2020 Vulnerability APK Version(s) Deadline to fix To
confirm you've upgraded correctly, submit the updated version of your
app to the Play Console and check back after five hours. We'll show a
warning message if the app hasn't been updated correctly.
While these vulnerabilities may not affect every app, it's best to
stay up to date on all security patches.
If you have technical questions about the vulnerability, you can post
to Stack Overflow and use the tag "android-security." For
clarification on steps you need to take to resolve this issue, you can
contact our developer support team.
Best,
The Google Play Team
The only new thing added on app version 486 is an RSA encryption I added to some data.
This encryption is made in the following way: I manually generated an RSA key pair, stored the private key in my server and deployed the public key together with the apk.
For some requests in the app, I use the public key to encrypt the data before sending it through post request using URLConnection on the server side I decrypt it and process, then I send the response back to the user UNENCRYPTED.
so take in consideration the following:
0- there are only two requests in the app that use this technic
1- this update was made to ensure all the requests which arrive at the server came from my official app since last week I got 3 DoS attacks
2- those request existed for ages and always used standard android HTTPSUrlConnection without any extra encryption... what I made now was add an extra layer of encryption (how could it make the app less secure?)
3- the data transmitted is completely inoffensive
I know what a MITM attack is and I have done it for ages to reverse engineer some apps, I can't make this type of attack against my app without modifying the compiled code
That being said, how can I solve this problem without downgrade the work i took one week to implement?
this is the code:
post.put("p", "test");
post.put("s", Encrypter.encrypt(userid + "|" + did));
final String data = SimpleJsonDeserializer.getDefaultJson().toJson(post);
FirebaseCrashlytics.getInstance().log(data);
conn = (HttpsURLConnection) new URL(App.CLOUD_FUNCTIONS_HOST + "warsaw?v=2").openConnection();
conn.setDoInput(true);
conn.setDoOutput(true);
conn.setRequestMethod("POST");
conn.addRequestProperty("accept", "application/json");
conn.addRequestProperty("content-type", "application/json; charset=utf-8");
conn.addRequestProperty("content-length", data.length() + "");
conn.getOutputStream().write(data.getBytes());
conn.getOutputStream().flush();
conn.getOutputStream().close();
this is the Encrypter class (which is the new added on 486)
public final class Encrypter {
private static Cipher cipher;
public static void initialize(#NonNull final Context c) {
if (cipher == null) {
try {
cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
X509EncodedKeySpec spec = new X509EncodedKeySpec(Base64.decode("my public key base64 coded".getBytes(), Base64.DEFAULT));
KeyFactory kf = KeyFactory.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, kf.generatePublic(spec));
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeySpecException | InvalidKeyException e) {
App.shouldNeverHappen(e);
}
}
}
public static String encrypt(#NonNull final String s) throws IllegalBlockSizeException {
if (s.isEmpty() || s.length() > 240)
throw new IllegalBlockSizeException("Data must not be longer than 245 bytes");
try {
return new String(Base64.encode(cipher.doFinal(s.getBytes()), Base64.DEFAULT));
} catch (BadPaddingException e) {
App.shouldNeverHappen(e);
return "";
}
}
}
The only way to use "inject" a malicious key would modify the compiled code (which is IMPOSSIBLE to prevent) but even doing this the attacker would only be able to modify the post data signature in the requests SENT
what would be useless for any sort of phishing attack
I'm 100% sure this kind of warning is flagged by some automatical code inspection (which google spend ZERO work on re-evaluating manually), so since I got this problem, i've deployed several versions of my app just "masking around" the code to try not to be flagged on this automatic code inspection... not successful so far.
I have an app that generates a key for encryption/decryption and it is working just fine. I store my key in KeyStore and IV as first 12B in encrypted file saved on external storage. When I want to decrypt the file, I get the file from external storage (hence I get IV) and key from KeyStore, and I am able to get original content. My second application App2 can access file in external storage (hence it can get IV), but it can't get key from App1 KeyStore. I was reading about KeyChain and it says in official documentation it is not app private (Use the KeyChain API when you want system-wide credentials). Can I somehow store my key in this KeyChain or somewhere else so my App2 can get it (with some user approval or something similar). Here is the code I used to create and store key in App1.
private static SecretKey createAndStoreKey() {
KeyGenerator keyGen;
try {
// Generate 256-bit key
keyGen = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEY_STORE_NAME);
final KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build();
keyGen.init(keyGenParameterSpec);
SecretKey secretKey = keyGen.generateKey();
if(secretKey != null)
return secretKey;
else
return null;
}
catch (NoSuchProviderException e){
e.printStackTrace();
return null;
}
catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
}
catch (InvalidAlgorithmParameterException e){
e.printStackTrace();
return null;
}
}
Thank you all for the help.
Use the KeyChain API when you want system-wide credentials. When an app requests the use of any credential through the KeyChain API, users get to choose, through a system-provided UI, which of the installed credentials an app can access. This allows several apps to use the same set of credentials with user consent.
Use the Android Keystore provider to let an individual app store its
own credentials that only the app itself can access. This provides a
way for apps to manage credentials that are usable only by itself
while providing the same security benefits that the KeyChain API
provides for system-wide credentials. This method requires no user
interaction to select the credentials.
refrence
I have a rest server and I want only my app to be able to communicate with my REST server. ie if someone puts the url in a browser they wouldn't be able to communicate with my server
At the moment i am thinking of adding my key hash as an extra parameter on my request calls and then storing the key
The hash is not stored on my app but is automatically retrieved using the following method.
public static void getHashes(Activity act) {
PackageInfo info;
try {
info = act.getPackageManager().getPackageInfo("com.my.package.myapp", PackageManager.GET_SIGNATURES);
for (Signature signature : info.signatures) {
MessageDigest md;
md = MessageDigest.getInstance("SHA");
md.update(signature.toByteArray());
String something = new String(Base64.encode(md.digest(), 0));
//String something = new String(Base64.encodeBytes(md.digest()));
Log.i("hash key", something);
}
} catch (NameNotFoundException e1) {
Log.e("name not found", e1.toString());
} catch (NoSuchAlgorithmException e) {
Log.e("no such an algorithm", e.toString());
} catch (Exception e) {
Log.e("exception", e.toString());
}
}
I will have this method only called once for example at a certain time so that i can see it on the logcat and save it to my server after i compile to the production apk. this means it won't be outputed again for anyone else to see.
I will store this key on my server and my app will send this key everytime the a request is made. if the key is different then my server will not respond.
Is this method secure? can anyone see a flaw in it?
No, it's not secure.
An attacker can observe a single instance of this hash being sent in a request, and then include it in their own requests and your server won't be able to differentiate. The hash could, for example, be published on the web by the first person to obtain it.
However, this isn't just a problem with your approach: there are no known secure solutions to the general problem, see this answer for a slightly more comprehensive discussion (in the context of WCF rather than Android, but the argument is identical).
No, you need to ask the server for the challenge first and then return the hashed value of the app key, that challenge, content of the message itself (other than signatures) and some random array of bytes that is known on both app and server side. If done this way, should be immune to the network sniffing, replay attacks and also attempts to use legitimate app as a message template generator (valid authentication, other content may be replaced). You may also authenticate the server side same way.
Unfortunately almost nothing can be done against reverse-engineering of your app after the .apk file is in the hands of the attacker. However Google and Amazon app stores offer some protection against getting the .apk for disassembling.
I have a piece of code in my application that retrieves the application certificate at runtime and uses it as a key to encrypt some confidential information.
Is it possible for some attacker to get that certificate byte[] by decompiling my code or is that certificate only visible to my application?
Here is how I get the certificate:
PackageManager pm = this.getPackageManager();
String packageName = this.getPackageName();
int field = PackageManager.GET_SIGNATURES;
PackageInfo packageInfo;
packageInfo = pm.getPackageInfo(packageName, field);
Signature[] signatures = packageInfo.signatures;
// and here we have the DER encoded X.509 certificate
byte[] certificate = signatures[0].toByteArray()
PackageInfo signature is public key,
It is not private key. And this key is accessible to any app installed on your phone.
The application certificate is public; the private key used to sign the app never leaves your possession (unless you give it away). The private key cannot be reconstructed from the public key. The only way they would be able to decrypt your confidential info is via some sort of brute force attempt, against which there is ultimately no defense. Fortunately for you, the expense of running such a brute force attempt is so large that nobody in their right mind would attempt it unless they thought there were nuclear secrets etc. inside.