PHP實現AES對稱加密


采坑記錄

Base64編碼
所有的數據都能被編碼為只用65個字符就能表示的文本。
標准的Base64每行為76個字符,每行末尾添加一個回車換行符(\r\n)。不論每行是否滿76個字符,都要添加一個回車換行符。
65字符:A~Z a~z 0~9 + / =
URL Base64算法中,為了安全,會把 + 替換成 - ,把 / 替換成 _
= 有時候用 ~ 或 . 代替
Base64的應用:密鑰,密文,圖片
數據簡單加密或者預處理
Base64編碼解碼與btoa、atob

Hex 編碼
二進制數據最常用的一種表示方式。用0-9 a-f 16個字符表示。每個十六進制字符代表4bit。也就是2個十六進制字符代表一個字節。在實際應用中,尤其在密鑰初始化的時候,一定要分清楚自己傳進去的密鑰是哪種方式編碼的,采用對應方式解析,才能得到正確的結果

1:openssl_encrypt中aes-128-cbc、aes-256-cbc中的128、256是與秘鑰位數有關的,16位秘鑰需要使用aes-128-cbc模式。
參考文章:https://www.douban.com/note/628737539/

加密后的字符串如果直接用post form形式提交給php后端,會出現無法解密的情況,經過多次測試,終於找到原因。

AES是基於數據塊的加密方式,也就是說,每次處理的數據是一塊(16字節),當數據不是16字節的倍數時填充,這就是所謂的分組密碼(區別於基於比特位的流密碼),16字節是分組長度。

2:ECB CBC 等的區別

CBC: CBC模式的全稱是Cipher Block Chaining模式(密文分組鏈接模式)
ECB模式只進行了加密,而CBC模式則在加密之前進行了一次XOR

分組加密的幾種方式
ECB:是一種基礎的加密方式,密文被分割成分組長度相等的塊(不足補齊),然后單獨一個個加密,一個個輸出組成密文。
CBC:是一種循環模式,前一個分組的密文和當前分組的明文異或操作后再加密,這樣做的目的是增強破解難度。
CFB/OFB實際上是一種反饋模式,目的也是增強破解的難度。

3: 初始化向量
當加密第一個明文分組時,由於不存在“前一個密文分組”,因此需要事先准備一個長度為一個分組的比特序列來代替“前一個密文分組”,這個比特序列稱為初始化向量(Initialization Vector),通常縮寫為IV,一般來說,每次加密時都會隨機產生一個不同的比特序列來作為初始化向量。

所以:ECB 模式不需要IV

CBC模式的特點
明文分組在加密之前一定會與“前一個密文分組”進行XOR運算,因此即使明文分組1和明文分組2的值是相等的,密文分組1和2的值也不一定是相等的。這樣一來,ECB模式的缺陷在CBC模式中就不存在了。
加密過程:在CBC模式中,無法單獨對一個中間的明文分組進行加密。例如,如果要生成密文分組3,則至少需要湊齊明文分組1、2、3才行。
解密過程:假設CBC模式加密的密文分組中有一個分組損壞了。在這種情況下,只要密文分組的長度沒有發生變化,則解密時最多只有2個分組受到數據損壞的影響。

參考文章:https://www.cnblogs.com/wangle1001986/p/11468419.html

快捷測試

echo abc | openssl aes-128-cbc -k 123 -base64

要點注意:

1 使用何種填充算法。mcrypt自動使用NUL("\0"),openssl自動使用PKCS7
2 是否對數據做了base64編碼處理。mcrypt默認不使用,openssl默認使用

涉及知識講解:

1 AES詳解:

AES是當前最為常用的安全對稱加密算法

AES-128:16位密鑰key
AES-192:24位密鑰key
AES-256:32位密鑰key

即算法統一使用MCRYPT_RIJNDAEL_128,並通過key的位數來選定是以何種AES標准加密,iv固定16位(openssl的AES加密iv始終為16位),mode選用CBC模式

openssl加解密函數中,options參數尤為重要,它是兼容mcrypt算法的關鍵:
options = 0:默認模式,自動對明文進行pkcs7 padding,且數據做base64編碼處理
options = 1:OPENSSL_RAW_DATA,自動對明文進行pkcs7 padding,數據未經base64編碼處理
options = 2:OPENSSL_ZERO_PADDING,要求待加密的數據長度已按"\0"填充與加密算法數據塊長度對齊,即同mcrypt默認填充的方式一致,且對數據做base64編碼處理(此模式下openssl要求待加密數據已按"\0"填充好,其並不會自動幫你填充數據)。

參考文章:
https://www.cnblogs.com/jingxiaoniu/p/12217789.html

項目背景

在開發支付寶小程序時,獲取用戶手機號,需要AES解密得到手機號明文。
官方並沒有提供PHP解密的實例,所以要用PHP實現AES算法的解密過程。

要點

  1. PHP實現AES解密可以用 mcrypt 類方法,以及 openssl 族的方法。mcrypt 在PHP7.2被棄用,所以推薦使用 openssl 實現。
  2. screct_key 即 aes_key 是從支付寶小程序管理中心后台獲取的。

實現過程

openssl 實現方式

/**
	 * openssl 解密
	 * @param unknown $encryptedData
	 * @return string
	 */
	protected static function decryptOpenssl($encryptedData, $screct_key) {
		$aesKey = base64_decode($screct_key);
		$aesIV = null;
		$aesCipher = base64_decode($encryptedData);
		$result = openssl_decrypt($aesCipher, "AES-128-CBC", $aesKey, 1, $aesIV); //1=OPENSSL_RAW_DATA 模式
// 		$result = openssl_decrypt($aesCipher, "AES-128-CBC", $aesKey, 2, $aesIV);
		return $result;
	}

