SkillAgentSearch skills...

SecuritySample

(Android) Hide encrypted secret API keys in C/C++ code, retrieve and decrypt them via JNI. Google SafetyNet APIs example.

Install / Use

/learn @Catherine22/SecuritySample

README

SecuritySample

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:

  1. I'm not sure if the app which is connecting to my server is that app I published.
  2. Can I trust this Android API?
  3. Is this a real, compatible device?
  4. 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.

Features

1. Get encrypted data from native code through NDK

  • 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.

2. Evaluate the security and compatibility of the Android environments in which your apps run

  • Call SafetyNet Attestation APIs.

Instruction of JNI, encryption and decryption

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

Step1. Generate a pair of RSA keys and encrypt your messages.

Run [RSAHelper] to get encrypted messages, using RSA modulus and exponent for decryption.

Step2. Fill in MODULUS and EXPONENT

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;
}

Step3. Add the decryption method to your project

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;
}

Step4. Create C/C++ files

  • 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]).

![JNI 1][ndk1]

  • 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();

Step5. Run your app

    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();
    }
}

Instruction of SafetyNet Attestation APIs

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.

  1. Use SafetyNetApi the deprecated class or you'd probably get 403 error by calling SafetyNet.getClient(context)
  2. JWS (JSON Web Token) contains header, payload and signature, your environment information is refer to the payload.
  3. 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).
  4. 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

Step1. Generate an API key from google developers console (optional)

  • 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}" />-->

Step2. Build GoogleApiClient and call SafetyNet APIs

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");
        safet
View on GitHub
GitHub Stars54
CategoryDevelopment
Updated24d ago
Forks6

Languages

Java

Security Score

100/100

Audited on Mar 8, 2026

No findings