JNI AES加解密 c++和java互通详解

1. 前言

AES加密标准又称为高级加密标准Rijndael加密法,是美国国家标准技术研究所NIST旨在取代DES的21世纪的加密标准。AES的基本要求是,采用对称分组密码体制,密钥长度可以为128、192或256位,分组长度128位,算法应易在各种硬件和软件上实现。

AES加密数据块和密钥长度可以是128b、192b、256b中的任意一个。AES加密有很多轮的重复和变换。大致步骤如下:

①密钥扩展(Key Expansion);

②初始轮(InitialRound);

③重复轮(Rounds),每一重复轮又包括字节间减法运算(SubBytes)、行移位(ShiftRows)、列混合(MixColurmns)、轮密钥加法运算(AddRoundKey)等操作;

①最终轮(Final Round),最终轮没有列混合操作(MixColumns)。

关于AES加密的原理这里不再赘述,这里放个链接

AES 加密算法的原理详解

这篇文章我们主要讲实现,就拿jni和java来实现互通,下面我们以下面的规格进行编写代码

  • AES/ECB/PKCS5Padding
  • 加密后进行base64操作,防止乱码
  • jni 使用 openssl库
  • java 使用 javax.crypto.Cipher

2. JNI实现

jni采用openssl为基础库,关于openssl的静态库编译,请看我的这篇文章

使用clang编译openssl1.1.1d

Android studio如何配置请直接看我代码

下面开始介绍AES的具体实现

首先定义号方法cipher.h

class cipher {
public:
    static int aes_encrypt(unsigned char *in, unsigned char *key, int ketLen, unsigned char *out);

    static int aes_decrypt(unsigned char *in, unsigned char *key, int keyLen, unsigned char *out);

    static char *Base64encode(const void *pin, int inlen, char *pout, char bNewLine = 1);

    static void Base64decode(const char *pin, void *pout, int *poutLen, char bNewLine = 1);
};

java方法定义

public native String AesEncode(String content, String key);

public native String AesDecode(String content, String key);

那么相应的我们在jni中就需要定义下面两个方法

extern "C"
JNIEXPORT jstring JNICALL
Java_com_dds_openssl_OpenCipher_AesEncode(JNIEnv *env, jobject thiz,
                                          jstring _content,
                                          jstring _key)
                                          
extern "C"
JNIEXPORT jstring JNICALL
Java_com_dds_openssl_OpenCipher_AesDecode(JNIEnv *env, jobject thiz,
                                          jstring _content,
                                          jstring _key)                                          

