前言
在客户端日常的开发中,尽管有了Https的加持,以及Java层对加解密算法等的支持,我们还是会接到一些需要在Native层实现加解密算法的需求,因为相比于Java,在移动端用c/c++实现的逻辑要更为安全,当然这个安全也是有一定范围的,众所周知,所谓“加密”,是一个加大破解难度的过程,量子力学上来说,这个世界没有秘密
扯远了哈,我们先来一个简易的加解密算法,开箱即用
简易的加解密算法
我们首先介绍的是一个对称加密算法,
之所以叫对称加密,是因为加密和解密使用相同的一个密钥的加密算法,根据加密Key,我们将明文加密成密文,然后又可以将密文用加密Key解密成明文。
相信了解过位运算的同学,这个时候会想到一个运算符,天然的契合了我们上面所讲的对称加密运算。
也就是 异或
异或的定义
什么是异或?知乎上有一个回答很中肯,即只有男人和女人才能生孩子。什么意思呢?在程序的视觉就是:不同的性别 男人+女人 = true ,反之则是false,以我们二进制的 0 和 1 来说就是:
0^1=11^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做了异或转为了二进制,最后可能大概率会得到一个错误的字符,即不能解析成字符,如下面所示:
如果我们不需要明文保存加密结果,纯粹都是二进制,那其实也没太大问题对吧,但如果我们做得是一个程序的核心加解密库,那就不可避免会有这样的需求,比如查看用户的日志这种很基础的行为,我们这个方案就不能实现了,这个问题有解吗?
必须有解!!!
很简单,我们将密文结果再用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算法有了,剩下来的就是改造我们之前的算法了
改造之后的简易加解密算法
改造范围很小哈,总结起来就是两小步
- 提前将
明文和appKey转成base64 - 异或
加密后,将加密后的密文再转成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()));
}
解密
然后是解密,是加密操作的相反操作
- 提前将
密文base64解密转成二进制流 - appkey base64
加密 - 异或解密后,将解密后的结果再用
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()));
}
最后,我们看下控制台的效果
总结
好了,本篇文章就到这了,大家有疑问的,欢迎留言,加解密算法这一块,后面应该陆续还有2~3篇文章,大家有兴趣的可以关注下哈,都会收录到我的这个专栏里