数据存储安全之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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容