然后是加密的实现

  1. 第一步:获取key和需要加密的数据

    // 需要加密的内容
    const char *content = env->GetStringUTFChars(_content, JNI_FALSE);
    // 需要加密内容的长度
    int contentLen = env->GetStringLength(_content);
    // 密钥 这个密钥长度必须是16的倍数
    const char *key = env->GetStringUTFChars(_key, JNI_FALSE);
    // 密钥长度
    int keyLen = env->GetStringLength(_key);
    
  2. 获取加密完成后数据的长度

     int encLen = contentLen + AES_BLOCK_SIZE - contentLen % AES_BLOCK_SIZE;
    

    这长度是怎么来的呢,下面这张图是java的填充和长度规则,所以要适配java,我们必须要按照规则来

    • 当加密内容长度小于16位,加密出来的内容为16位
    • 当加密内容长度等于16时,加密出来的内容长16+16=32位
    • 正好就是上面的等式
    算法/模式/填充              16字节加密后数据长度      不满16字节加密后长度
    AES/CBC/NoPadding           16                        不支持
    AES/CBC/PKCS5Padding        32                        16
    AES/CBC/ISO10126Padding     32                        16
    AES/CFB/NoPadding           16                        原始数据长度
    AES/CFB/PKCS5Padding        32                        16
    AES/CFB/ISO10126Padding     32                        16
    AES/ECB/NoPadding           16                        不支持
    AES/ECB/PKCS5Padding        32                        16
    AES/ECB/ISO10126Padding     32                        16
    AES/OFB/NoPadding           16                        原始数据长度
    AES/OFB/PKCS5Padding        32                        16
    AES/OFB/ISO10126Padding     32                        16
    AES/PCBC/NoPadding          16                        不支持
    AES/PCBC/PKCS5Padding       32                        16
    AES/PCBC/ISO10126Padding    32                        16 
    
  3. 进行padding处理

    这里的padding我们使用PKCS5Padding,因为在各个平台,PKCS5Padding的支持最好,

    这个padding是啥呢

    简单点说就是,要加密的内容根据block_size进行分块之后,不足块大小的内容要进行补位,

    补位有的话,上面的表格已经列出来了

    我们用的PKCS5Padding就是在不足的地方用块剩余大小进行补位

    如:

    加密内容长12,我们需要补4个0x04

    加密内容长度为4,我们需要补12个0x12

    加密长度为16,我们需要的补16个0x16

    下面是我们的实现

    /**
     * 
     * @param pSrc 需要padding的内容
     * @param nSrcLen 原文长度
     * @param kCodeLen 加密出内容长度
     */
    void Padding(unsigned char *pSrc, int nSrcLen, int kCodeLen) {
        if (nSrcLen < kCodeLen) {
            unsigned char ucPad = (unsigned char) (kCodeLen - nSrcLen);
            for (int nID = kCodeLen; nID > nSrcLen; --nID) {
                pSrc[nID - 1] = ucPad;
            }
        }
    }
    

    PKCS7Padding类似

  4. 开始AES加密

    int cipher::aes_encrypt(unsigned char *in,
                            unsigned char *key, int keyLen,
                            unsigned char *out) {
        AES_KEY aes;
        if (!in || !key || !out) return 0;
        // 检查key长度
        if (keyLen != 16 && keyLen != 24 && keyLen != 32) {
            LOGE("aes_encrypt key length is invalid");
            return -1;
        }
        // 设置key
        if (AES_set_encrypt_key(key, keyLen << 3, &aes) < 0) { // keyLen*8
            LOGE("aes_encrypt AES_set_encrypt_key error");
            return -2;
        }
        // ecb模式进行加密
        AES_ecb_encrypt(in, out, &aes, AES_ENCRYPT);
        return 1;
    }
    
  5. base64处理

    char *cipher::Base64encode(const void *pin, int inLen, char *pout, char bNewLine) {
        BIO *bMem, *b64;
        BUF_MEM *bptr;
    
        bMem = BIO_new(BIO_s_mem());
        b64 = BIO_new(BIO_f_base64());
        if (!bNewLine) {
            BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); //one line file
        }
        b64 = BIO_push(b64, bMem);
    
        BIO_write(b64, pin, inLen);
        BIO_flush(b64);
        BIO_get_mem_ptr(b64, &bptr);
    
        if (pout == NULL) {
            pout = (char *) malloc(bptr->length + 1);
        }
        memcpy(pout, bptr->data, bptr->length);
        pout[bptr->length] = 0;
    
        BIO_free_all(b64);
        return pout;
    }
    

    bNewLine设置为false,对应java中的defalut类型

  6. 解密流程类似,这里只放代码

    extern "C"
    JNIEXPORT jstring JNICALL
    Java_com_dds_openssl_OpenCipher_AesDecode(JNIEnv *env, jobject thiz,
                                              jstring _content,
                                              jstring _key) {
        LOGD("------------encrypt----------------");
        const char *content = env->GetStringUTFChars(_content, JNI_FALSE);
        int contentLen = env->GetStringLength(_content);
        const char *key = env->GetStringUTFChars(_key, JNI_FALSE);
        int keyLen = env->GetStringLength(_key);
        LOGD("dec content:%s", content);
        LOGD("dec key:%s", key);
        int encLen = 0;
        unsigned char *pEncData = (unsigned char *) malloc(contentLen);
        // base64解密
        cipher::Base64decode(content, pEncData, &encLen, false);
        LOGD("dec base64 hex :%s", char2HexStr(pEncData, encLen).c_str());
        int dataLen = encLen + 1;
        unsigned char *pData = (unsigned char *) malloc(dataLen);
        memset(pData, 0, dataLen);
        // ase 解密
        if (cipher::aes_decrypt(pEncData, (unsigned char *) key, keyLen, pData) <= 0) {
            free(pEncData);
            free(pData);
            env->ReleaseStringUTFChars(_content, content);
            env->ReleaseStringUTFChars(_key, key);
            return NULL;
        }
        pData[dataLen] = 0;
        LOGD("dec aes:%s", pData);
        jstring jStr = env->NewStringUTF((const char *) (pData));
        free(pEncData);
        free(pData);
        env->ReleaseStringUTFChars(_content, content);
        env->ReleaseStringUTFChars(_key, key);
        return jStr;
    }
    
    int cipher::aes_decrypt(unsigned char *in, unsigned char *key, int keyLen, unsigned char *out) {
        AES_KEY aes;
        if (!in || !key || !out) return 0;
        // 检查key长度
        if (keyLen != 16 && keyLen != 24 && keyLen != 32) {
            LOGE("aes_decrypt key length is invalid");
            return -1;
        }
        // 设置key
        if (AES_set_decrypt_key(key, keyLen << 3, &aes) != 0) { // keyLen*8
            LOGE("aes_decrypt AES_set_decrypt_key error");
            return -2;
        }
    
        AES_ecb_encrypt(in, out, &aes, AES_DECRYPT);
        return 1;
    }
    
    

3. JAVA实现

java使用javax.crypto.Cipher实现,我们知道,在不同国家之间的密码算法的交流,出口的密钥长度是受限制的,国际标准AES的加密位数是128位,但是128位的加密强度并不能满足国内各位大佬们,尽管他们连128位的都懒得破解。

好了,具体怎么解决256位的问题,请看下面这片文章,这里不再赘述

https://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html

下面先贴出来一个测试用例

   @Test
    public void testAES2() throws Exception {
        String content = "123456";
        String key = "GAOQXQQ99QPKOMTZE9YF96OLTD8EU6T9";


        String encrypt = AESCrypt.encrypt(key, content,
                false, null,
                "AES/ECB/PKCS5Padding", null);
        Log.d(TAG, "加密出内容:" + encrypt);

        String decrypt = AESCrypt.decrypt(key, content,
                false, null,
                "AES/ECB/PKCS5Padding", null);
        Log.d(TAG, "解密出内容:" + decrypt);

        assertEquals("123456", decrypt.trim());


    }

我们可以看到java中的PKCS5Padding实现起来就比较简单了

  1. 第一步 获取密钥

        private static SecretKeySpec generateKey(final String password, boolean needDigest, String algorithm)
                throws NoSuchAlgorithmException, UnsupportedEncodingException {
            byte[] bytes;
            if (needDigest) {
                final MessageDigest digest = MessageDigest.getInstance(algorithm);
                byte[] bytesPwd = password.getBytes(CHARSET);
                digest.update(bytesPwd, 0, bytesPwd.length);
                bytes = digest.digest();
                log(algorithm + " key ", bytes);
            } else {
                bytes = password.getBytes(CHARSET);
                log("algorithm:" + algorithm + ",key ", bytes);
            }
    
    
            return new SecretKeySpec(bytes, "AES");
        }
    
    
  2. 开始加密

       /**
         * More flexible AES encrypt that doesn't encode
         *
         * @param key     AES key typically 128, 192 or 256 bit
         * @param iv      Initiation Vector
         * @param message in bytes (assumed it's already been decoded)
         * @return Encrypted cipher text (not encoded)
         * @throws GeneralSecurityException if something goes wrong during encryption
         */
        public static byte[] encrypt(final SecretKeySpec key, final byte[] message, final byte[] iv, String aes_mode)
                throws GeneralSecurityException {
            final Cipher cipher = Cipher.getInstance(aes_mode);
            if (aes_mode.contains("CBC")) {
                IvParameterSpec ivSpec = new IvParameterSpec(iv);
                cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
            } else {
                cipher.init(Cipher.ENCRYPT_MODE, key);
            }
            byte[] cipherText = cipher.doFinal(message);
            log("aesEnc", cipherText);
            return cipherText;
        }
    
    

    完整代码如下AESCrypt.java

    public class AESCrypt {
        private static final String TAG = "dds_AESCrypt";
        private static final String CHARSET = "UTF-8";
    
        //AESCrypt-ObjC uses blank IV (not the best security, but the aim here is compatibility)
        private static final byte[] ivBytes = {
                0x00, 0x00, 0x00, 0x00,
                0x00, 0x00, 0x00, 0x00,
                0x00, 0x00, 0x00, 0x00,
                0x00, 0x00, 0x00, 0x00};
    
        //togglable log option (please turn off in live!)
        public static boolean DEBUG_LOG_ENABLED = true;
    
    
        /**
         * Generates hash of the password which is used as key
         *
         * @param password   password
         * @param needDigest true:Generates hash of the password  false:no deal
         * @param algorithm  SHA1/SHA-256/MD5
         * @return SecretKeySpec
         */
        private static SecretKeySpec generateKey(final String password, boolean needDigest, String algorithm)
                throws NoSuchAlgorithmException, UnsupportedEncodingException {
            byte[] bytes;
            if (needDigest) {
                final MessageDigest digest = MessageDigest.getInstance(algorithm);
                byte[] bytesPwd = password.getBytes(CHARSET);
                digest.update(bytesPwd, 0, bytesPwd.length);
                bytes = digest.digest();
                log(algorithm + " key ", bytes);
            } else {
                bytes = password.getBytes(CHARSET);
                log("algorithm:" + algorithm + ",key ", bytes);
            }
    
    
            return new SecretKeySpec(bytes, "AES");
        }
    
    
        /**
         * Encrypt and encode message using 256-bit AES with key generated from password.
         *
         * @param password   used to generated key
         * @param message    the thing you want to encrypt assumed String UTF-8
         * @param needDigest true:Generates hash of the password  false:no deal
         * @param algorithm  SHA1/SHA-256/MD5
         * @return Base64 encoded CipherText
         * @throws GeneralSecurityException if problems occur during encryption
         */
        public static String encrypt(final String password, String message, boolean needDigest,
                                     String algorithm, String aes_mode, final byte[] iv)
                throws GeneralSecurityException {
    
            try {
                final SecretKeySpec key = generateKey(password, needDigest, algorithm);
                if (aes_mode.contains("NoPadding")) {
                    //不足16的倍数补空格
                    if (message.getBytes(CHARSET).length % 16 != 0) {
                        int tem = message.getBytes(CHARSET).length % 16;
                        StringBuilder messageBuilder = new StringBuilder(message);
                        for (int i = 0; i < 16 - tem; i++) {
                            messageBuilder.append(" ");
                        }
                        message = messageBuilder.toString();
                    }
                }
    
                log("input message", message);
    
                byte[] cipherText = encrypt(key, message.getBytes(CHARSET), iv, aes_mode);
    
                String encoded = new String(Base64.encode(cipherText), CHARSET);
    
                log("base64Enc", encoded);
                return encoded;
            } catch (UnsupportedEncodingException e) {
                if (DEBUG_LOG_ENABLED)
                    Log.e(TAG, "UnsupportedEncodingException ", e);
                throw new GeneralSecurityException(e);
            }
        }
    
    
        /**
         * More flexible AES encrypt that doesn't encode
         *
         * @param key     AES key typically 128, 192 or 256 bit
         * @param iv      Initiation Vector
         * @param message in bytes (assumed it's already been decoded)
         * @return Encrypted cipher text (not encoded)
         * @throws GeneralSecurityException if something goes wrong during encryption
         */
        public static byte[] encrypt(final SecretKeySpec key, final byte[] message, final byte[] iv, String aes_mode)
                throws GeneralSecurityException {
            final Cipher cipher = Cipher.getInstance(aes_mode);
            if (aes_mode.contains("CBC")) {
                IvParameterSpec ivSpec = new IvParameterSpec(iv);
                cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
            } else {
                cipher.init(Cipher.ENCRYPT_MODE, key);
            }
            byte[] cipherText = cipher.doFinal(message);
            log("aesEnc", cipherText);
            return cipherText;
        }
    
    
        /**
         * Decrypt and decode ciphertext using 256-bit AES with key generated from password
         *
         * @param password  used to generated key
         * @param base64Enc the encrpyted message encoded with base64
         * @return message in Plain text (String UTF-8)
         * @throws GeneralSecurityException if there's an issue decrypting
         */
        public static String decrypt(final String password, String base64Enc, boolean needDigest,
                                     String algorithm, String aes_mode, final byte[] iv)
                throws GeneralSecurityException {
    
            try {
                final SecretKeySpec key = generateKey(password, needDigest, algorithm);
    
                log("base64Enc", base64Enc);
                byte[] decodedCipherText = Base64.decode(base64Enc.getBytes(CHARSET));
                log("aesEnc", decodedCipherText);
    
                byte[] decryptedBytes = decrypt(key, decodedCipherText, ivBytes, aes_mode);
    
                String message = new String(decryptedBytes, CHARSET);
    
                log("output message", message);
    
    
                return message;
            } catch (UnsupportedEncodingException e) {
                if (DEBUG_LOG_ENABLED)
                    Log.e(TAG, "UnsupportedEncodingException ", e);
    
                throw new GeneralSecurityException(e);
            }
        }
    
    
        /**
         * More flexible AES decrypt that doesn't encode
         *
         * @param key               AES key typically 128, 192 or 256 bit
         * @param iv                Initiation Vector
         * @param decodedCipherText in bytes (assumed it's already been decoded)
         * @return Decrypted message cipher text (not encoded)
         * @throws GeneralSecurityException if something goes wrong during encryption
         */
        public static byte[] decrypt(final SecretKeySpec key, final byte[] decodedCipherText, final byte[] iv, String aes_mode)
                throws GeneralSecurityException {
            final Cipher cipher = Cipher.getInstance(aes_mode);
            if (aes_mode.contains("CBC")) {
                IvParameterSpec ivSpec = new IvParameterSpec(iv);
                cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
            } else {
                cipher.init(Cipher.DECRYPT_MODE, key);
            }
            return cipher.doFinal(decodedCipherText);
        }
    
    
        private static void log(String what, byte[] bytes) {
            if (DEBUG_LOG_ENABLED)
                Log.d(TAG, what + "[" + bytes.length + "] [" + bytesToHex(bytes) + "]");
        }
    
        private static void log(String what, String value) {
            if (DEBUG_LOG_ENABLED)
                Log.d(TAG, what + "[" + value.length() + "] [" + value + "]");
        }
    
        public static String bytesToHex(byte[] bytes) {
            final char[] hexArray = {'0', '1', '2', '3', '4', '5', '6', '7', '8',
                    '9', 'A', 'B', 'C', 'D', 'E', 'F'};
            char[] hexChars = new char[bytes.length * 2];
            int v;
            for (int j = 0; j < bytes.length; j++) {
                v = bytes[j] & 0xFF;
                hexChars[j * 2] = hexArray[v >>> 4];
                hexChars[j * 2 + 1] = hexArray[v & 0x0F];
            }
            return new String(hexChars);
        }
    
    }
    
    

