Mythsman


乐极生悲,苦尽甘来。


Isaac64解密算法JNI的封装

前言

众所周知,理论上最安全的加密方式是使用一次一密(OTP)。但是传递与明文长度相等的、完全随机的加密面板这件事情并不具有实践意义,因此就诞生了流密码(Stream Cipher)。流密码将一个密钥作为种子,按照某种伪随机数生成算法生成供OTP使用的加密面板。有了加密面板之后,就可以逐字使用传统的 Vernam 算法 或者 Vigenère 算法进行加密解密。

由于这样进行的加密解密操作没有复杂的计算、并且不需要对数据进行预取分块等复杂操作,因此执行效率很高,非常适合用作流媒体数据的加密。最常见的流加密算法就是大名鼎鼎的RC4。其实 RC4 本质就是一个伪随机数生成器,加密方式其实就是用某个密钥作为种子,通过该生成器生成一个与明文等长的二进制流,再用 Vernam 算法(逐字异或)对明文处理得到密文。

由于是采用 Vernam 算法进行实际的加密,因此判断这类流加密算法的一个很典型的特点,就是对于相同的密钥,将明文和密文进行异或得到的数据是完全相等的(就是那个一次一密的加密板)。

当然,由于 RC4 算法太常见了,业内在使用流密码时常常会选择一些较为小众的伪随机数生成器,比如 Bob Jenkins 提出的 isaac 。而以 isaac 作为伪随机数生成器再结合 Vernam 或者 Vigenère 的加密方法就是 isaac 流加密算法。

由于业务需求,本次我们需要实现 ISAAC64位 的算法。

业界实现

目前较为常见的实现有以下三个。

  1. ISAAC paper 中的伪随机数生成器实现。
  2. Apache Commons Math 中的加密算法实现。
  3. Rosetta Code 提供的加密算法实现。
  4. GNU CoreUtils 中的加密算法实现。

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位。不过也并没有结合加密功能。

JavaApi封装

引入

既然我们需要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标准库,因此源文件可以直接拿过来用。

生成JNI

先准备一个有 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 中的数据就从密文变成了明文。

参考资料

ISAAC Home Page

ISAAC 多语言实现

ISAAC 在GNU中的实现