2015年10月14日 星期三

AES 加密心得

前言:要跟java那邊用AES加密後的資料對接

在網路上找到許多不同語言的實作方式,但加密出來的結果都不一樣

原來是 AES encryption 有以下幾種mode

● ECB should not be used if encrypting more than one block of data with the same key.
當使用相同key加密一個block以上的資料時,ECB不應該被使用
● CBC, OFB and CFB are similar, however OFB/CFB is better because you only need encryption and not decryption, which can save code space.
CBC, OFB 和CFB類似。OFB/CFB比較好,因為你只需要加密不需要解密,以減少code的量
● CTR is used if you want good parallelization (ie. speed), instead of CBC/OFB/CFB.
當你想要好的平行處理(如:速度),使用CTR。而不是CBC/OFB/CFB。
The most important caveat with CTR mode is that you never, ever re-use the same counter value with the same key. If you do so, you have effectively given away your plaintext. ( http://stackoverflow.com/questions/4951468/ctr-mode-use-of-initial-vectoriv CTR mode use of Initial Vector(IV) )
最重要的是,你不要重複使用相同的key(IV,隨機產生),如果你這樣做,你已有效地給了你的明文
● XTS mode is the most common if you are encoding a random accessible data (like a hard disk or RAM).
XTS用在硬碟和RAM上
● OCB is by far the best mode, as it allows encryption and authentication in a single pass. However there are patents on it in USA.
OCB最好,因為他允許加密和認證在單通道。然而美國擁有其專利。( 所以意味著你在網路上找不到實作他的code )

你必須每次都用獨特的IV去加密,如果你不能保證他的隨機性,請用只需要隨機數(非IV)的OCB。固定的IV使得人們能不斷的猜測下一個,隨機數能避免這個風險

初始向量 Initialization vector (IV) 可被公開
http://ijecorp.blogspot.com/2013/08/python-m2crypto-aes-encrypt-decrypt.html
IV 本身並不需要保護,它是可以被公開的。而IV的最大長度必須是 16 bytes,而且產生IV的方式必須是無法預測的,也就是隨機產生即可。
http://stackoverflow.com/questions/8804574/aes-encryption-how-to-transport-iv
There is no security hole by sending the IV in clear text - this is similar to storing the salt for a hash in clear: As long as the attacker has no controll over the IV/salt, and as long as it is random, there is nor problem.
用明文傳送IV沒有安全的漏洞。這就像你做hash加了salt一樣,只要攻擊者無法掌握IV(salt)並且他是隨機的,就不會有問題。

使用php做AES CBC 128 pkcs5padding加密
$value = "张根";
$key = "Bar12345Bar12345"; //16 Character Key

echo strToHex('张根'); // hex: e5bca0e6a0b9

$encrypted = getEncrypt($value, $key);
echo "\n\$encrypted:".$encrypted;
echo "\n\getDecrypt(\$encrypted, \$key):".getDecrypt($encrypted, $key);

function pkcs5_pad ($text, $blocksize) { // https://github.com/stevenholder/PHP-Java-AES-Encrypt/blob/master/security.php
 $pad = $blocksize - (strlen($text) % $blocksize); 
 return $text . str_repeat(chr($pad), $pad); 
} 

function getEncrypt($sStr, $sKey) { // http://stackoverflow.com/questions/4537099/problem-with-aes-256-between-java-and-php
 global $iv;
 $sStr = pkcs5_pad($sStr, 16); // 這個16是 mcrypt_get_block_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC) 的結果
 echo "\n\$sStr:".$sStr;  // 測試pkcs5 padding的結果
 // 產生$iv,如果用class寫,可以避免全域變數
 // $iv = mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_ECB), MCRYPT_RAND);
 // echo "\n\$iv:".$iv; // 這是隨機產生的內容
  return base64_encode( // 用bin2hex()亦可,但解密時要用hex2bin()
    mcrypt_encrypt(
        MCRYPT_RIJNDAEL_128, 
        $sKey,
        $sStr,
        MCRYPT_MODE_CBC,
        "ThisIsASecretKet" // $iv,測試時寫死
    )
  );
}

function getDecrypt($sStr, $sKey) {
 global $iv; // 要與 getEncrypt()產生的$iv一致,才能解出來
  return mcrypt_decrypt(
    MCRYPT_RIJNDAEL_128, 
    $sKey, 
    base64_decode($sStr), // 加密時用bin2hex(),則解密時要用hex2bin()
    MCRYPT_MODE_CBC,
    "ThisIsASecretKet"
  );
}

function strToHex($string) // http://ditio.net/2008/11/04/php-string-to-hex-and-hex-to-string-functions/
{
    $hex='';
    for ($i=0; $i < strlen($string); $i++)
    {
        $hex .= dechex(ord($string[$i]));
    }
    return $hex;
}

結果:
e5bca0e6a0b9 //這邊是utf8中文字轉hex的結果,如果其他地方字串轉hex不是這個代表他們的字串原本編碼不是utf-8
$sStr:张根 // pkcs5 padding的結果
$encrypted:LE/jvtjPWJk7qJc49Xl3eQ== // aes加密後 base64_decode()的結果
\getDecrypt($encrypted, $key):张根 // aes解密結果

說明:
因為java那邊只能用 128bit,所以只能選 MCRYPT_RIJNDAEL_128
$iv = Initial Vector(IV) 初始向量
在 https://github.com/stevenholder/PHP-Java-AES-Encrypt/blob/master/security.php 範例中,我們可以看到他加密不是用 mcrypt_encrypt 而是
public static function encrypt($input, $key) {
    $size = mcrypt_get_block_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_ECB);
    $input = Security::pkcs5_pad($input, $size);
    $td = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_ECB, '');
    $iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($td), MCRYPT_RAND);
    mcrypt_generic_init($td, $key, $iv);
    $data = mcrypt_generic($td, $input);
    mcrypt_generic_deinit($td);
    mcrypt_module_close($td);
    $data = base64_encode($data);
    return $data;
}
mcrypt_generic() => 低階API ,更有彈性
mcrypt_encrypt() => 高階工具( higher-level utility )
參考資料:
http://stackoverflow.com/questions/2773535/mcrypt-generic-vs-mcrypt-encrypt mcrypt_generic vs mcrypt_encrypt