代码收录

jni : https://github.com/ddsbear/AnyNdk

java : https://github.com/ddssingsong/AnyTool

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
众所周知,Java编译后的Jar包和Class文件,可以轻而易举的使用反编译工具(如JD-GUI)进行反编译,拿到源码。为了保护自己发布的Jar包和Class文件,采用的方式大多是混淆方式,这种方式对于Class文件的密是不彻底的,还是能够通过分析得出核心算法。本工具是采用jvmti方式对Class文件进行密,使用C++生成密和解密库,先用密库对Jar包进行密,将密后的Jar包及解密库文件发布出去,执行时候需要JVM引入解密库文件,解密后执行。c++的.dll文件和.so文件的破解难度是很大的,这就能有效的保护软件和代码的知识产权. 使用方法: 1.打开windows命令行(运行=>cmd=>回车),在命令行中 进入 EncryptJar目录 2.执行 java -jar encrypt.jar 3.输入h,然后回车,可以看到帮助菜单 4.输入3,然后按回车键,进入入jar文件功能 5.输入要密的jar文件的路径 6.提示输入秘钥(key)的时候,直接回车,不要输入任何字符(否则后面classhook将不可解密密后的jar包) 7.输入目标路径(密后的jar文件路径,此处要注意:jar文件名要保持相同,将密后的文件保存到不同的目录) 8.将密后的jar包,替换原来的没有密的jar包,与要发布的程序一起进行发布.(一般替换lib目录下对应的jar包即可) 9.密后的jar包运行方法: windows下: 拷贝libClassHook.dll文件到程序的根目录(通常为要执行的jar程序的根目录) 使用以下命令启动程序: java -agentlib:libClassHook -jar xxxxxxxxxxx.jar 则在运行过程中会自动进行解密操作(解密过程是运行过程中用c++的dll进行解密的,可以有效防止破解class文件) 如果执行过程报错,可将程序根目录添到环境变量path中去 Linux下: 拷贝libClassHook.so到程序的根目录(通常为要执行的jar程序的根目录) 使用以下命令启动程序: java -agentlib:ClassHook -jar xxxxxxxxxxx.jar (这里要删除掉lib,linux系统下会自动补全) 则在运行过程中会自动进行解密操作(解密过程是运行过程中用c++的dll进行解密的,可以有效防止破解class文件) 如果执行过程报错,可以在程序根目录下执行以下语句:export LD_LIBRARY_PATH=`pwd`:$LD_LIBRARY_PATH 或将libClassHook.so 拷贝到/usr/lib目录中去。 支持操作系统:密请在windows64位系统并安装了64位jdk环境下进行。 需要解密运行的程序支持LINUX(64位)和windows(64位)安装了JDK1.8以上的系统。 测试程序: (t_lib目录下的jar包为经过密的jar包) java -agentlib:libClassHook -jar test.jar

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ddssingsong

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值