首页 > 技术文章 > PHP实现3DES加密,加密模式MCRYPT_MODE_CBC,填充方式PKCS7 Padding,密钥不足补0

fish-minuet 2018-09-20 09:55 原文

为什么要对数据进行加密

  数据的安全性越来越得以重视。举个例子说,保存在数据库中的用户密码并不是明文保存的,而是采用md5加密后存储,这样即使数据库被脱库,仍能保证用户密码安全。但是,md5是不可逆的,开发人员根本就不知道用户的密码到底是什么。有些时候,我们希望加密后存储的数据是可逆的,比如一些接口密钥,这样即使数据库被脱库,如果没有对应的解密方式,攻击者盗取的密钥也是不能使用的

什么是3DES(原理):

  3DES(Triple DES),是三重数据加密算法的通称,相当于对每个数据块应用三次DES加密算法。由于计算机运算能力的增强,原版DES密码的密钥长度变得容易被暴力破解;3DES即是设计用来提供一种相对简单的方法,即通过增加DES的密钥长度来避免类似的攻击,而不是设计一种全新的块密码算法。3DES是DES向AES过渡的加密算法。

  DES算法是一种块密码加密算法,将明文分成多个长64bit的组,并使用一个64bit的密钥,对每个明文块使用同样的算法从而获得同样长度的密文块。  

 

 

  首先将64bit的明文数据分为左(L0),右(R0)两部分,然后R0经过费斯妥函数处理,将结果与L0做异或操作。异或的结果作为R1,R0作为L1,这样算是完成一个“回次”(round)。在经过16个回次以后(最有一个回次完成异或后不交换位置),再将两个32bit的块合并,这样就得到了这个明文块对应的密文块。

  

  因为3DES算法是对一个数据进行三次DES算法,所以有3个64bit密钥k1、k2、k3。

     加密算法为:

 密文 = EK3(DK2(EK1(明文)))

     也就是说,使用K1为密钥进行DES加密,再用K2为密钥进行DES“解密”,最后以K3进行DES加密。

     而解密则为其反过程:

 明文 = DK1(EK2(DK3(密文)))

     即以K3解密,以K2“加密”,最后以K1解密。

     如果k1=k2=k3,则此时3DES算法兼容DES算法,结果一致。

  

  上文我们简单描述了DES算法的流程,不过那是针对一个64bit的明文块的加密操作,结果是这个明文块对应的64bit的密文块。那么对于多个明文块加密是怎么衔接的呢?这就要依靠块密码的加密模式了。

加密模式:

  密码学中,块密码的工作模式允许使用同一个块密码密钥对多于一个一块的数据进行加密,并保证其安全性。常见的工作模式包括:ECB,CBC,OFB和CFB等。这里我们就只简单了解一下CBC的工作模式。

  在CBC模式中,每个明文块先与前一个密文块进行异或后,再进行加密。在这种方法中,每个密文块都依赖于它前面的所有明文块。第一个明文块没有前一个密文块,所以我们使用一组初始化向量(IV)代替。CBC模式的加密流程如下图所示:

 

  正如上文所述,明文会被以64bit为一组划分为若干租进行加密,每一组使用DES算法由明文获得密文。可是待加密的明文并不能保证总是可以正好分成若干个64bit的组,最后一组正好满64bit的可能性往往是比较低的,那么为了加密方便,应该怎么办呢,Padding就是用来解决这个问题的。

填充方式(PKCS7 Padding):

  我们这里简单了解一下Byte Padding中的Zero Padding、PKCS7 Padding 和 PKCS5 Padding。更多信息请参考wikipedia的Padding_(cryptography)

 

  Zero Padding:所有需要填充的地方都以0填充。 下面的例子是每8byte为一块的数据格式,最后一块只有4byte,所以要填充4byte的\x00。

 

      ... | DD DD DD DD DD DD DD DD | DD DD DD DD 00 00 00 00 |

 

     PKCS7 Padding:填充的内容是需要填充的字节数。如果最后一个数据块长度为len,每个块的长度为k,则要填充的内容为:

 

      01 -- if lth mod k = k-1

 

      02 -- if lth mod k = k-2

 

                        .

 

                        .

 

                        .

 

      k k ... k k -- if lth mod k = 0

 

     需要注意的是,如果最后一个数据块的长度len恰好等于k,则需要在后面再添加一个完整的padding块,kk...kk。下面的例子是每8byte为一块,最后一块有8byte,需要填充8byte的\x08。

 

    ... | DD DD DD DD DD DD DD DD | DD DD DD DD DD DD DD DD | 08 08 08 08 08 08 08 08 |

 

     PKCS5 Padding:PKCS5 和 PKCS7 的唯一区别是PKCS5只能用来填充64bit的数据块,除此之外可以混用。

 

     PHP的mcrypt 默认的填充值为 null ('\0'),java或.NET 默认填充方式为 PKCS7 。如果把java或.NET 填充模式改为 Zeros 即可得到与mcrypt 一致的结果。

 

使用PHP实现3DES加密:

  流程图:

    

 

  代码实现:

<?php
/**
* Created by PhpStorm.
* User: zjl
* Date: 2018/9/18
* Time: 17:11
*/
namespace app\des;