options 參數即為重要,它是兼容 mcrpty 算法的關鍵:

options = 0: 默認模式,自動對明文進行 pkcs7 padding,且數據做 base64 編碼處理。
options = 1: OPENSSL_RAW_DATA,自動對明文進行 pkcs7 padding, 且數據未經 base64 編碼處理。這里的理解很重要
options = 2: OPENSSL_ZERO_PADDING,要求待加密的數據長度已按 "0" 填充與加密算法數據塊長度對齊,即同 mcrpty 默認填充的方式一致,且對數據做 base64 編碼處理。注意,此模式下 openssl 要求待加密數據已按 "0" 填充好,其並不會自動幫你填充數據,如果未填充對齊,則會報錯。

mcrypt 實現方式,可以參考alipay sdk 的aop/AopEnctypt.php 的 decrypt

class AliBizDataCrypt {
	 * 解密方法
	 *
	 * @param string $encryptedData : 需要解密的報文
	 * @return string
	 */
	protected static function decrypt($encryptedData, $screct_key) {
		// AES, 128 模式加密數據 CBC
		$encryptedDataBase64Decoded = base64_decode($encryptedData);
		$screct_key = base64_decode($screct_key);
		// 設置全0的IV
		$iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
		$iv = str_repeat("\0", $iv_size);
		$decrypt_str = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $screct_key, $encryptedDataBase64Decoded, MCRYPT_MODE_CBC, $iv);
		$decrypt_str = self::stripPKSC7Padding($decrypt_str);
		return $decrypt_str;
	}
	
	/**
	 * 移去填充算法
	 *
	 * @param string $source
	 * @return string
	 */
	protected static function stripPKSC7Padding($source) {
		$char = substr($source, - 1);
		$num = ord($char);
		if( $num == 62 )
			return $source;
		$source = substr($source, 0, - $num);
		return $source;
	}
}	

注意,PHP 7.1 之前使用 mcrypt 在PHP 7.1中Mcrypt已經被棄用了

//AES/ECB/PKCS5Padding
//加密
public function encrypt($data, $key) {
		$l = strlen($key);
		if ($l < 16){
			$key = str_repeat($key, ceil(16/$l));
		}
		$key = base64_decode($key);
		
		$alg = MCRYPT_RIJNDAEL_128; // AES
		$mode = MCRYPT_MODE_ECB; // not recommended unless used with OTP
		
		$block_size = mcrypt_get_block_size($alg, $mode);
		$data = $this->pkcs5_pad($data, $block_size);
		
		if (function_exists('mcrypt_encrypt')){
			$val = mcrypt_encrypt($alg, $key, $data, $mode);
		}else{
			$val = openssl_encrypt($data, $this->encrypt_method, $key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING);
			NLog::fl('ERROR', __CLASS__.' ' . __FUNCTION__ . ' 0 mcrypt_not_exists ');
			if ($val === false) {
				NLog::fl('ERROR', __CLASS__.' ' . __FUNCTION__ . ' 0 openssl_error_string '. openssl_error_string());
			}
		}
		return base64_encode($val);
	}
function pkcs5_pad($text, $blocksize){
		$pad = $blocksize - (strlen($text) % $blocksize);
		return $text . str_repeat(chr($pad), $pad);
	}
//解密
public function mcrypt_decrypt($data, $key) {
		$data = base64_decode($data);
		$key = base64_decode($key);
		$decrypted= mcrypt_decrypt(MCRYPT_RIJNDAEL_128,$key,$data, MCRYPT_MODE_ECB);
		$dec_s = strlen($decrypted);
		$padding = ord($decrypted[$dec_s-1]);
		$decrypted = substr($decrypted, 0, -$padding);
		return $decrypted;
	}

有用的信息

AES-256 is different from RIJNDAEL-256. The 256 in AES refers to the key size, where the 256 in RIJNDAEL refers to block size. AES-256 is RIJNDAEL-128 when used with a 256 bit key

The security of AES-256 versus AES-128 isn't that significant;

參考:https://stackoverflow.com/questions/6770370/aes-256-encryption-in-php 

js AES加密

require('crypto-js');
var plain = '{"id":"21_actid", "unicode":"test00001"}';
var keyStr = 'bXlfdGVzdF8yMV9hZXNfZW5jcnlfa2V5'; //32位

var encryptedBase64 = encrypt(plain, keyStr);

// console.log(encrypted,encryptedBase64);
postman.setGlobalVariable("encryptData",encryptedBase64);

var myFormData = pm.request.body.formdata;
var appId = myFormData.get('appId');
var timestamp = myFormData.get('timestamp');
var signres = appId+timestamp+encryptedBase64;

console.log(signres);
var sign = CryptoJS.MD5(signres).toString();

postman.setGlobalVariable("sign",sign);

var base64 = 'SGVsbG8gd29ybGQ=';
var words = CryptoJS.enc.Base64.parse(base64);
var textString = CryptoJS.enc.Utf8.stringify(words);
// console.log(textString);

//CryptoJS.pad.Pkcs7 NoPadding
//注意事項:
//1、PKCS5Padding的補碼方式,其實就是 PKCS7
function encrypt(str, key){
    var words = CryptoJS.enc.Base64.parse(key);
    // console.log(words);
    // var keyAES = CryptoJS.enc.Utf8.stringify(words);

    // var key = CryptoJS.enc.Utf8.parse(key);
    var encrypted = CryptoJS.AES.encrypt(str, words, {mode: CryptoJS.mode.ECB, padding:CryptoJS.pad.Pkcs7});
    return encrypted.toString();
    //You don't need to convert encrypted value to base64, encrypted.toString() returns base64 value.
   
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM