普通网友 2026-03-23 19:50 采纳率: 98.3%
浏览 0
已采纳

Android国密SM4加密在JNI层实现时为何出现密文长度不一致?

在Android JNI层实现SM4加密时,密文长度不一致是典型问题:SM4为分组密码(块长128位),**明文长度非16字节整数倍时,若填充方式不统一(如Java层用PKCS#7、JNI层用ZeroPadding或未填充),会导致加解密后长度错乱**;此外,JNI中误将`jbyteArray`长度直接当作原始字节数(忽略JNI GetByteArrayElements可能返回拷贝或偏移)、或未正确处理`env->GetArrayLength()`与实际有效数据长度的差异,也会引发截断或溢出;更隐蔽的是,部分C端SM4库(如GMSSL)默认启用CBC模式+随机IV,若Java层未同步传递/解析IV,或IV被错误纳入密文拼接,会使输出长度浮动16字节。根本原因在于**Java与JNI层在填充策略、IV管理、字节数组边界处理三方面缺乏严格契约对齐**。需统一采用PKCS#7填充、显式IV分离传输,并通过`GetByteArrayRegion`安全读取数据,杜绝指针越界与隐式长度假设。
  • 写回答

1条回答 默认 最新

  • 马迪姐 2026-03-23 19:50
    关注
    ```html

    一、现象层:密文长度抖动——Android SM4加解密中最易复现的“玄学Bug”

    开发中常观察到:同一明文在Java层加密后长度为48字节,而JNI层加密输出却为32或64字节;解密时更出现javax.crypto.BadPaddingException: pad block corrupted。该现象非随机偶发,而是稳定复现于明文长度%16 ≠ 0的边界场景(如15/17/31字节),本质是跨语言密码学契约断裂的第一道裂缝。

    二、机制层:SM4分组密码的刚性约束与JNI内存模型的隐式假设

    • 块长铁律:SM4严格要求输入为128位(16字节)整数倍,任何非对齐明文必须填充——但PKCS#7ZeroPadding生成的填充字节数不同(前者填1–16字节,后者恒填至满块且末尾补0,无法区分真实0与填充0)
    • JNI字节数组陷阱GetByteArrayElements()可能返回原始指针或JVM拷贝副本,且GetArrayLength()仅返回Java数组声明长度,若调用方传入含前导/尾随冗余数据的byte[](如从Base64.decode截取片段),则C层读取范围与Java预期严重偏离

    三、架构层:Java-JNI密码学契约缺失的三大断裂点

    断裂维度Java层典型行为JNI层常见误操作长度影响
    填充策略Cipher.getInstance("SM4/CBC/PKCS5Padding")(实际为PKCS#7)GMSSL库未显式设置SM4_set_padding(SM4_PKCS7),默认ZeroPadding明文15字节→Java输出32B,JNI输出16B(解密失败)
    IV管理随机生成16B IV,与密文拼接为IV||CIPHERTEXTGMSSL启用CBC时自动生成IV,但未禁用并覆盖,导致IV被重复嵌入密文长度浮动+16B,且每次加密结果不可重现
    字节数组边界ByteBuffer.wrap(data, offset, length)精确控制有效载荷直接用env->GetArrayLength(jarr)作为memcpy长度,忽略offset参数读取越界(崩溃)或截断(解密乱码)

    四、验证层:可复现的诊断流程图

    graph TD A[输入明文len=15] --> B{Java层加密} B --> C[PKCS#7填充→填1字节0x01] C --> D[输出密文len=32] D --> E{JNI层加密} E --> F[ZeroPadding→填1字节0x00] F --> G[输出密文len=16] G --> H[Java解密失败:BadPaddingException] H --> I[定位:填充不一致]

    五、解决层:三位一体的加固方案

    1. 填充标准化:Java端显式使用"SM4/CBC/PKCS7Padding"(需Android API ≥ 28或BouncyCastle);JNI端GMSSL调用SM4_set_padding(ctx, SM4_PKCS7)
    2. IV显式管控:Java生成随机IV后,通过独立参数传入JNI(如jobject ivBytes),JNI绝不调用SM4_encrypt_init()自动IV,改用SM4_set_iv()注入
    3. 内存安全读取:弃用GetByteArrayElements,统一采用env->GetByteArrayRegion(jarr, start, len, buf),确保零拷贝且长度受控

    六、工程层:生产就绪的JNI SM4封装模板(关键片段)

    // 安全读取 + PKCS#7填充 + IV分离
    JNIEXPORT jbyteArray JNICALL Java_com_example_Sm4Helper_nativeEncrypt
      (JNIEnv *env, jclass clazz, jbyteArray data, jint offset, jint len, jbyteArray iv) {
        // 1. 精确提取有效数据
        uint8_t *plaintext = (uint8_t*)malloc(len);
        env->GetByteArrayRegion(data, offset, len, (jbyte*)plaintext);
    
        // 2. PKCS#7填充计算
        int padded_len = ((len + 15) / 16) * 16;
        uint8_t *padded = (uint8_t*)malloc(padded_len);
        memcpy(padded, plaintext, len);
        int pad = padded_len - len;
        memset(padded + len, pad, pad); // PKCS#7填充字节值=填充长度
    
        // 3. 注入IV(非自动生成)
        uint8_t iv_buf[16];
        env->GetByteArrayRegion(iv, 0, 16, (jbyte*)iv_buf);
        SM4_set_iv(ctx, iv_buf);
    
        // 4. 加密输出不含IV,由Java层拼接
        uint8_t *ciphertext = (uint8_t*)malloc(padded_len);
        SM4_encrypt(ctx, padded, ciphertext, padded_len);
    
        jbyteArray result = env->NewByteArray(padded_len);
        env->SetByteArrayRegion(result, 0, padded_len, (jbyte*)ciphertext);
    
        free(plaintext); free(padded); free(ciphertext);
        return result;
    }

    七、演进层:超越“能用”的高阶实践

    在金融级合规场景中,需进一步:① 使用SecureRandom替代Random生成IV;② 对JNI层SM4上下文做memset_s清零防内存泄露;③ 在OnLoad中预加载GMSSL引擎并校验FIPS模式;④ 为JNI函数添加__attribute__((no_stack_protector))避免栈保护干扰密码学常量时间特性。这些不是优化,而是将“跨语言密码学契约”从约定升级为强制规范。

    ```
    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论

报告相同问题?

问题事件

  • 已采纳回答 3月24日
  • 创建了问题 3月23日