前言
- 本文介绍Android中 AES 加、解密使用;
- 对称加密,如何安全的保存秘钥;
- 防止反编译二次打包,动态调试。
Android 保证数据存储安全无非就是加密,本文简单介绍 AES 加密的用法。
然而Android apk 容易被反编译,面对一些强大的逆向工作者,如果安全的保存我们的秘钥,又成为一个问题。秘钥硬编码到 java 代码层,显然是不可取的。参考各方资料后,普遍做法是通过 jni 将秘钥保存在 so 库中。
那么你肯定又会问,放 so 库中 代码被反编译 二次打包 debug 一下不是照样能拿到吗?
是的,但我们可以在加载 so 库的时候验证一下应用签名,签名不一致的话,应用直接退出就 ok 了。
一 、AES 加、解密
高级加密标准(英语:Advanced Encryption Standard,缩写:AES),在密码学中又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用。经过五年的甄选流程,高级加密标准由美国国家标准与技术研究院(NIST)于2001年11月26日发布于FIPS PUB 197,并在2002年5月26日成为有效的标准。2006年,高级加密标准已然成为对称密钥加密中最流行的算法之一。(百度百科)
加密算法原理本文不做讨论,只介绍在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 要加密的内容
* @param key 秘钥
* @return 加密后的 内容
*/
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 加密过的内容
* @param key 秘钥
* @return 解密后的内容
*/
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编程 将应用签名信息 保存在 C++代码中 ,由于在加载 so 库的时候会首先调用JNI_OnLoad方法,所以可再次方法中验证签名信息,不匹配则 返回 JNI_ERR 应用会退出。
1、java 成获取应用签名
/**
* 展示了如何用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++层获取应用签名
将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("内存分配失败");
return JNI_ERR;
}
LOGE("应用中读取到的签名为:%s", sign);
LOGE("native中预置的签名为:%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!现在我们可以 安全的保存我们的 秘钥 了。
4、秘钥写在 C++层
java native 方法:
/**
* so中获取秘钥
* @return
*/
public native String getSecret();
C 对应实现:
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