近日工作上的原因,需要实现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(); } 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是那么的相似,以至于会产生他们的结果肯定会相同的遐想。在使用它之前,我们先来学习学些它的用法。这里有一篇文章,http://blog.csdn.net/gdwzh/article/details/19230 ,对EVP_Encrypt系列函数进行了很详细的解释。如果你不想看那么长的,我们这里取出了几个重要的函数列在下面:
【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使用对应的填充方式,理论上加解密的结果是一样的。知道了这些函数,如何使用呢?上个文章,http://blog.csdn.net/njzhujinhua/article/details/6532896写的很清楚,也很生动。如果你不想跳过去,这里有他的代码(整理后的):
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); }
void decrypt(unsigned char* in, int inl, unsigned char *out, 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_DecryptInit_ex(&ctx, EVP_aes_128_ecb(), NULL, key, iv); int len = 0; int outl = 0; EVP_DecryptUpdate(&ctx, out+len, &outl, in+len, inl); len += outl; EVP_DecryptFinal_ex(&ctx, out+len, &outl); len+=outl; out[len]=0; EVP_CIPHER_CTX_cleanup(&ctx); }
注释少了点,好在大家都熟悉了。inl代表了输入串的长度,这个很重要。
到这里,加密解密都有了。考虑到Java端使用了Base64对加密串又进行了加密,C语言上怎么实现呢?照例先长知识,http://blog.csdn.net/wavemoon/article/details/5800094。这篇博文说明了使用openssl进行base64加解密的操作。我们主要用到两个函数,列举如下
【EVP_EncodeBlock】
原型:int EVP_EncodeBlock(unsigned char *t, const unsigned char *f, int n);
功能:该函数将参数f里面的字符串里面的n个字节的字符串进行BASE64编码并输出到参数t里面。返回数据的字节长度。事实上,在函数EVP_EncodeUpdate和EVP_EncodeFinal里面就调用了该函数完成BASE64编码功能。
参数: t -- 接收编码后的数据缓冲区;f -- 编码前的数据,n -- 编码前的数据长度。
返回值:编码后密文的长度。
【EVP_DecodeBlock】
原型:int EVP_DecodeBlock(unsigned char *t, const unsigned char *f, int n);
功能:该函数将字符串f中的n字节数据进行BASE64解码,并输出到t指向的内存中,输出数据长度为outl。成功返回解码的数据长度,返回返回-1。
参数:t – 接收解码后的数据缓冲区。f -- 解码前的数据, n -- 解码前的数据长度。
返回值:解码后字符的长度。
但这里有一个大坑,不知道你发现了没有。
按照base64的算法,任何长度的串编码后长度均未4的倍数,解码后均未3的倍数。理论上,编码(encode)时,如果输入串不是3的倍数,会在后面补0,以保持3的倍数,反映到encode后的串,就是后面对应补了'=';'='在正常base64编码中不会存在,因此,base64解码时有能力去除尾部的'/0'(虽然上述有些函数没有这么干)。但是直接使用EVP_EncodeBlock(...) / EVP_DecodeBlock(...) 编码、解码,原串经过编码、解码后可能无法还原!---尾部可能会多'/0', 比如:
'1234'
--> EVP_EncodeBlock(...) 变为:'MTIzNA=='
--> EVP_DecodeBlock(...) 变为:'1234/0/0' 尾部多了两个/0
当然这对于以/0结尾的字符串是没影响的,对于二进制数据则直接意味着错误!
EVP_DecodeBlock内部同样调用EVP_DecodeInit + EVP_DecodeUpdate + Evp_DecodeFinal实现,但是并未处理尾部的'='字符,因此结果字符串长度总是为3的倍数。若要获取精确的正确长度,外部需添加额外代码,类似下面这样:
while(input_str[--input_str_len] = '=') output_len--;
return output_len; // 获取实际长度
实际就是原输入串尾部有几个 '=', decode后输出串的长度减几就ok了。
大坑就是这个,讲完上全部的代码。main函数在最下面。
/** build with shell: gcc -Wall aes.c -lcrypto -o aes **/ #include <string.h> #include <stdio.h> #include <stdlib.h> #include <openssl/evp.h> 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); } void decrypt(unsigned char* in, int inl, unsigned char *out, 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_DecryptInit_ex(&ctx, EVP_aes_128_ecb(), NULL, key, iv); int len = 0; int outl = 0; EVP_DecryptUpdate(&ctx, out+len, &outl, in+len, inl); len += outl; EVP_DecryptFinal_ex(&ctx, out+len, &outl); len+=outl; out[len]=0; EVP_CIPHER_CTX_cleanup(&ctx); } int main(int argc, char **argv) { unsigned char content[400]; unsigned char key[] = "HelloWorld"; unsigned char en[400],de[400],base64[400], base64_out[400]; int len; memset(content, 0,400); memset(en, 0, 400); memset(de, 0, 400); memset(base64, 0,400); memset(base64_out, 0, 400); strcpy(content, "HelloHbnfjkwahgruiep"); printf("%d %s\n", strlen((const char*)content), content); encrypt(content,strlen((const char*)content), en, &len, key); int encode_str_size = EVP_EncodeBlock(base64, en, len); printf("%d %s\n", encode_str_size, base64); int length = EVP_DecodeBlock(base64_out, base64, strlen((const char*)base64)); //EVP_DecodeBlock内部同样调用EVP_DecodeInit + EVP_DecodeUpdate + Evp_DecodeFinal实现,但是并未处理尾部的'='字符,因此结果字符串长度总是为3的倍数 while(base64[--encode_str_size] == '=') length--; decrypt(base64_out, length, de, key); printf("%d %s\n", strlen((const char*)de), de); return 0; }