前言
- 本文介紹Android中 AES 加、解密使用;
- 對稱加密,如何安全的保存秘鑰;
- 防止反編譯二次打包,動態(tài)調(diào)試。
Android 保證數(shù)據(jù)存儲安全無非就是加密,本文簡單介紹 AES 加密的用法。
然而Android apk 容易被反編譯,面對一些強大的逆向工作者,如果安全的保存我們的秘鑰,又成為一個問題。秘鑰硬編碼到 java 代碼層,顯然是不可取的。參考各方資料后,普遍做法是通過 jni 將秘鑰保存在 so 庫中。
那么你肯定又會問,放 so 庫中 代碼被反編譯 二次打包 debug 一下不是照樣能拿到嗎?
是的,但我們可以在加載 so 庫的時候驗證一下應(yīng)用簽名,簽名不一致的話,應(yīng)用直接退出就 ok 了。
一 、AES 加、解密
高級加密標(biāo)準(zhǔn)(英語:Advanced Encryption Standard,縮寫:AES),在密碼學(xué)中又稱Rijndael加密法,是美國聯(lián)邦政府采用的一種區(qū)塊加密標(biāo)準(zhǔn)。這個標(biāo)準(zhǔn)用來替代原先的DES,已經(jīng)被多方分析且廣為全世界所使用。經(jīng)過五年的甄選流程,高級加密標(biāo)準(zhǔn)由美國國家標(biāo)準(zhǔn)與技術(shù)研究院(NIST)于2001年11月26日發(fā)布于FIPS PUB 197,并在2002年5月26日成為有效的標(biāo)準(zhǔn)。2006年,高級加密標(biāo)準(zhǔn)已然成為對稱密鑰加密中最流行的算法之一。(百度百科)
加密算法原理本文不做討論,只介紹在Android 中的使用。
AES 加密
首先可通過KeyGenerator生成秘鑰
自己 可debug 一個 key 保存來。
String key = "";
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(128);
SecretKey secretKey = keyGenerator.generateKey();
key = Base64.encodeToString(secretKey.getEncoded(),Base64.NO_WRAP);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
然后借助Cipher類進行加解密
/**
* 加密
* @param text 要加密的內(nèi)容
* @param key 秘鑰
* @return 加密后的 內(nèi)容
*/
public String AESencrypt(String text ,String key) {
try {
Cipher cipher = Cipher.getInstance("AES");
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), "AES");
cipher.init(Cipher.ENCRYPT_MODE,secretKeySpec);
byte[] bytes = cipher.doFinal(text.getBytes());
return Base64.encodeToString(bytes,Base64.NO_WRAP);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
}
return "";
}
解密:
/**
* 解密
* @param text 加密過的內(nèi)容
* @param key 秘鑰
* @return 解密后的內(nèi)容
*/
public String AESdecrypt(String text, String key) {
try {
Cipher cipher = Cipher.getInstance("AES");
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), "AES");
cipher.init(Cipher.DECRYPT_MODE,secretKeySpec);
byte[] bytes = cipher.doFinal(Base64.decode(text, Base64.NO_WRAP));
String s = new String(bytes);
return s;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
}
return "";
}
測試代碼:
String text = "test the AES encrypt";
String encrypt = AESencrypt(text, key);
String decrypt = AESdecrypt(encrypt, key);
Log.d(Tag,encrypt);
Log.d(Tag,decrypt);
輸出就不展示了,用法很簡單。
二、安全的保存秘鑰實踐
通過 Android 的 NDK編程 將應(yīng)用簽名信息 保存在 C++代碼中 ,由于在加載 so 庫的時候會首先調(diào)用JNI_OnLoad方法,所以可再次方法中驗證簽名信息,不匹配則 返回 JNI_ERR 應(yīng)用會退出。
1、java 成獲取應(yīng)用簽名
/**
* 展示了如何用Java代碼獲取簽名
*/
private String getSign() {
try {
// 下面幾行代碼展示如何任意獲取Context對象,在jni中也可以使用這種方式
// Class<?> activityThreadClz = Class.forName("android.app.ActivityThread");
// Method currentApplication = activityThreadClz.getMethod("currentApplication");
// Application application = (Application) currentApplication.invoke(null);
// PackageManager pm = application.getPackageManager();
// PackageInfo pi = pm.getPackageInfo(application.getPackageName(), PackageManager.GET_SIGNATURES);
PackageManager pm = getPackageManager();
PackageInfo pi = pm.getPackageInfo(getPackageName(), PackageManager.GET_SIGNATURES);
Signature[] signatures = pi.signatures;
Signature signature0 = signatures[0];
return signature0.toCharsString();
} catch (Exception e) {
e.printStackTrace();
return "";
}
}
可 debug 此方法 將簽名保存在 C++代碼層:
static const char* SIGN = "308201dd3082……";
2、C++層獲取應(yīng)用簽名
將1中的 java 代碼通過 jni 翻譯成 cpp 語言
static jobject getApplication(JNIEnv *env) {
jobject application = NULL;
jclass activityThreadClazz = env->FindClass("android/app/ActivityThread");
if (activityThreadClazz!=NULL) {
jmethodID currentApplication = env->GetStaticMethodID(activityThreadClazz, "currentApplication", "()Landroid/app/Application;");
if (currentApplication!=NULL) {
application = env->CallStaticObjectMethod(activityThreadClazz, currentApplication);
} else {
LOGE("Cannot find method: currentApplication() in ActivityThread.");
}
env->DeleteLocalRef(activityThreadClazz);
} else {
LOGE("Cannot find class: android.app.ActivityThread");
}
return application;
}
static int verifySign(JNIEnv *env) {
jobject application = getApplication(env);
if (application==NULL) {
LOGE("application ==null");
return JNI_ERR;
}
jclass application_clz = env->GetObjectClass(application);
jmethodID getPackageManager = env->GetMethodID(application_clz, "getPackageManager", "()Landroid/content/pm/PackageManager;");
jobject packageManager = env->CallObjectMethod(application, getPackageManager);
jclass pm_clz = env->GetObjectClass(packageManager);
jmethodID getPackageName = env->GetMethodID(application_clz, "getPackageName", "()Ljava/lang/String;");
jstring package_name = (jstring)(env->CallObjectMethod(application, getPackageName));
jmethodID getPackageInfo = env->GetMethodID(pm_clz, "getPackageInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
jobject packageInfo = env->CallObjectMethod(packageManager, getPackageInfo,package_name,64);
jclass packageInfo_claz = env->GetObjectClass(packageInfo);
jfieldID signatures_field = env->GetFieldID(packageInfo_claz, "signatures", "[Landroid/content/pm/Signature;");
jobjectArray signatures = (jobjectArray)(env->GetObjectField(packageInfo, signatures_field));
jobject signature0 = env->GetObjectArrayElement(signatures, 0);
jclass signature0_clz = env->GetObjectClass(signature0);
jmethodID toCharsString = env->GetMethodID(signature0_clz, "toCharsString", "()Ljava/lang/String;");
jstring sign_str = (jstring)(env->CallObjectMethod(signature0, toCharsString));
env->DeleteLocalRef(application);
env->DeleteLocalRef(application_clz);
env->DeleteLocalRef(packageManager);
env->DeleteLocalRef(pm_clz);
env->DeleteLocalRef(package_name);
env->DeleteLocalRef(packageInfo);
env->DeleteLocalRef(packageInfo_claz);
env->DeleteLocalRef(signatures);
env->DeleteLocalRef(signature0);
env->DeleteLocalRef(signature0_clz);
const char *sign = env->GetStringUTFChars(sign_str, NULL);
if (sign==NULL) {
LOGE("內(nèi)存分配失敗");
return JNI_ERR;
}
LOGE("應(yīng)用中讀取到的簽名為:%s", sign);
LOGE("native中預(yù)置的簽名為:%s", SIGN);
int result = strcmp(sign, SIGN);
env->ReleaseStringUTFChars(sign_str,sign);
env->DeleteLocalRef(sign_str);
if (result ==0) {
LOGE("簽名一致");
return JNI_OK;
}
LOGE("簽名不一致");
return JNI_ERR;
}
3、JNI_OnLoad
jint JNI_OnLoad(JavaVM* vm, void* reserved){
JNIEnv *env = NULL;
if (vm->GetEnv((void**)&env,JNI_VERSION_1_6)!=JNI_OK) {
return JNI_ERR;
}
if (verifySign(env)==JNI_OK) {
return JNI_VERSION_1_6;
}
return JNI_ERR;
}
OK!現(xiàn)在我們可以 安全的保存我們的 秘鑰 了。
4、秘鑰寫在 C++層
java native 方法:
/**
* so中獲取秘鑰
* @return
*/
public native String getSecret();
C 對應(yīng)實現(xiàn):
JNIEXPORT jstring JNICALL
Java_com_keke_androidsecurity_MainActivity_getSecret(JNIEnv *env, jobject instance) {
//此處返回你的秘鑰
std::string secret = "……";
return env->NewStringUTF(secret.c_str());
}
5、測試Activity
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
String key = getSecret();
Log.d(Tag,key);
Log.d(Tag,getSign());
String text = "test the AES encrypt";
String encrypt = AESencrypt(text, key);
String decrypt = AESdecrypt(encrypt, key);
Log.d(Tag,encrypt);
Log.d(Tag,decrypt);
}
end