為什么要對數據進行加密
數據的安全性越來越得以重視。舉個例子說,保存在數據庫中的用戶密碼並不是明文保存的,而是采用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。
加密算法為:
- 密文 = E K3(D K2(E K1(明文)))
也就是說,使用K1為密鑰進行DES加密,再用K2為密鑰進行DES“解密”,最后以K3進行DES加密。
而解密則為其反過程:
- 明文 = D K1(E K2(D K3(密文)))
即以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;
}
}