前言
需求:需要在NDK层对一个Java层的字符串进行RSA加密,然后对加密的结果进行Base64返回到Java层
方案:选择使用OpenSSL来实现。
编译libssl.a和libcrypto.a静态库
在github上找到了一个项目,可以直接将OpenSSL编译成Android可以使用的,项目地址为
但是这个项目有点小问题,部分编译脚本需要做点改动,改动后的项目见
主要做了3个改动:
- 将最低版本支持从Android 21改到了Android 14
- 修复一个armeabi-v7a无法编译出来的问题
- 升级了openssl的版本到openssl-1.1.0e
之后将项目clone下来,进入到tools目录,执行build-openssl4android.sh编译脚本
| 1 | ./build-openssl4android.sh android-armeabi armeabi-v7a | 
这里只编译了armeabi-va7和armeabi架构CPU的so,如果有需要,请自行更改命令参数编译X86等架构的so。
经过很长时间的编译。。。大概要10来分钟吧。。。在根目录下的output会产生一个android目录,里面有openssl-armeabi和openssl-armeabi-v7a两个文件夹,包含了openssl的头文件以及编译好的.a静态库
实现JNI函数
编译好后.a静态库,就可以创建jni项目了
进入jni项目根目录,创建Application.mk文件
| 1 | APP_ABI := armeabi armeabi-v7a | 
进入jni项目根目录,创建Android.mk文件
| 1 | LOCAL_PATH := $(call my-dir) | 
进入jni项目根目录,拷贝编译好的openssl文件
接着将第一步编译好的静态库文件进行拷贝,将output目录下android整个目录进行拷贝,拷贝到jni项目根目录下,拷贝完成后将android目录重命名为openssl
进入jni项目根目录,创建native.cpp,搭建基础的结构
| 1 | 
 | 
声明java层函数
在Java层创建com/fucknmb/Test类,声明一个native函数
| 1 | package com.fucknmb; | 
实现native_rsa函数
native_rsa函数有两个参数,一个是base64之后的公钥(不含头部和尾部,以及没换行),第二个是待加密的明文内容,该函数的返回值是加密后的密文进行base64。
对于第一个参数,我们需要将其转为公钥文件字符串,追加头部和尾部,其实现如下:
| 1 | /** | 
openssl rsa加密后,我们需要对密文进行Base64,openssl同样提供了Base64算法,实现如下
| 1 | /** | 
这个函数有一点需要注意的就是这一行
| 1 | std::string result(bufferPtr->data, bufferPtr->length); | 
第二个参数表示长度,不能少,否则base64后的字符串长度会出现异常,导致decode的时候末尾会出现一大堆的乱码,而网上大多数的代码,是缺失这一个参数的。
接下来就是rsa的实现了
| 1 | /** | 
同样这个函数也有几个地方需要注意:
第一点:
| 1 | static std::string result((char *) to, status); | 
第二个参数表示密文长度,一般来说,这个值会是128,如果第二个值不传,会导致加密后的密文经过string的构造函数后,丢失一部分数据,导致数据的不正确
第二点:
| 1 | rsa_size = rsa_size - RSA_PKCS1_PADDING_SIZE; | 
对于RSA_PKCS1_PADDING_SIZE,最大加密长度为需要减去11
2017.7.17修改,第二点经过试验,废弃!
第三点:
| 1 | //明文长度 | 
RSA_public_encrypt函数的第一个参数传的是明文长度,而不是最大加密长度rsa_size,网上的所有代码这个参数都是传错的,传了rsa_size,而实际上这个参数的参数名是flen,表示from字符串的length。如果这个参数传了最大加密长度,将直接导致java层无法正确解密JNI层加密后的数据。
最后不要忘记加头文件的引用
| 1 | 
 | 
需要的函数都有了,实现以下native_rsa函数,简单组装一下以上函数即可
| 1 | jstring native_rsa(JNIEnv *env, jobject thiz, jstring base64PublicKey, jstring content) { | 
私钥解密
如果你还需要用的私钥解密部分,可以继续实现base64的decode函数,以及rsa的私钥串生成函数,rsa的解密函数
base64 decode函数的实现如下:
| 1 | /** | 
rsa的私钥串生成函数的试下如下:
| 1 | /** | 
私钥解密函数的实现如下:
| 1 | /** | 
如果你要解密公钥加密后的密文,只需要这样调用即可返回明文
| 1 | //公钥串和私钥串 | 
最后注意一下base64PublicKey和base64PrivateKey,这两个字符串是不包含换行的,就是私钥和公钥的encoded之后的字节数组base64后的值,因此需要自己调用generatePublicKey和generatePrivateKey追加头和尾。
RSA公钥和私钥的生成
生成私钥
| 1 | openssl genrsa -out rsa_private_key.pem 1024 | 
这条命令让openssl随机生成了一份私钥,加密长度是1024位。加密长度是指理论上最大允许”被加密的信息“长度的限制,也就是明文的长度限制。随着这个参数的增大(比方说2048),允许的明文长度也会增加,但同时也会造成计算复杂度的极速增长。一般推荐的长度就是1024位(128字节,之前的代码的最大加密长度128就是这么来的)。
生成公钥
| 1 | openssl rsa -in rsa_private_key.pem -out rsa_public_key.pem -pubout | 
密钥文件最终将数据通过Base64编码进行存储。可以看到上述生成的密钥文件内容每一行的长度都很规律。这是由于RFC2045中规定:The encoded output stream must be represented in lines of no more than 76 characters each。也就是说Base64编码的数据每行最多不超过76字符,对于超长数据需要按行分割。
上面的generatePublicKey和generatePrivateKey函数我们是按64位一行进行分割的,如果你有需要,可以将值修改为76。
第一步生成私钥文件不能直接使用,需要进行PKCS#8编码:
| 1 | openssl pkcs8 -topk8 -in rsa_private_key.pem -out pkcs8_rsa_private_key.pem -nocrypt | 
第二步和第三步生成的公钥和私钥就可以用了,这里有个问题需要注意,如果你的公钥和私钥是类似下面这种格式的
| 1 | -----BEGIN PUBLIC KEY----- | 
那么,你无需调用generatePublicKey或者generatePrivateKey函数,此时已经是需要的公钥串和私钥串,但是如果你的公钥和私钥没有头部和尾部,并且不是换行的,就需要调用一下进行转换,因为我这边Java层传入的是后者,所以需要调用generatePublicKey或者generatePrivateKey进行转换。
Java层调用公钥加密函数部分
| 1 | String base64PublicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDP0tzYxBF5IGfNvuIHzAqvza/ZxfH8aEiPFA4nY/W3js+cG3JUU86Jkc7jUG9XfGdW6SJ38ANs5tyWqYkJyoUErB2PjQQQDmHhbgpBUSeOdwGr/LPtrTrotrNXwpRY9eodkcbcMlbT0gvdnohRSISCjJ2KmFcBMkeO9R2DWe6oIwIDAQAB"; | 