/**
* 3DES-CBC 加密解密算法
*
*/

class Mcrypt3DES{
//加密秘钥,
private $_key;
private $_iv;
public function __construct($key, $iv)
{
$this->_key = $key;
$this->_iv = $iv;
}

/**
* 对字符串进行3DES加密
* @param string 要加密的字符串
* @return mixed 加密成功返回加密后的字符串,否则返回false
*/
public function encrypt3DES($str)
{


$td = mcrypt_module_open(MCRYPT_3DES, "", MCRYPT_MODE_CBC, "");
if ($td === false) {
return false;
}
//检查加密key,iv的长度是否符合算法要求
$key = $this->fixLen($this->_key, mcrypt_enc_get_key_size($td));
if ( empty($this->_iv) )
{
$iv_t = mcrypt_create_iv(mcrypt_enc_get_iv_size($td), MCRYPT_RAND);//从随机源创建初始向量
}
else
{
$iv_t = $this->_iv;
}
$iv = $this->fixLen($iv_t, mcrypt_enc_get_iv_size($td));


//加密数据长度处理,长度必须是 n * 分组大小,否则需要后补数据,根据不同的补码方式,来补不同的数据
$str = $this->addPKCS7Padding($str, mcrypt_enc_get_block_size($td));

//初始化加密所需的缓冲区
if (mcrypt_generic_init($td, $key, $iv) !== 0) {
return false;
}
$result = mcrypt_generic($td, $str);

/**
* 对加密后的数据进行base64加密处理,在入库时,varchar类型会自动移除字符串末尾的“空格”。
* 由于加密后的数据可能是以空格(ASCII 32)结尾, 这种特性会导致数据损坏。
* 官方建议请使用 tinyblob/tinytext(或 larger)字段来存储加密数据。
*/
$result = base64_encode($result);

mcrypt_generic_deinit($td);
mcrypt_module_close($td);
return $result;
}

/**
* 对加密的字符串进行3DES解密
* @param string 要解密的字符串
* @return mixed 加密成功返回加密后的字符串,否则返回false
*/
public function decrypt3DES($str)
{
$td = mcrypt_module_open(MCRYPT_3DES, "", MCRYPT_MODE_CBC, "");
if ($td === false) {
return false;
}

//检查加密key,iv的长度是否符合算法要求
$key = $this->fixLen($this->_key, mcrypt_enc_get_key_size($td));
if ( empty($this->_iv) )
{
$iv_t = mcrypt_create_iv(mcrypt_enc_get_iv_size($td), MCRYPT_RAND);//从随机源创建初始向量
}
else
{
$iv_t = $this->_iv;
}
$iv = $this->fixLen($iv_t, mcrypt_enc_get_iv_size($td));

//初始化加密所需的缓冲区
if (mcrypt_generic_init($td, $key, $iv) !== 0) {
return false;
}

$result = mdecrypt_generic($td, base64_decode($str));

mcrypt_generic_deinit($td);
mcrypt_module_close($td);

/**
* 通过 mdecrypt_generic() 函数解密之后的数据是加密之前对加密数据长度补"\0"的数据。
* 使用 rtrim($str, "\0") 移除字符串末尾的 "\0" 。
*/
return $this->stripPKSC7Padding($result);
}

/**
* 返回适合算法长度的key,iv字符串,末尾使用0补齐
* @param string $str key或iv的值
* @param int $td_len 符合条件的key或iv长度
* @return string 返回处理后的key或iv值
*/
private function fixLen($str, $td_len)
{
$str_len = strlen($str);
if ($str_len > $td_len) {
return substr($str, 0, $td_len);
} else if($str_len < $td_len) {
return str_pad($str, $td_len, '0');
}
return $str;
}

/**
* 返回适合算法的分组大小的字符串长度,末尾使用\0补齐
* @param string $str 要加密的字符串
* @param int $td_group_len 符合算法的分组长度
* @return string 返回处理后字符串
*/
private function strPad($str, $td_group_len)
{
$padding_len = $td_group_len - (strlen($str) % $td_group_len);
return str_pad($str, strlen($str) + $padding_len, "\0");
}

/**
* 返回解密后移除字符串末尾的 "\0"的数据
* @param string $str 解密后的字符串
* @return string 返回处理后字符串
*/
private function strUnPad($str)
{
return rtrim($str, "\0");
}

/**
* 为字符串添加PKCS7 Padding
* @param string $str 源字符串
*/
private function addPKCS7Padding($str, $td_group_len){
$pad = $td_group_len - (strlen($str) % $td_group_len);
if ($pad <= $td_group_len) {
$char = chr($pad);
$str .= str_repeat($char, $pad);
}
return $str;
}

/**
* 去除字符串末尾的PKCS7 Padding
* @param string $source 带有padding字符的字符串
*/
private function stripPKSC7Padding($str){
$char = substr($str, -1, 1);
$num = ord($char);
if($num > 8){//8是此算法的分组大小,可通过mcrypt_enc_get_block_size获取
return $str;
}
$len = strlen($str);
for($i = $len - 1; $i >= $len - $num; $i--){
if(ord(substr($str, $i, 1)) != $num){
return $str;
}
}
$source = substr($str, 0, -$num);
return $source;
}
}

 

推荐阅读