Hiding encrypted secret API keys in C/C++ code and decrypting them via JNI.
Native code is harder to decompile than Java code. That's what you write secret keys in C/C++ code. To be safer, you can encrypt those secret keys before you fill in them. So you have to decrypt them to use.
Using SafetyNet Attestation APIs.
SafetyNet is a nifty solution in the following scenarios:
- I'm not sure if the app which is connecting to my server is that app I published.
- Can I trust this Android API?
- Is this a real, compatible device?
- Whether my application is running on a rooted device or not.
SafetyNet APIs are used to evaluate if the environment where your app runs is safe and compatible with the Android API or not. Verify the integrity, compatibility and signature of your app by calling Attestation APIs. Let your server decide to continue or to stop connecting to that untrusted device immediately.
- Hiding Secret keys in C/C++ code.
- Using RSAHelper to encrypt your secret keys (For example, authorization key, public key, iv parameters of DES algorithm or something). Paste those encrypted strings to this project. Then, fill in the parameters generated by RSAHelper in this project for decryption to decrypt the messages.
- Call SafetyNet Attestation APIs.
In the begining, you might need to create a keystore.properties file to keep some information you need.
storeFile=/Users/workspace/Keystores/xxx.jks
storePassword=xxxx
keyAlias=xxxx
keyPassword=xxxx
Run RSAHelper to get encrypted messages, using RSA modulus and exponent for decryption.
Hide RSA parameters in Config.cpp
JNIEXPORT jobjectArray JNICALL
Java_com_catherine_securitysample_JNIHelper_getKeyParams(JNIEnv *env, jobject instance) {
jobjectArray valueArray = (jobjectArray) env->NewObjectArray(2, env->FindClass("java/lang/String"), 0);
const char *hash[2];
//MODULUS
hash[0] = "Fill in the modulus created by RSAHelper";
//EXPONENT
hash[1] = "Fill in the exponent created by RSAHelper";
for (int i = 0; i < 2; i++) {
jstring value = env->NewStringUTF(hash[i]);
env->SetObjectArrayElement(valueArray, i, value);
}
return valueArray;
}
In JNIHelper,
/**
* Decrypt messages by RSA algorithm<br>
*
* @param message
* @return Original message
* @throws NoSuchAlgorithmException
* @throws NoSuchPaddingException
* @throws InvalidKeyException
* @throws IllegalBlockSizeException
* @throws BadPaddingException
* @throws UnsupportedEncodingException
* @throws InvalidAlgorithmParameterException
* @throws InvalidKeySpecException
* @throws ClassNotFoundException
*/
public String decryptRSA(String message) throws NoSuchAlgorithmException, NoSuchPaddingException,
InvalidKeyException, IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException,
InvalidAlgorithmParameterException, ClassNotFoundException, InvalidKeySpecException {
Cipher c2 = Cipher.getInstance(Algorithm.rules.get("RSA")); // 创建一个Cipher对象,注意这里用的算法需要和Key的算法匹配
BigInteger m = new BigInteger(Base64.decode(getKeyParams()[0].getBytes(), Base64.DEFAULT));
BigInteger e = new BigInteger(Base64.decode(getKeyParams()[1].getBytes(), Base64.DEFAULT));
c2.init(Cipher.DECRYPT_MODE, convertStringToPublicKey(m, e)); // 设置Cipher为解密工作模式,需要把Key传进去
byte[] decryptedData = c2.doFinal(Base64.decode(message.getBytes(), Base64.DEFAULT));
return new String(decryptedData, Algorithm.CHARSET);
}
/**
* You can component a publicKey by a specific pair of values - modulus and
* exponent.
*
* @param modulus When you generate a new RSA KeyPair, you'd get a PrivateKey, a
* modulus and an exponent.
* @param exponent When you generate a new RSA KeyPair, you'd get a PrivateKey, a
* modulus and an exponent.
* @throws ClassNotFoundException
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
*/
private Key convertStringToPublicKey(BigInteger modulus, BigInteger exponent)
throws ClassNotFoundException, NoSuchAlgorithmException, InvalidKeySpecException {
byte[] modulusByteArry = modulus.toByteArray();
byte[] exponentByteArry = exponent.toByteArray();
RSAPublicKeySpec rsaPublicKeySpec = new RSAPublicKeySpec(new BigInteger(modulusByteArry),
new BigInteger(exponentByteArry));
KeyFactory kFactory = KeyFactory.getInstance(Algorithm.KEYPAIR_ALGORITHM);
PublicKey publicKey = kFactory.generatePublic(rsaPublicKeySpec);
return publicKey;
}
- There are two ways to use JNI -- CmakeLists.txt and Android.mk, I used Android.mk here.
- Create jni folder in main/ .Then add Android.mk, Application.mk and C/C++ files(Config.cpp).
- In build.gradle:
externalNativeBuild {
ndkBuild {
path 'src/main/jni/Android.mk'
}
}
- In JNIHelper,
static {
//relate to LOCAL_MODULE in Android.mk
System.loadLibrary("keys");
}
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
public native String[] getAuthChain(String key);
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
public native String[] getKeyParams();
private final static String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
StringBuilder sb = new StringBuilder();
try {
// Example of a call to a native method
TextView tv = (TextView) findViewById(R.id.sample_text);
String[] authChain = getAuthChain("LOGIN");
sb.append("Decrypted secret keys\n[ ");
for (int i = 0; i < authChain.length; i++) {
sb.append(decryptRSA(authChain[i]));
sb.append(" ");
}
sb.append("]\n");
String[] authChain2 = getAuthChain("OTHER");
sb.append("secret keys\n[ ");
for (int i = 0; i < authChain.length; i++) {
sb.append(authChain2[i]);
sb.append(" ");
}
sb.append("]");
Log.d(TAG, sb.toString());
tv.setText(sb.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
In the begining, you might need to create a keystore.properties file to keep some information you need.
storeFile=/Users//Keystores/xxx.jks
storePassword=xxxx
keyAlias=xxxx
keyPassword=xxxx
Things you must know before you start developing.
- Use SafetyNetApi the deprecated class or you'd probably get 403 error by calling SafetyNet.getClient(context)
- JWS (JSON Web Token) contains header, payload and signature, your environment information is refer to the payload.
- There are two APIs you might need - SafetyNet API and Android device verification API. You get your device and app information with SafetyNet API, and check whether the information is truthful with another. Then let your server decide the next step (like shutting down the app or something).
- Attestation should not run on your UI thread, you can use HandlerThread to deal with this situation.
JWS Header: A string representing a JSON object that describes the digital signature or MAC operation applied to create the JWS Signature value. JWS Payload: The bytes to be secured -- aka, the message. The payload can contain an arbitrary sequence of bytes. JWS Signature: A byte array containing the cryptographic material that secures the JWS Header and the JWS Payload. For more information, see https://tools.ietf.org/html/rfc7515
-
You can skip this step if you don't verify your attestation response from google APIs (I feel like this step is kind of like https validation. It probabily means man-in-the-middle attacks are allowed if you do not check the response.). Of course you can also validate the SSL certificate chain by yourself. Google highly recommends you to check your JWS statement.
-
What "Android Device Verification API" dose is only checking JWS certificates and signatures. Its response (JSON payload) has nothing to do with the Android environments in which your app run.
-
Get your API key here: https://console.developers.google.com/, and don't forget to add and enable "Android Device Verification API".
-
Make sure the API key you post to "Android Device Verification API" is unrestricted.
-
There is a daily quota restriction of connecting "Android Device Verification API".
-
In gradle.porpeties, add your google API key
safetynet_api_key = XXXXXXXXX
- In build.gradle
android {
defaultConfig {
buildConfigField("String", "API_KEY", "\"${safetynet_api_key}\"")
}
}
- DO NOT add any safetyNet meta-data in your manifest
<!--<meta-data-->
<!--android:name="com.google.android.safetynet.ATTEST_API_KEY"-->
<!--android:value="${safetynet_api_key}" />-->
In MyApplication,
public class MyApplication extends Application {
public HandlerThread safetyNetLooper;
public static MyApplication INSTANCE;
@Override
public void onCreate() {
INSTANCE = this;
safetyNetLooper = new HandlerThread("SafetyNet task");
safetyNetLooper.start();
super.onCreate();
}
}
In manifest,
<application
android:name=".MyApplication">
</application>
SafetyNetHelper safetyNetHelper = new SafetyNetHelper(BuildConfig.API_KEY);
Handler handler = new Handler(MyApplication.INSTANCE.safetyNetLooper.getLooper());
GoogleApiClient googleApiClient = new GoogleApiClient.Builder(contex)
.addApi(SafetyNet.API)
.addConnectionCallbacks(googleApiConnectionCallbacks)
.addOnConnectionFailedListener(googleApiConnectionFailedListener)
.setHandler(handler) //Run on a new thread
.build();
//Don't forget to connect!
googleApiClient.connect();
byte[] requestNonce = generateOneTimeRequestNonce();
SafetyNet.SafetyNetApi.attest(googleApiClient, requestNonce)
.setResultCallback(new ResultCallback<SafetyNetApi.AttestationResult>() {
@Override
public void onResult(@NonNull SafetyNetApi.AttestationResult attestationResult) {
Status status = attestationResult.getStatus();
boolean isSuccess = status.isSuccess();
if (!isSuccess)
callback.onFail(ErrorMessage.SAFETY_NET_API_NOT_WORK, ErrorMessage.SAFETY_NET_API_NOT_WORK.name());
else {
try {
final String jwsResult = attestationResult.getJwsResult();
final JwsHelper jwsHelper = new JwsHelper(jwsResult);
final AttestationResult response = new AttestationResult(jwsHelper.getDecodedPayload());
if (!verifyJWSResponse) {
callback.onResponse(response.getFormattedString());
//release SafetyNet HandlerThread
MyApplication.INSTANCE.safetyNetLooper.quit();
} else {
AndroidDeviceVerifier androidDeviceVerifier = new AndroidDeviceVerifier(ctx, jwsResult);
androidDeviceVerifier.verify(new AttestationTaskCallback() {
@Override
public void error(String errorMsg) {
callback.onFail(ErrorMessage.FAILED_TO_CALL_GOOGLE_API_SERVICES, errorMsg);
//release SafetyNet HandlerThread
MyApplication.INSTANCE.safetyNetLooper.quit();
}
@Override
public void success(boolean isValidSignature) {
if (isValidSignature)
callback.onResponse("isValidSignature true\n\n" + response.getFormattedString());
else
callback.onFail(ErrorMessage.ERROR_VALID_SIGNATURE, ErrorMessage.ERROR_VALID_SIGNATURE.name());
//release SafetyNet HandlerThread
MyApplication.INSTANCE.safetyNetLooper.quit();
}
});
}
} catch (JSONException e) {
callback.onFail(ErrorMessage.EXCEPTION, e.getMessage());
//release SafetyNet HandlerThread
MyApplication.INSTANCE.safetyNetLooper.quit();
}
}
}
});
- SafetyNetUtils
For more information, see https://tools.ietf.org/html/rfc7515
The JWS payloads I got by running this app on the real device and the nox monitor are a little different.
- On my mobile phone, ctsProfileMatch and basicIntegrity were both true.
{
"nonce":"pUkGirEXYOQefux33VWeSEmR0kBkLNGQaiQiZvE3VAc=",
"timestampMs":1498814112718,
"apkPackageName":"com.catherine.securitysample",
"apkDigestSha256":"FPgrs1x05EaZiJkfKaitzEXTazg+GDDqYtbR5XyJiJE=",
"ctsProfileMatch":true,
"extension":"CbRP9k08+pZE",
"apkCertificateDigestSha256":["9mLFS3eHWOBcHlA4MmODmfGvzgkbg2YSQ2z/ww9lCfw="],
"basicIntegrity":true
}
- On a rooted one, ctsProfileMatch and basicIntegrity were both false.
{
"nonce":"FWypInssEmM+YBl61JCVPFx+bC5naGuIPQhkP3ait68=",
"timestampMs":1502958413970,
"apkPackageName":"",
"apkDigestSha256":"",
"ctsProfileMatch":false,
"extension":"CdVwxgDa4bqk",
"apkCertificateDigestSha256":"",
"basicIntegrity":false
}
- First you must finish step1.
- You can skip this step if you don't verify your attestation response from google APIs (I feel like this step is kind of like https validation. It probabily means man-in-the-middle attacks are allowed if you do not check the response.). Of course you can also validate the SSL certificate chain by yourself. Google highly recommends you to check your JWS statement.
- What "Android Device Verification API" dose is only checking JWS certificates and signatures. Its response (JSON payload) has nothing to do with the Android environments in which your app run.
- I have this app call google Android Device Verification API until daily API queries exceed the quota limit. Then, instead of google server, the JWS response will be verified by devices. Here is a sample AttestationAsyncTask.
Follow these steps to verify the origin of the JWS message:
- Extract the SSL certificate chain from the JWS message.
- Validate the SSL certificate chain and use SSL hostname matching to verify that the leaf certificate was issued to the hostname attest.android.com.
- Use the certificate to verify the signature of the JWS message.
- Post the JWS payload to your server to check the payload and return commands to your app.
If you want to read more about google security services for Android, you can watch Google Security Services for Android: Mobile Protections at Google Scale, the YouTube video. Or you could see README_cn, they are almost the same.
Your workflow would be one of them:
- (Security risk) Call Attestation APIs → Get a JWS response → Send JWS to your server → ?? - it depends on your server.
- (Recommendation) Call Attestation APIs → Get a JWS response → Check the JWS response (step 4) → Send valid JWS to your server → ?? - it depends on your server.
As you add new secret keys, you must refill modulus, exponent and the other encrypted keys, because you'll get different RSA KeyPair (private key and public key) for every execution.
- Server Authentication During SSL Handshake
- Verifying a Certificate Chain
- JSON Web Signature (JWS) draft-jones-json-web-signature-01
Copyright 2017 Catherine Chen (https://github.com/Catherine22)
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.