Android重学系列(四):NDK实现一个加解密算法的骚操作

2,359 阅读5分钟

前言

在客户端日常的开发中,尽管有了Https的加持,以及Java层对加解密算法等的支持,我们还是会接到一些需要在Native层实现加解密算法的需求,因为相比于Java,在移动端用c/c++实现的逻辑要更为安全,当然这个安全也是有一定范围的,众所周知,所谓“加密”,是一个加大破解难度的过程,量子力学上来说,这个世界没有秘密

扯远了哈,我们先来一个简易的加解密算法,开箱即用

简易的加解密算法

我们首先介绍的是一个对称加密算法,

image.png

之所以叫对称加密,是因为加密和解密使用相同的一个密钥的加密算法,根据加密Key,我们将明文加密成密文,然后又可以将密文用加密Key解密成明文。

相信了解过位运算的同学,这个时候会想到一个运算符,天然的契合了我们上面所讲的对称加密运算。

也就是 异或

异或的定义

什么是异或?知乎上有一个回答很中肯,即只有男人和女人才能生孩子。什么意思呢?在程序的视觉就是:不同的性别 男人+女人 = true ,反之则是false,以我们二进制的 0 和 1 来说就是:

  1. 0^1=1
  2. 1^1=0

这里我们用^表示异或

结合起来就是:0^1^1=0

我们有时忘了概念的话,可以在控制台用Python校验一下(开发小Tips哈,例如位运算等一些操作我们也可以用下面这种方式校验)

$: python3