http://php.net/manual/en/function.mcrypt-encrypt.php
string mcrypt_encrypt ( string $cipher , string $key , string $data , string $mode [, string $iv ] )
mode的格式是 MCRYPT_MODE_modename ,modename可使用"ecb", "cbc", "cfb", "ofb", "nofb" or "stream"
如: MCRYPT_MODE_ECB

因為mcrypt_encrypt() 出來的結果打印是二進位亂碼,所以都用 bin2hex()或base64_encode()去轉換一次

報錯:
Warning: mcrypt_encrypt(): Key of size 15 not supported by this algorithm. Only keys of sizes 16, 24 or 32 supported
如果你出現這個錯誤,請把$key補到16位(或24, 32位)
$key=$key."\0"; //缺幾位就補幾個


PHP範例參考資料:
https://github.com/stevenholder/PHP-Java-AES-Encrypt/blob/master/security.php 使用pkcs5_pad()方法
http://stackoverflow.com/questions/4537099/problem-with-aes-256-between-java-and-php  getEncrypt($sStr, $sKey)和getDecrypt($sStr, $sKey) 原型

使用Java做AES CBC 128 pkcs5padding加密
import java.io.UnsupportedEncodingException;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;

public class Encryptor {
    public static String encrypt(String key1, String key2, String value) {
        try {
            IvParameterSpec iv = new IvParameterSpec(key2.getBytes("UTF-8"));

            SecretKeySpec skeySpec = new SecretKeySpec(key1.getBytes("UTF-8"),
                    "AES");
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
            byte[] encrypted = cipher.doFinal(value.getBytes());
            System.out.println("encrypted string:"
                    + Base64.encodeBase64String(encrypted));
            return Base64.encodeBase64String(encrypted);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

    public static String decrypt(String key1, String key2, String encrypted) {
        try {
            IvParameterSpec iv = new IvParameterSpec(key2.getBytes("UTF-8"));

            SecretKeySpec skeySpec = new SecretKeySpec(key1.getBytes("UTF-8"),
                    "AES");
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
            byte[] original = cipher.doFinal(Base64.decodeBase64(encrypted));

            return new String(original);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

    public static void main(String[] args) throws UnsupportedEncodingException {
        String key1 = "Bar12345Bar12345"; // 128 bit key
        String key2 = "ThisIsASecretKet";
        System.out.println(decrypt(key1, key2,
                encrypt(key1, key2, new String("张根".getBytes("utf-8")))));
        System.out.println(parseByte2HexStr("张根".getBytes("utf-8"))); // print "张根" utf-8 in hex
    }
    
    /**
     * 将16进制转换为二进制
     * 
     * @param hexStr
     * @return
     */
    public static byte[] parseHexStr2Byte(String hexStr) {
        if (hexStr.length() < 1)
            return null;
        byte[] result = new byte[hexStr.length() / 2];
        for (int i = 0; i < hexStr.length() / 2; i++) {
            int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16);
            int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2), 16);
            result[i] = (byte) (high * 16 + low);
        }
        return result;
    }

    /**
     * 将二进制转换成16进制
     * 
     * @param buf
     * @return
     */
    public static String parseByte2HexStr(byte buf[]) {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < buf.length; i++) {
            String hex = Integer.toHexString(buf[i] & 0xFF);
            if (hex.length() == 1) {
                hex = '0' + hex;
            }
            sb.append(hex.toLowerCase());
        }
        return sb.toString();
    }
}

結果:
encrypted string:LE/jvtjPWJk7qJc49Xl3eQ== // aes加密後 base64_decode()的結果,需與php結果一致
张根 // aes 解密結果
e5bca0e6a0b9 // "张根"utf8轉hex的結果

說明:
parseHexStr2Byte(String hexStr) 和 parseByte2HexStr(byte buf[]) 這兩個function在這邊純粹測試用,與加解密無關。可忽略
我遇到的最大難點之一就是,java和php 英文字加密出來結果一樣,但是中文加密結果不一樣。( 這邊23樓也遇到一樣問題: http://my.oschina.net/Jacker/blog/86383?p=3#comments  )
原因是java在加密前的中文編碼有問題。先用
System.out.println(parseByte2HexStr("张根".getBytes("utf-8")));
檢測hex是否相同
加密時在外面就轉成utf-8再加密
encrypt(key1, key2, new String("张根".getBytes("utf-8"))) 

如果你的cipher(密文),想要用No Padding,如
Cipher cipher = Cipher.getInstance("AES/CBC/NoPADDING");
這樣你要加密的明文( "张根" ) 必須改成16位的字,否則java會報錯
encrypt(key1, key2, new String("123456789012345".getBytes("utf-8"))) // 必須改成16位字串,如1234567890123456
報錯:
javax.crypto.IllegalBlockSizeException: Input length not multiple of 16 bytes

雖然php不做padding可以加密,但結果不一樣。為了配合java這邊,php那邊還是要做pkcs5 padding

原因:
java文件檔的編碼不是utf-8
解法:
eclipse => "左邊導航欄"你的"專案"上點右鍵 => Properties => Resource => Text file encoding 選
Other: UTF-8 ( 不要選Inherited from container (GBK) ) => OK ( 這樣你原本GBK的文件中文內容會變成亂碼,代表原本編碼錯誤 )


JAVA範例參考資料:
http://stackoverflow.com/questions/15554296/simple-java-aes-encrypt-decrypt-example Simple java AES encrypt/decrypt example

使用javascript ( SlowAES )做AES CBC 128 pkcs5padding加密
<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="description" content="aes">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>aes</title>
        <script src="/jquery-1.10.2.min.js"></script>
        <script src="../js/aes.js"></script>
        <script src="../js/cryptoHelpers.js"></script>
        <script src="../js/jsHash.js"></script>
    </head>
    <body>
        <div id="output"></div>
        <script type="text/javascript">
        /**
        * An encryption setup to match our server-side one; see there for
        * documentation on it.
        **/
        function decrypt(input, key){
            var originalSize = 6;
            // var iv = 'R1We4y0JRP5w06Z8tUBPAw==';
            // iv = cryptoHelpers.base64.decode(iv); // 解密時需把base64加密後的iv解密成byte array
            var iv = "ThisIsASecretKet";
            iv = cryptoHelpers.convertStringToByteArray(iv);
            var cipherIn = input;
            // Set up encryption parameters
            var keyAsNumbers = cryptoHelpers.toNumbers( bin2hex( key ) );
            cipherIn = cryptoHelpers.base64.decode(cipherIn);
            var decrypted = slowAES.decrypt(
                cipherIn,
                slowAES.modeOfOperation.CBC,
                keyAsNumbers,
                iv
            );
            return cryptoHelpers.decode_utf8(cryptoHelpers.convertByteArrayToString(decrypted));
        }
        function encrypt( plaintext, key ){
            // Set up encryption parameters
            plaintext = cryptoHelpers.encode_utf8(plaintext);
            var inputData = cryptoHelpers.convertStringToByteArray(plaintext);
            var keyAsNumber = cryptoHelpers.toNumbers(bin2hex(key));
            var iv = cryptoHelpers.generateSharedKey(8); // 假設自動生成的iv做base64 encode加密後的結果是 R1We4y0JRP5w06Z8tUBPAw==
            var iv = "ThisIsASecretKet";
            iv = cryptoHelpers.convertStringToByteArray(iv);
            var encrypted = slowAES.encrypt(
                inputData,
                slowAES.modeOfOperation.CBC,
                keyAsNumber,
                iv
            );
            return cryptoHelpers.base64.encode(encrypted);
        }
        // Equivilent to PHP bin2hex
        function bin2hex (s) {
            var i, f = 0,
                a = [];
            s += '';
            f = s.length;
            for (i = 0; i < f; i++) {
                a[i] = s.charCodeAt(i).toString(16).replace(/^([\da-f])$/, "0$1");
            }
            return a.join('');
        }
        // Equivilent to PHP hex2bin
        function hex2bin(hex) {
            var str = '';
            for (var i = 0; i < hex.length; i += 2)
                str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
            return str;
        }
        /**
        * Some simple testing code
        **/
        $(function(){
            var key = "Bar12345Bar12345"; // key
            var plaintext = "张根";
            var output = "";
            var cipherText = encrypt(plaintext,key);
            var newPlaintext = decrypt(cipherText,key);
            output += ("<br>plaintext=" + plaintext);
            output += ("<br>cipherText=" + cipherText);
            output += ("<br>newPlaintext=" + newPlaintext);
            $('#output').html(output);
        });
        </script>
    </body>
</html>
結果:
plaintext=张根
cipherText=LE/jvtjPWJk7qJc49Xl3eQ==
newPlaintext=张根

如果要傳做過base64加密後的iv給php端,php端的iv要這樣設定,才能解密
$aes->set_iv(base64_decode($iv));

SlowAES的aes.js、cryptoHelpers.js、jsHash.js
https://code.google.com/p/slowaes/source/browse/trunk/js/

加密出來的結果要傳送
POST
1. 塞入表單後submit POST
GET
1. 塞入表單後submit GET
2. 組URL
url = $('#action').val()+"&aes_encrypt="+encodeURIComponent($('#reqParam').val())+"&iv="+$('#iv').val();
location.href = url;
必須用在字段上使用 encodeURIComponent 。
1. 勿組出url後再encodeURIComponent(url), 因為http:// 也會被encode
2. 使用encodeURI無效


其他java或php實作AES範例:
http://www.movable-type.co.uk/scripts/aes-php.html  Aes Ctr <PHP>
http://www.movable-type.co.uk/scripts/aes.html Aes Ctr <javascript> => github: https://github.com/chrisveness/crypto
http://aesencryption.net/ AES encryption <PHP/JAVA> =>Java驗證未過,可能是當初測時編碼問題
https://code.google.com/p/crypto-js/#AES crypto-js<javascript>
http://point-at-infinity.org/jsaes/ jsaes: AES in JavaScript <javascript>
http://www.cnblogs.com/yipu/articles/3871576.html [转]php与java通用AES加密解密算法 (最初對接成功的範例,但有java中文編碼問題) <PHP/JAVA>
https://github.com/stevenholder/PHP-Java-AES-Encrypt  PHP-Java-AES-Encrypt<PHP/JAVA>
http://www.java2s.com/Code/Java/Security/BasicIOexamplewithCTRusingAES.htm Basic IO example with CTR using AES : File Secure IO « Security « Java <JAVA>
http://magiclen.org/aes/ 在Java、Android、PHP實現AES加解密,並且互通的方式 <PHP/JAVA>

參考資料:
https://zh.wikipedia.org/wiki/%E9%AB%98%E7%BA%A7%E5%8A%A0%E5%AF%86%E6%A0%87%E5%87%86 高階加密標準
http://stackoverflow.com/questions/1220751/how-to-choose-an-aes-encryption-mode-cbc-ecb-ctr-ocb-cfb  How to choose an AES encryption mode (CBC ECB CTR OCB CFB)?







1 則留言: