Java AES算法和openssl配對

jopen 11年前發布 | 56K 次閱讀 算法

近日工作上的原因,需要實現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);
}

參數in就是要加密的字符,inl是這個字符的長度;out存放加密后的串,len的值是加密串的長度,key就是你的加密的密鑰。注釋部分還簡單了介紹了下PKCSPadding的小原理。這是加密算法,作者也只給了加密算法。解密算法呢?只能自己來了。上文提到加密和解密還是是對應的,所以:

void decrypt(unsigned char* in, int inl, unsigned char *out, unsigned char *key){
    unsigned char iv[10000];
    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[10000];
    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(en, length, de, key);
    printf("%d %s\n", strlen((const char*)de), de);
    return 0;
}
以上工作花費兩天工作時間,著實不易呀。

來自:http://my.oschina.net/u/267094/blog/174035

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!