Java AES算法和openssl配對

近日工做上的緣由,須要實現Java  AES算法和C語言下基於openssl的AES 算法通訊。這是個老問題了,網上搜到很多資料,但都不是很詳細,沒能解決問題。只能本身來了。 java

先說說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要強多了。先上代碼: shell


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系列函數進行了很詳細的解釋。若是你不想看那麼長的,咱們這裏取出了幾個重要的函數列在下面: dom

    【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);
}

參數in就是要加密的字符,inl是這個字符的長度;out存放加密後的串,len的值是加密串的長度,key就是你的加密的密鑰。註釋部分還簡單了介紹了下PKCSPadding的小原理。這是加密算法,做者也只給了加密算法。解密算法呢?只能本身來了。上文提到加密和解密仍是是對應的,因此: ui

 

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;
}
以上工做花費兩天工做時間,着實不易呀。
相關文章
相關標籤/搜索