目标

在配置 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后,配置环境变量:

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

对应 armeabi-v7a

1
rustup target add armv7-linux-androideabi

对应 arm64-v8a

1
rustup target add aarch64-linux-android

对应 x86

1
rustup target add i686-linux-android

对应 x86_64

1
rustup target add x86_64-linux-android

配置 Cargo.toml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
[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

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
// 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
1
cargo build --target armv7-linux-androideabi --release

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

  • arm64-v8a
1
cargo build --target aarch64-linux-android --release

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

  • x86
1
cargo build --target i686-linux-android --release

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

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

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

在 Android 项目中使用

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

目录结构

1
2
3
4
5
6
7
8
9
app/src/main/jniLibs/
├── armeabi-v7a/
│   └── libjniutil.so
├── arm64-v8a/
│   └── libjniutil.so
├── x86/
│   └── libjniutil.so
└── x86_64/
    └── libjniutil.so

Android应用中调用接口

在 build.gradle 中配置 abiFilters:

1
2
3
4
5
6
7
8
9
android {

    defaultConfig {

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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);
    }
}