Python 3.9.10 (main, Jan 15 2022, 11:48:00) 
[Clang 13.0.0 (clang-1300.0.29.3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.

>>> 0^1
1
>>> 1^1
0
>>> 0^1^1
0

用表达式来表示就是: A^B^B=A

ndk实现一个简易对称加解密

好了,废话少说,第一步,我们先创建我们在Java层的Api入口

public class Crypto {

    static {
            System.loadLibrary("crypto-Engine");
    }

    public static native String encode(Object context, String postStr);

    public static native String decode(Object context, String postStr);
}

我们这里在CMake里将库的名称定为了crypto-Engine,并且采用常规的方式进行注册Jni方法,接下来就是我们在native层去编写对应的头文件和实现文件

头文件

//--->>>>Crypto.h<<<<---
#ifndef ANDROIDCRYPTO_MAIN_H
#define ANDROIDCRYPTO_MAIN_H
using namespace std;
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT jstring JNICALL
Java_com_army_cryptolib_Crypto_encode(JNIEnv *, jclass, jobject, jstring);

JNIEXPORT jstring JNICALL
Java_com_army_cryptolib_Crypto_decode(JNIEnv *, jclass, jobject, jstring);


#ifdef __cplusplus
}
#endif
#endif

然后我们在实现文件里先写死appKey

//--->>>>Crypto.cpp<<<<---
static string appKey = "fdfdfdjjwoejojw";
static int keyLen = appKey.length();

真实环境中我们不建议这样做哈,虽说是写死在so库里,但是只要我们对so用IDA略加反编译,就能获取到这种明文字符串了。此篇文章不再引申appKey的保护方案,后续文章会针对这一点进行补充

然后我们分别实现一下加密和解密

加密

有了前面的铺垫,其实加密就很简单了,就是将我们需要加密的字符进行1次循环,然后每个字符去跟我们提前定义好的appKey的字符(这里也会挨个去匹配)去进行异或操作

jstring
Java_com_army_cryptolib_Crypto_encode(JNIEnv *env, jclass type, jobject context, jstring postStr_) {

    if (!postStr_) return nullptr;
    const char *encode_str = env->GetStringUTFChars(postStr_, 0);
    string result;
    size_t len = strlen(encode_str);
    for (int i = 0; i < len; i++) {
        char bstr_asc = encode_str[i];
        char bkey_asc = appKey.at(i % keyLen);
        char n = (char) (bstr_asc ^ bkey_asc);
        result.push_back(n);
    }
    LOGD("encode:%s>%s", encode_str, result.c_str());
    return env->NewStringUTF(reinterpret_cast<const char *>(result.c_str()));
}

解密

这里特意写了两个方法,在此时其实是多余的哈,我们加密和解密完全可以只调上面的一个加密方法就行了,这里只是为了之后的知识点做铺垫

jstring
Java_com_army_cryptolib_Crypto_decode(JNIEnv *env, jclass type, jobject context, jstring postStr_) {
    const char *postStr = env->GetStringUTFChars(postStr_, 0);
    if (!postStr) return nullptr;
    const char *decode_str = env->GetStringUTFChars(postStr_, 0);

    string result;
    size_t len = strlen(decode_str);
    for (int i = 0; i < len; i++) {
        int dStr_asc = decode_str[i];
        int bKey_asc = appKey.at(i % keyLen);
        char c = (char) (dStr_asc ^ bKey_asc);
        result.push_back(c);
    }
    LOGD("decode:%s>%s", decode_str, result.c_str());
    return env->NewStringUTF(reinterpret_cast<const char *>(result.c_str()));
}

log

上面有使用到log方法,也贴一下代码

#ifndef ANDROIDCRYPTO_LOG_HPP
#define ANDROIDCRYPTO_LOG_HPP
#include <android/log.h>

#define TAG "Crypto"
static bool debug = true;

void setDebug(bool isDebug) {
    debug = isDebug;
}

#define LOGV(...) if (debug) __android_log_print(ANDROID_LOG_VERBOSE,TAG ,__VA_ARGS__)
#define LOGD(...) if (debug) __android_log_print(ANDROID_LOG_DEBUG,TAG ,__VA_ARGS__)
#define LOGI(...) if (debug) __android_log_print(ANDROID_LOG_INFO,TAG ,__VA_ARGS__)
#define LOGW(...) if (debug) __android_log_print(ANDROID_LOG_WARN,TAG ,__VA_ARGS__)
#define LOGE(...) if (debug) __android_log_print(ANDROID_LOG_ERROR,TAG ,__VA_ARGS__)

#endif //ANDROIDCRYPTO_LOG_HPP

思考

到这里,我们思考一下,像我们前面这样做,可行吗?难道不会产生问题吗?

答案肯定是有的,如我们将正常的字符去和key做了异或转为了二进制,最后可能大概率会得到一个错误的字符,即不能解析成字符,如下面所示:

image.png

如果我们不需要明文保存加密结果,纯粹都是二进制,那其实也没太大问题对吧,但如果我们做得是一个程序的核心加解密库,那就不可避免会有这样的需求,比如查看用户的日志这种很基础的行为,我们这个方案就不能实现了,这个问题有解吗?

必须有解!!!

很简单,我们将密文结果再用base64算法再转一遍不就行了。

什么是Base64

维基百科的解释是: Base64 是一种基于 64 个可打印字符来表示二进制数据的表示方法。由于 2^6=64,所以每 6 个比特为一个单元,对应某个可打印字符。3 个字节有 24 个比特,对应于 4 个 Base64 单元,即 3 个字节可由 4 个可打印字符来表示

相信有的同学可能不能很快的理解这个释义,我们记住一点就是,base64是将二进制转成我们可以认识的字符的一种算法就行了,至于原理,可以参考这篇文章一份简明的 Base64 原理解析

Base64算法实现

直接开撸代码,下面是头文件和实现文件,同样定义一个加密,一个解密Api

#ifndef __Crypt__Base64__
#define __Crypt__Base64__

#include <string>

using namespace std;

string base64_encode(unsigned char const* , unsigned int len);
string base64_decode(string const& s);

#endif /* defined(__Crypt__Base64__) */

接下来就是实现了

#include "Base64.h"

static const std::string base64_chars =
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
                "abcdefghijklmnopqrstuvwxyz"
                "0123456789+/";


static inline bool is_base64(unsigned char c) {
    return (isalnum(c) || (c == '+') || (c == '/'));
}

std::string base64_encode(unsigned char const *bytes_to_encode, unsigned int in_len) {
    std::string ret;
    int i = 0;
    int j = 0;
    unsigned char char_array_3[3];
    unsigned char char_array_4[4];

    while (in_len--) {
        char_array_3[i++] = *(bytes_to_encode++);
        if (i == 3) {
            char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
            char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
            char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
            char_array_4[3] = char_array_3[2] & 0x3f;

            for (i = 0; (i < 4); i++)
                ret += base64_chars[char_array_4[i]];
            i = 0;
        }
    }

    if (i) {
        for (j = i; j < 3; j++)
            char_array_3[j] = '\0';

        char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
        char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
        char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
        char_array_4[3] = char_array_3[2] & 0x3f;

        for (j = 0; (j < i + 1); j++)
            ret += base64_chars[char_array_4[j]];

        while ((i++ < 3))
            ret += '=';

    }

    return ret;

}

std::string base64_decode(std::string const &encoded_string) {
    size_t in_len = encoded_string.size();
    size_t i = 0;
    size_t j = 0;
    int in_ = 0;
    unsigned char char_array_4[4], char_array_3[3];
    std::string ret;

    while (in_len-- && (encoded_string[in_] != '=') && is_base64(encoded_string[in_])) {
        char_array_4[i++] = encoded_string[in_];
        in_++;
        if (i == 4) {
            for (i = 0; i < 4; i++)
                char_array_4[i] = static_cast<unsigned char>(base64_chars.find(char_array_4[i]));

            char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
            char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
            char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];

            for (i = 0; (i < 3); i++)
                ret += char_array_3[i];
            i = 0;
        }
    }

    if (i) {
        for (j = i; j < 4; j++)
            char_array_4[j] = 0;

        for (j = 0; j < 4; j++)
            char_array_4[j] = static_cast<unsigned char>(base64_chars.find(char_array_4[j]));

        char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
        char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
        char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];

        for (j = 0; (j < i - 1); j++) ret += char_array_3[j];
    }

    return ret;
}

base64算法网络上一大堆哈,这一段是从我的笔记里摘抄出来的,早就忘记出处,如果实现有性能问题,轻喷。后面有时间我再重写一遍。

其实整体很简单,有时间的同学可以看看代码,应该就能理解base64的意义了

base64算法有了,剩下来的就是改造我们之前的算法了

改造之后的简易加解密算法

改造范围很小哈,总结起来就是两小步

  1. 提前将明文appKey转成base64
  2. 异或加密后,将加密后的密文再转成base64

加密

jstring
Java_com_army_cryptolib_Crypto_encode(JNIEnv *env, jclass type, jobject context, jstring postStr_) {

    if (!postStr_) return nullptr;
    const char *encode_str = env->GetStringUTFChars(postStr_, 0);
    string result;

    string bstr = base64_encode((unsigned char *) encode_str,
                                (int) strlen(encode_str));
    string bkey = base64_encode((unsigned char *) appKey.c_str(),
                                appKey.length());
    size_t len = bstr.length();
    for (int i = 0; i < len; i++) {
        char bstr_asc = bstr.at(i);
        char bkey_asc = bkey.at(i % keyLen);
        char n = (char) (bstr_asc ^ bkey_asc);
        result.push_back(n);
    }
    result = base64_encode((unsigned char *) result.c_str(),
                           (int) result.length());
    LOGD("encode:%s>%s", encode_str, result.c_str());
    return env->NewStringUTF(reinterpret_cast<const char *>(result.c_str()));
}

解密

然后是解密,是加密操作的相反操作

  1. 提前将密文 base64解密转成二进制流
  2. appkey base64加密
  3. 异或解密后,将解密后的结果再用base64解密
jstring
Java_com_army_cryptolib_Crypto_decode(JNIEnv *env, jclass type, jobject context, jstring postStr_) {
    const char *postStr = env->GetStringUTFChars(postStr_, 0);
    if (!postStr) return nullptr;
    const char *decode_str = env->GetStringUTFChars(postStr_, 0);

    string result = "";
    string bstr = base64_decode(decode_str);
    string bkey = base64_encode((unsigned char *) appKey.c_str(),
                                appKey.length());

    size_t len = bstr.length();
    for (int i = 0; i < len; i++) {
        char bstr_asc = bstr.at(i);
        char bkey_asc = bkey.at(i % keyLen);
        char n = (char) (bstr_asc ^ bkey_asc);
        result.push_back(n);
    }
    result = base64_decode(result);
    LOGD("decode:%s>%s", decode_str, result.c_str());
    return env->NewStringUTF(reinterpret_cast<const char *>(result.c_str()));
}

最后,我们看下控制台的效果

image.png

总结

好了,本篇文章就到这了,大家有疑问的,欢迎留言,加解密算法这一块,后面应该陆续还有2~3篇文章,大家有兴趣的可以关注下哈,都会收录到我的这个专栏里