众所周知,理论上最安全的加密方式是使用一次一密(OTP)。但是传递与明文长度相等的、完全随机的加密面板这件事情并不具有实践意义,因此就诞生了流密码(Stream Cipher)。流密码将一个密钥作为种子,按照某种伪随机数生成算法生成供OTP使用的加密面板。有了加密面板之后,就可以逐字使用传统的 Vernam 算法 或者 Vigenère 算法进行加密解密。
由于这样进行的加密解密操作没有复杂的计算、并且不需要对数据进行预取分块等复杂操作,因此执行效率很高,非常适合用作流媒体数据的加密。最常见的流加密算法就是大名鼎鼎的RC4。其实 RC4 本质就是一个伪随机数生成器,加密方式其实就是用某个密钥作为种子,通过该生成器生成一个与明文等长的二进制流,再用 Vernam 算法(逐字异或)对明文处理得到密文。
由于是采用 Vernam 算法进行实际的加密,因此判断这类流加密算法的一个很典型的特点,就是对于相同的密钥,将明文和密文进行异或得到的数据是完全相等的(就是那个一次一密的加密板)。
当然,由于 RC4 算法太常见了,业内在使用流密码时常常会选择一些较为小众的伪随机数生成器,比如 Bob Jenkins 提出的 isaac 。而以 isaac 作为伪随机数生成器再结合 Vernam 或者 Vigenère 的加密方法就是 isaac 流加密算法。
由于业务需求,本次我们需要实现 ISAAC64位 的算法。
目前较为常见的实现有以下三个。
ISAAC paper 中的默认实现只是用C实现了其32位和64位的伪随机数生成器的功能,并没有结合实际的加密功能。
Apache Commons Math 的 org.apache.commons.math3.random.ISAACRandom
提供了 ISAAC 作为伪随机数生成器并结合 Vernam 的加密算法实现。但是只有32位的实现,并没有64位的实现。
Rosetta Code 非常人性化的提供了 C、C++、C#、Dephi、Go等近三十种语言的实现,并且同时支持了 Vernam 算法和 Vigenère 算法,可以说是很有心了。但也是只有32位的实现,没有64位的实现。
GNU CoreUtils 提供了 C 实现的 ISAAC 伪随机数算法,同时适配了32位和64位。不过也并没有结合加密功能。
既然我们需要64位的算法,考虑再三后我们选择了 CoreUtils 中的实现。核心文件如下:
https://github.com/coreutils/coreutils/blob/master/gl/lib/rand-isaac.h
https://github.com/coreutils/coreutils/blob/master/gl/lib/rand-isaac.c
由于只依赖了C标准库,因此源文件可以直接拿过来用。
先准备一个有 native 方法的类,可以先把加载动态库的逻辑写进来。(虽然运行时肯定报错,毕竟这个动态连接库目前还没有生成)
package com.mythsman.api.manager;
public class IsaacManager {
static {
try {
System.loadLibrary("isaac");
} catch (Throwable e) {
e.printStackTrace();
}
}
/**
* @param data 待解密的数据
* @param decodeKey 解密key
*/
public native void decrypt(byte[] data, int decodeKey);
}
然后在适当的位置将这个文件编译一把,再生成一个用于静态加载的头文件。
$ javac com/mythsman/api/manager/IsaacManager.java
$ javah com.mythsman.api.manager.IsaacManager
$ ls -la com_mythsman_api_manager_IsaacManager.h
-rw-r--r-- 1 myths staff 547 Jun 18 00:03 com_mythsman_api_manager_IsaacManager.h
生成的头文件大致如下
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_mythsman_api_manager_IsaacManager */
#ifndef _Included_com_mythsman_api_manager_IsaacManager
#define _Included_com_mythsman_api_manager_IsaacManager
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_myths_api_manager_IsaacManager
* Method: decrypt
* Signature: ([BI)V
*/
JNIEXPORT void JNICALL Java_com_mythsman_api_manager_IsaacManager_decrypt
(JNIEnv *, jobject, jbyteArray, jint);
#ifdef __cplusplus
}
#endif
#endif
根据这个头文件,我们就可以利用 rand-isaac 实现一波加密算法了:com_mythsman_api_manager_IsaacManager.c
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <stdio.h>
#include <string.h>
#include "rand-isaac.h"
#include "com_mythsman_api_manager_IsaacManager.h"
/*
* Class: com_mythsman_api_manager_IsaacManager
* Method: decrypt
* Signature: ([BI)V
*/
JNIEXPORT void JNICALL Java_com_mythsman_api_manager_IsaacManager_decrypt
(JNIEnv *jniEnv, jobject obj, jbyteArray data, jint key) {
jbyte *olddata = (*jniEnv)->GetByteArrayElements(jniEnv, data, 0);
struct isaac_state state;
isaac_word result[ISAAC_WORDS];
memset((void *) state.m, 0, ISAAC_WORDS * sizeof(isaac_word));
memset((void *) result, 0, ISAAC_WORDS * sizeof(isaac_word));
*(uint32_t *) state.m = key;
isaac_seed(&state);
int encrypted_len = (*jniEnv)->GetArrayLength( jniEnv, data);
for (int i = 0; i < encrypted_len; i++) {
int mod = i % (ISAAC_WORDS * sizeof(isaac_word));
if (mod == 0) {
memset((void *) result, 0, ISAAC_WORDS * sizeof(isaac_word));
isaac_refill(&state, result);
}
int decrypt_tbl_idx = ISAAC_WORDS * sizeof(isaac_word) - 1 - mod;
*(olddata + i) ^= *((uint8_t *) result + decrypt_tbl_idx);
}
(*jniEnv)->ReleaseByteArrayElements(jniEnv, data, olddata, 0);
}
这里的代码最好在 CLion 里写,涉及到 JNI 的相关用法还是要看看源码的,而且用起来也要小心,搞不好就容易内存泄漏或者 core 。。。
都搞完了,整一个 Cmake 文件。主要需要引入一下 jni 所在的头文件,以及生成动态链接库即可。
project(issac)
cmake_minimum_required(VERSION 3.5.0)
include_directories(
$ENV{JAVA_HOME}/include
$ENV{JAVA_HOME}/include/darwin
$ENV{JAVA_HOME}/include/linux
${PROJECT_SOURCE_DIR}
)
add_library(isaac SHARED rand-isaac.c com_mythsman_api_manager_IsaacManager.c)
为了编译不污染源文件,先生成一个空的 build 文件夹,现在当前的文件结构如下:
$ tree .
.
├── CMakeLists.txt
├── build
├── com_mythsman_api_manager_IsaacManager.c
├── com_mythsman_api_manager_IsaacManager.h
├── rand-isaac.c
└── rand-isaac.h
现在进入 build 文件夹,执行 cmake .. && make
即可:
$ cmake .. && make
-- The C compiler identification is AppleClang 12.0.5.12050022
-- The CXX compiler identification is AppleClang 12.0.5.12050022
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/myths/Downloads/isaac/build
Scanning dependencies of target isaac
[ 33%] Building C object CMakeFiles/isaac.dir/rand-isaac.c.o
[ 66%] Building C object CMakeFiles/isaac.dir/com_mythsman_api_manager_IsaacManager.c.o
[100%] Linking C shared library libisaac.dylib
[100%] Built target isaac
$ ls
CMakeCache.txt CMakeFiles Makefile cmake_install.cmake libisaac.dylib
这样就生成了我们要用的 libisaac.dylib 动态链接库。
启动时,需要带上JVM参数以指定加载动态链接库的路径:-Djava.library.path=/path/to/libisaac.xxx
如果用于单测,则需要在该模块的 gradle 配置文件中加上:
test {
systemProperty "java.library.path", "/path/to/libisaac.xxx"
}
然后就可以直接用啦~
byte[] data = xxxx;
int decodeKe = xxxx;
IsaacManager isaacManager = new IsaacManager();
isaacManager.decrypt(data, decodeKey);
这样 data 中的数据就从密文变成了明文。