数据存储安全之AES 加密

前言

  • 本文介绍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

你可能感兴趣的:(数据存储安全之AES 加密)