Java AES算法和OpenSSL配对

近日工作上的原因,需要实现Java  AES算法和C语言下基于OpenSSL的AES 算法通信。这是个老问题了,网上搜到不少资料,但都不是很详细,没能解决问题。只能自己来了。

先说说AES算法。AES算法的实现有四种,如CBC/ECB/CFB/OFB,这四种Java和C都有实现。AES算法还有末尾的填充(padding),java支持的padding方式有三种NoPadding/PKCS5Padding/,而C却不能显式的设置padding方式,默认的padding就是在末尾加 '\0'。这是一个大坑,多少人都坑在这了。另外,网上很多JAVA AES算法,很多都用SecureRandom,如果你的代码中出现了SecureRandom这个东西,那么你再也不能用C解出来了。

先说Java端的。从良心上说,java的封装比C要强多了。先上代码:

    public static String encrypt(String content, String passwd) {
        try {
            Cipher aesECB = Cipher.getInstance("AES/ECB/PKCS5Padding");
            SecretKeySpec key = new SecretKeySpec(passwd.getBytes(), "AES");
            aesECB.init(Cipher.ENCRYPT_MODE, key);
            byte[] result = aesECB.doFinal(content.getBytes());
            return new BASE64Encoder().encode(result);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (IllegalBlockSizeException e) {
            e.printStackTrace();
        } catch (BadPaddingException e) {
            e.printStackTrace();
        }
        return null;
    }
 
    public String decrypt(String content, String passwd) {
        try {
            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");// 创建密码器
            SecretKeySpec key = new SecretKeySpec(passwd.getBytes(), "AES");
            cipher.init(Cipher.DECRYPT_MODE, key);// 初始化
            byte[] result = new BASE64Decoder().decodeBuffer(content);
            return new String(cipher.doFinal(result)); // 解密
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        <span></span>} catch (NoSuchPaddingException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (IllegalBlockSizeException e) {
            e.printStackTrace();
        } catch (BadPaddingException e) {
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return null;
    }

以上就是两个加密解密函数,默认使用AES算法的ECB,填充方式选择了PKCS5Padding。中间用到了Base64算法将加密后的字串进行再加密,主要是为了可视化读和传递。使用Base64算法要引用sun.misc.BASE64Decoder和sun.misc.BASE64Encoder;

Java就是这么简单,当然它一开始并没有这么简单,我也是从SecureRandom里面跳出来的。

关于openssl库,先看EVP。EVP是OpenSSL自定义的一组高层算法封装函数,它是对具体算法的封装。使得可以在同一类加密算法框架下,通过相同的接口去调用不同的加密算法或者便利地改变具体的加密算法,这样大提高 了代码的可重用性。当你使用EVP的时候你就会发现,它的使用方法和Java是那么的相似,以至于会产生他们的结果肯定会相同的遐想。在使用它之前,我们先来学习学些它的用法。我们这里取出了几个重要的函数列在下面:

【EVP_CIPHER_CTX_init】

该函数初始化一个EVP_CIPHER_CTX结构体,只有初始化后该结构体才能在下面介绍的函数中使用。操作成功返回1,否则返回0。

【EVP_EncryptInit_ex】

该函数采用ENGINE参数impl的算法来设置并初始化加密结构体。其中,参数ctx必须在调用本函数之前已经进行了初始化。参数type通常通过函数类型来提供参数,如EVP_des_cbc函数的形式,即我们上一章中介绍的对称加密算法的类型。如果参数impl为NULL,那么就会使用缺省的实现算法。参数key是用来加密的对称密钥,iv参数是初始化向量(如果需要的话)。在算法中真正使用的密钥长度和初始化密钥长度是根据算法来决定的。在调用该函数进行初始化的时候,除了参数type之外,所有其它参数可以设置为NULL,留到以后调用其它函数的时候再提供,这时候参数type就设置为NULL就可以了。在缺省的加密参数不合适的时候,可以这样处理。操作成功返回1,否则返回0。

【EVP_EncryptUpdate】

该函数执行对数据的加密。该函数加密从参数in输入的长度为inl的数据,并将加密好的数据写入到参数out里面去。可以通过反复调用该函数来处理一个连续的数据块。写入到out的数据数量是由已经加密的数据的对齐关系决定的,理论上来说,从0到(inl+cipher_block_size-1)的任何一个数字都有可能(单位是字节),所以输出的参数out要有足够的空间存储数据。写入到out中的实际数据长度保存在outl参数中。操作成功返回1,否则返回0。

【EVP_EncryptFinal_ex】

该函数处理最后(Final)的一段数据。在函数在padding功能打开的时候(缺省)才有效,这时候,它将剩余的最后的所有数据进行加密处理。该算法使用标志的块padding方式(AKA PKCS padding)。加密后的数据写入到参数out里面,参数out的长度至少应该能够一个加密块。写入的数据长度信息输入到outl参数里面。该函数调用后,表示所有数据都加密完了,不应该再调用EVP_EncryptUpdate函数。如果没有设置padding功能,那么本函数不会加密任何数据,如果还有剩余的数据,那么就会返回错误信息,也就是说,这时候数据总长度不是块长度的整数倍。操作成功返回1,否则返回0。

PKCS padding标准是这样定义的,在被加密的数据后面加上n个值为n的字节,使得加密后的数据长度为加密块长度的整数倍。无论在什么情况下,都是要加上padding的,也就是说,如果被加密的数据已经是块长度的整数倍,那么这时候n就应该等于块长度。比如,如果块长度是9,要加密的数据长度是11,那么5个值为5的字节就应该增加在数据的后面。

【EVP_DecryptInit_ex, EVP_DecryptUpdate和EVP_DecryptFinal_ex】

这三个函数是上面三个函数相应的解密函数。这些函数的参数要求基本上都跟上面相应的加密函数相同。如果padding功能打开了,EVP_DecryptFinal会检测最后一段数据的格式,如果格式不正确,该函数会返回错误代码。此外,如果打开了padding功能,EVP_DecryptUpdate函数的参数out的长度应该至少为(inl+cipher_block_size)字节;但是,如果加密块的长度为1,则其长度为inl字节就足够了。三个函数都是操作成功返回1,否则返回0。

需要注意的是,虽然在padding功能开启的情况下,解密操作提供了错误检测功能,但是该功能并不能检测输入的数据或密钥是否正确,所以即便一个随机的数据块也可能无错的完成该函数的调用。如果padding功能关闭了,那么当解密数据长度是块长度的整数倍时,操作总是返回成功的结果。

前面我们说过,openssl的填充padding方式不能自定义,之后采用默认的在尾端加字符'\0',但是EVP会默认打开Padding,且使用的Padding方式为PKCS padding,所以只要java使用对应的填充方式,理论上加解密的结果是一样的。知道了这些函数,如何使用呢?上个代码(整理后的):

void encrypt(unsigned char* in, int inl, unsigned char *out, int* len, unsigned char * key){
    unsigned char iv[8];
    EVP_CIPHER_CTX ctx;
    //此init做的仅是将ctx内存 memset为0 
    EVP_CIPHER_CTX_init(&ctx);
 
    //cipher  = EVP_aes_128_ecb(); 
    //原型为int EVP_EncryptInit_ex(EVP_CIPHER_CTX *ctx,const EVP_CIPHER *cipher, ENGINE *impl, const unsigned char *key, const unsigned char *iv) 
    //另外对于ecb电子密码本模式来说,各分组独立加解密,前后没有关系,也用不着iv 
    EVP_EncryptInit_ex(&ctx, EVP_aes_128_ecb(), NULL, key, iv); 
 
    *len = 0;
    int outl = 0;
    //这个EVP_EncryptUpdate的实现实际就是将in按照inl的长度去加密,实现会取得该cipher的块大小(对aes_128来说是16字节)并将block-size的整数倍去加密。
    //如果输入为50字节,则此处仅加密48字节,outl也为48字节。输入in中的最后两字节拷贝到ctx->buf缓存起来。 
    //对于inl为block_size整数倍的情形,且ctx->buf并没有以前遗留的数据时则直接加解密操作,省去很多后续工作。 
    EVP_EncryptUpdate(&ctx, out+*len, &outl, in+*len, inl);
    *len+=outl;
    //余下最后n字节。此处进行处理。
    //如果不支持pading,且还有数据的话就出错,否则,将block_size-待处理字节数个数个字节设置为此个数的值,如block_size=16,数据长度为4,则将后面的12字节设置为16-4=12,补齐为一个分组后加密
    //对于前面为整分组时,如输入数据为16字节,最后再调用此Final时,不过是对16个0进行加密,此密文不用即可,也根本用不着调一下这Final。
    int test = inl>>4;
    if(inl != test<<4){
        EVP_EncryptFinal_ex(&ctx,out+*len,&outl); 
        *len+=outl;
    }
    EVP_CIPHER_CTX_cleanup(&ctx);
}

相关推荐