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.
   
}

posted on 2020-03-01 13:00  aworkstory  阅读(3207)  评论(0编辑  收藏  举报

导航