Rust 通过 JNI 提供加密解密接口

0 评论
/
10 阅读
/
9307 字
08 2025-04

目标

在配置 Java 项目时,配制参数通过 AES 来加密,但反编译 class,很容易拿到秘钥, Rust 开发 JNI 提供给 java 接口,是一个相对安全的方法(不是绝对安全)。

实现步骤

要 实现 jniutil 库将支持 armeabi-v7a、arm64-v8a、x86 和 x86_64 四种 ABI。

你需要:

  1. 配置 Rust 目标。
  2. 设置 NDK 工具链。
  3. 分别编译每个目标的 .so 文件。
  4. 将生成的库集成到 Android 项目中。

编译工具

NDK

安装完NDK后,配置环境变量:

set ANDROID_NDK_HOME="C:\Program Files (x86)\Android\AndroidNDK\android-ndk-r23c"

对应 armeabi-v7a

rustup target add armv7-linux-androideabi

对应 arm64-v8a

rustup target add aarch64-linux-android

对应 x86

rustup target add i686-linux-android

对应 x86_64

rustup target add x86_64-linux-android

配置 Cargo.toml

[package]
name = "jniutil"
version = "0.1.0"
edition = "2024"

[dependencies]
jni = "0.19.0"
rust-crypto = "0.2"
clap = "2.33"
hex = "0.4"
base64 = "0.13.0"
# reqwest = { version = "0.11", features = ["json"] }
# tokio = { version = "1.0", features = ["full"] }
# futures = { version = "0.3.*" }
# async-std = { version = "1", features = ["attributes", "tokio1"] }

[lib]
crate-type = ["cdylib"]

注意:

crate-type 只适用于 2024版本的rust,如果是2024之前的版本要用crate_type

代码 /src/lib.rs

// This is the interface to the JVM that we'll
// call the majority of our methods on.
use jni::JNIEnv;

// These objects are what you should use as arguments to your native function.
// They carry extra lifetime information to prevent them escaping this context
// and getting used after being GC'd.
use jni::objects::{JClass, JString};

// This is just a pointer. We'll be returning it from our function.
// We can't return one of the objects with lifetime information because the
// lifetime checker won't let us.
use jni::sys::jstring;

use std::error::Error;
use crypto::aes::KeySize::KeySize128;
use crypto::blockmodes::PkcsPadding;
use crypto::buffer::{RefReadBuffer, RefWriteBuffer};

const DP_CBC_KEY: &str    = "1111111111111111";
const DP_CBC_OFFSET: &str = "1111111111111111";

// 注意包名 com_doopp_common_util_JniUtil
#[no_mangle]
pub extern "system" fn Java_com_doopp_common_util_JniUtil_dpEnc(
    env: JNIEnv,
    // this is the class that owns our
    // static method. Not going to be
    // used, but still needs to have
    // an argument slot
    _class: JClass,
    input: JString,
) -> jstring {
    // First, we have to get the string out of java. Check out the `strings`
    // module for more info on how this works.
    let input: String = env
        .get_string(input)
        .expect("Couldn't get java string!")
        .into();
    // Then we have to create a new java string to return. Again, more info
    // in the `strings` module.
    let enc_str: String = encrypt(DP_CBC_KEY, DP_CBC_OFFSET, input.as_str()).unwrap();
    let output = env
        .new_string(enc_str)
        .expect("Couldn't get encrypt string!");
    // Finally, extract the raw pointer to return.
    output.into_inner()
}

// 注意包名 com_doopp_common_util_JniUtil
#[no_mangle]
pub extern "system" fn Java_com_doopp_common_util_JniUtil_dpDec(
    env: JNIEnv,
    // this is the class that owns our
    // static method. Not going to be
    // used, but still needs to have
    // an argument slot
    _class: JClass,
    input: JString,
) -> jstring {
    // First, we have to get the string out of java. Check out the `strings`
    // module for more info on how this works.
    let input: String = env
        .get_string(input)
        .expect("Couldn't get java string!")
        .into();
    // Then we have to create a new java string to return. Again, more info
    // in the `strings` module.
    let dec_str: String = decrypt(DP_CBC_KEY, DP_CBC_OFFSET, input.as_str()).unwrap();
    let output = env
        .new_string(dec_str)
        .expect("Couldn't get decrypt string!");
    // Finally, extract the raw pointer to return.
    output.into_inner()
}

/// 加密
fn encrypt(key: &str, iv: &str, text: &str) -> Result<String, Box<dyn Error>> {
    let mut encrypt = crypto::aes::cbc_encryptor(
        KeySize128,
        key.as_bytes(),
        iv.as_bytes(),
        PkcsPadding
    );
    let mut read_buffer = RefReadBuffer::new(text.as_bytes());
    let mut result = vec![0; 4096];
    let mut write_buffer = RefWriteBuffer::new(&mut result);
    encrypt.encrypt(&mut read_buffer, &mut write_buffer, true).unwrap();
    let enc_str = base64::encode_config(trim(&result)?, base64::STANDARD);
    Ok(enc_str)
}

/// 解密
fn decrypt(key: &str, iv: &str, input: &str) -> Result<String, Box<dyn Error>> {
    let mut decrypt = crypto::aes::cbc_decryptor(
        KeySize128,
        key.as_bytes(),
        iv.as_bytes(),
        PkcsPadding
    );
    let base_text = base64::decode_config(input, base64::STANDARD)?;
    let mut read_buffer = RefReadBuffer::new(&base_text);
    let mut result = vec![0; input.len()];
    let mut write_buffer = RefWriteBuffer::new(&mut result);
    decrypt.decrypt(&mut read_buffer, &mut write_buffer, true).unwrap();
    let dec_str = String::from_utf8(trim(&result)?)?;
    Ok(dec_str)
}

fn trim(input: &Vec<u8>)  -> Result<Vec<u8>, Box<dyn Error>> {
    let zero : u8 = 0;
    let mut fz_idx = input.len();
    for (idx, val) in input.iter().rev().enumerate() {
        if &zero!=val
        {
            fz_idx = fz_idx - idx;
            break;
        }
    }
    Ok(input[0..fz_idx].to_vec())
}

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {}
}

编译 libjniutil.dylib 并拷贝在 /resources/jni_lib 目录下

构建不同 ABI 的库

在命令行中,使用 cargo build 为每个目标分别编译,生成对应的 .so 文件。例如:

  • armeabi-v7a

    cargo build --target armv7-linux-androideabi --release
    

输出文件位于:target/armv7-linux-androideabi/release/libjniutil.so

  • arm64-v8a

    cargo build --target aarch64-linux-android --release
    

输出文件位于:target/aarch64-linux-android/release/libjniutil.so

  • x86

    cargo build --target i686-linux-android --release
    

输出文件位于:target/i686-linux-android/release/libjniutil.so

  • x86_64

    cargo build --target x86_64-linux-android --release
    

输出文件位于:target/x86_64-linux-android/release/libjniutil.so 使用 –release 参数可以生成优化的发布版本。

在 Android 项目中使用

将生成的 .so 文件放入 Android 项目的 app/src/main/jniLibs 目录,结构如下:

目录结构

app/src/main/jniLibs/
├── armeabi-v7a/
│   └── libjniutil.so
├── arm64-v8a/
│   └── libjniutil.so
├── x86/
│   └── libjniutil.so
└── x86_64/
    └── libjniutil.so

Android应用中调用接口

在 build.gradle 中配置 abiFilters:

android {

    defaultConfig {

        ndk {
            abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
        }
    }
}

Android的JNI调用代码 注意包名和 lib.rs 的一致

package com.example.myapp; // 替换为你的包名

public class JniUtil {
    static {
        // 加载 Rust 编译生成的动态库,文件名是 libjniutil.so,但加载时只写 "jniutil"
        System.loadLibrary("jniutil");
    }

    // 声明本地方法(这些方法需要在 Rust 中实现)
    // 示例 1:无参数,返回字符串
    public native String getHelloMessage();

    // 示例 2:带整数参数,返回计算结果
    public native int addNumbers(int a, int b);

    // 示例 3:带字符串参数,返回处理后的字符串
    public native String processText(String input);

    // 可选:一个简单的 Java 方法用于测试
    public String getWelcomeMessage() {
        return "Welcome from Java!";
    }

    // 主函数用于测试(如果直接运行 Java 类)
    public static void main(String[] args) {
        JniUtil util = new JniUtil();
        
        // 调用本地方法
        System.out.println("Hello Message: " + util.getHelloMessage());
        System.out.println("Add 3 + 5: " + util.addNumbers(3, 5));
        System.out.println("Processed Text: " + util.processText("Hello from Java"));
        
        // 调用 Java 方法
        System.out.println(util.getWelcomeMessage());
    }
}

MainActivity

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        JniUtil util = new JniUtil();
        String message = util.getHelloMessage();
        Log.d("JniUtil", "Message from Rust: " + message);
    }
}
标签:
    暂无数据