1. 程式人生 > >PHP 遷移 Mcrypt 至 OpenSSL 加密演算法詳解

PHP 遷移 Mcrypt 至 OpenSSL 加密演算法詳解

浪費了“黃金五年”的Java程式設計師,還有救嗎? >>>   

對稱加解密演算法中,當前最為安全的是 AES 加密演算法(以前應該是是 DES 加密演算法),PHP 提供了兩個可以用於 AES 加密演算法的函式簇:McryptOpenSSL

其中 Mcrypt 在 PHP 7.1.0 中被 Deprecated,在 PHP 7.2.0 中被移除,所以即可起你應該使用 OpenSSL 來實現 AES 的資料加解密。

在一些場景下,我們不能保證兩套通訊系統都使用了相函式簇去實現加密演算法,可能 siteA 使用了最新的 OpenSSL 來實現了 AES 加密,但作為第三方服務的 siteB 可能仍在使用 Mcrypt 演算法,這就要求我們必須清楚 Mcrypt 同 OpenSSL 之間的差異,以便保證資料加解密的一致性。

下文中我們將分別使用 Mcrypt 和 OpenSSL 來實現 AES-128/192/256-CBC 加解密,二者同步加解密的要點為:

1、使用何種填充演算法。Mcrypt 自動使用 NUL("\0"),OpenSSL 自動使用 PKCS7。

2、否對資料做了base64編碼處理。Mcrypt 預設不使用 base64 編碼(雖然我們很建議使用),OpenSSL 預設使用 base64編碼。

協同好以上兩點,就可以讓 Mcrypt 和 OpenSSL 之間一致性的對資料進行加解密。

AES 概述

AES 是當前最為常用的安全對稱加密演算法,關於對稱加密這裡就不在闡述了。

AES 有三種演算法,主要是對資料塊的大小存在區別:

AES-128:需要提供 16 位的金鑰 key

AES-192:需要提供 24 位的金鑰 key

AES-256:需要提供 32 位的金鑰 key

PKCS7 填充演算法

PKCS7 填充演算法我覺得也有必要提及一下, AES 是按資料塊大小(128/192/256)對待加密內容進行分塊處理的,會經常出現最後一段資料長度不足的場景,這時就需要填充資料長度到加密演算法對應的資料塊大小。

主要的填充演算法有填充 NUL("\0") 和 PKCS7,Mcrypt 預設使用的 NUL("\0") 填充演算法,當前已不被推薦,OpenSSL 則預設模式使用 PKCS7 對資料進行填充並對加密後的資料進行了 base64encode 編碼,所以建議開發中使用 PKCS7 對待加密資料進行填充,已保證通用性(alipay sdk 中雖然使用了 Mcrypt 加密簇,但使用 PKCS7 演算法對資料進行了填充,這樣在一定程度上親和了 OpenSSL 加密演算法)。

下面我們給出 PKCS7 填充演算法 PHP 的實現:

/**
 * PKCS7填充演算法
 * @param string $source
 * @return string
 */
function addPKCS7Padding($source, $cipher = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC)
{
    $source = trim($source);
    $block  = mcrypt_get_block_size($cipher, $mode);
    $pad    = $block - (strlen($source) % $block);
    if ($pad <= $block) {
        $char = chr($pad);
        $source .= str_repeat($char, $pad);
    }
    return $source;
}

/**
 * 移去PKCS7填充演算法
 * @param string $source
 * @return string
 */
function stripPKSC7Padding($source)
{
    $source = trim($source);
    $char   = substr($source, -1);
    $num    = ord($char);
    if ($num == 62) {
        return $source;
    }

    $source = substr($source, 0, -$num);
    return $source;
}

Mcrypt

特點:預設使用 NUL("\0") 自動對待加密資料進行填充以對齊加密演算法資料塊長度。

獲取 mcrypt 支援的演算法,這裡我們只關注 AES 演算法。

$cipher_list = mcrypt_list_algorithms();
print_r($cipher_list);

//  加密演算法名稱  對應的 常量標識
// 'rijndael-128' == MCRYPT_RIJNDAEL_128
// 'rijndael-192' == MCRYPT_RIJNDAEL_192
// 'rijndael-256' == MCRYPT_RIJNDAEL_256

注意:mcrypt 雖然支援 AES 三種演算法,但除 MCRYPT_RIJNDAEL_128 外,MCRYPT_RIJNDAEL_192/256 並未遵循 AES-192/256 標準進行加解密的演算法,即如果你同其他系統通訊(java/.net),使用 MCRYPT_RIJNDAEL_192/256 可能無法被其他嚴格按照 AES-192/256 標準的系統正確的資料解密。官方文件頁面中也有人在 User Contributed Notes 中提及。這裡給出如何使用 mcrpyt 做標註的 AES-128/192/256 加解密

  1. AES-128-CBC = mcrpyt(cipher MCRYPT_RIJNDAEL_128 + key 16位 + iv 16 位 + mode MCRYPT_MODE_CBC)
  2. AES-192-CBC = mcrpyt(cipher MCRYPT_RIJNDAEL_128 + key 24位 + iv 16 位 + mode MCRYPT_MODE_CBC)
  3. AES-256-CBC = mcrpyt(cipher MCRYPT_RIJNDAEL_128 + key 32位 + iv 16 位 + mode MCRYPT_MODE_CBC)

即演算法統一使用 MCRYPT_RIJNDAEL_128,並通過 key 的位數 來選定是以何種 AES 標準做的加密,iv 是建議新增且建議固定為16位(OpenSSL的 AES加密 iv 始終為 16 位,便於統一對齊),mode 選用的 CBC 模式。

mcrypt 在對資料進行加密處理時,如果發現數據長度與使用的加密演算法的資料塊長度未對齊,則會自動使用 "\0" 對待加密資料進行填充,但 "\0" 填充模式已不再被推薦,為了與其他系統有更好的相容性,建議大家手動對資料進行 PKCS7 填充。

OpenSSL

php 文件地址

openssl 簇加密方法更為簡單明確,mcrypt 還要將加密演算法分為 cipher + mode 去指定,openssl 則只需要直接指定 method 為 AES-128-CBC,AES-192-CBC,AES-256-CBC 即可。且提供了三種資料處理模式,即 預設模式 0 / OPENSSL_RAW_DATA / OPENSSL_ZERO_PADDING

openssl 預設的資料填充方式是 PKCS7,為相容 mcrpty 也提供處理 "\0" 填充的資料的模式,具體為下: 

//我們只看部分引數 還有一些預設引數沒列出
// 加密
openssl_encrypt(string $data, string $method, string $key, int $options = 0, string $iv = "")
// 解密
openssl_decrypt(string $data, string $method, string $key, int $options = 0, string $iv = "")

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" 填充好,其並不會自動幫你填充資料,如果未填充對齊,則會報錯。

故可以得出 mcrpty簇 與 openssl簇 的相容條件如下:

1、如果 A 系統使用了 mcrypt 的預設的 "\0" 自動填充演算法進行了資料加密,那 B 系統使用 openssl 進行解密時,需要選擇 OPENSSL_ZERO_PADDING 模式。這裡同時要特別注意,OPENSSL_ZERO_PADDING 模式是認為資料是做了 base64 編碼處理的,如果 A 系統 mcrpty 沒有對資料做 base64_encode 處理,則 B 解密資料時還需要將資料 base64_encode 後再傳遞給 openssl_decrypt。

2、如果 A 系統手動使用了 PKCS7 對待加密資料做了處理,那 B 系統只需根據 A 系統是否對資料做了 base64 編碼處理選擇 0或 OPENSSL_ZERO_PADDING 模式即可。如果對 A 使用了 base64 編碼,則 B 使用 0 模式;如果 A 沒有使用 base64 編碼,則使用 OPENSSL_RAW_DATA 模式。

加解密例項

建議將原始碼複製到本地執行,根據執行結果更好理解。

<?php
/**
 * MCRYPT_RIJNDAEL_128 & CBC + 16位Key + 16位iv = openssl_encrypt(AES-128-CBC, 16位Key, 16位iv) = AES-128
 * MCRYPT_RIJNDAEL_128 & CBC + 24位Key + 16位iv = openssl_encrypt(AES-192-CBC, 24位Key, 16位iv) = AES-192
 * MCRYPT_RIJNDAEL_128 & CBC + 32位Key + 16位iv = openssl_encrypt(AES-256-CBC, 32位Key, 16位iv) = AES-256
 * ------------------------------------------------------------------------------------------------------
 * openssl_簇 options
 * 0 : 預設模式,自動對資料做 pkcs7 填充, 且返回的加密資料經過 base64 編碼
 * 1 : OPENSSL_RAW_DATA, 自動對資料做 pkcs7 填充, 且返回的加密資料未經過 base64 編碼
 * 2 : OPENSSL_ZERO_PADDING, 處理使用 NUL("\0") 的資料,故需手動使用 NUL("\0") 填充好資料再做加密處理,如未做則會報錯
 * --------------------------------------------------------------------------------------------------------
 * 加密工具類
 */

// mcrypt AES 固定使用 MCRYPT_RIJNDAEL_128 通過 key 的長度來決定具體使用的具體何種 AES
$mcrypt_cipher = MCRYPT_RIJNDAEL_128;
$mcrypt_mode   = MCRYPT_MODE_CBC;

// aes-128=16 aes-192=24 aes-256=32
$key_size = 16;
$key      = get_random_str($key_size);
// openssl AES 向量長度固定 16 位 這裡為相容建議固定長度為 16 位
$iv_size = 16;
$iv      = get_random_str($iv_size);

// 隨機字串
function get_random_str($length = 16)
{
    $char_set = array_merge(range('a', 'z'), range('A', 'Z'), range('0', '9'));
    shuffle($char_set);
    return implode('', array_slice($char_set, 0, $length));
}

/**
 * 加密演算法
 * @param  string $content 待加密資料
 * @param  string $key     加密key 注意 key 長度要求
 * @param  string $iv      加密向量 固定為16位可以保證與openssl的相容性
 * @param  string $cipher  加密演算法
 * @param  string $mode    加密模式
 * @param  bool $pkcs7     是否使用pkcs7填充 否則使用 mcrypt 自帶的 NUL("\0") 填充
 * @param  bool $base64    是否對資料做 base64 處理 因加密後資料會有非列印字元 所以推薦做 base64 處理
 * @return string          加密後的內容
 */
function user_mcrypt_encrypt($content, $key, $iv, $cipher = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC, $pkcs7 = true, $base64 = true)
{
    //AES, 128 模式加密資料 CBC
    $content           = $pkcs7 ? addPKCS7Padding($content) : $content;
    $content_encrypted = mcrypt_encrypt($cipher, $key, $content, $mode, $iv);
    return $base64 ? base64_encode($content_encrypted) : $content_encrypted;
}

/**
 * 解密演算法
 * @param  [type] $content_encrypted 待解密的內容
 * @param  [type] $key     加密key 注意 key 長度要求
 * @param  [type] $iv      加密向量 固定為16位可以保證與openssl的相容性
 * @param  [type] $cipher  加密演算法
 * @param  [type] $mode    加密模式
 * @param  bool $pkcs7     帶解密內容是否使用了pkcs7填充 如果沒使用則 mcrypt 會自動移除填充的 NUL("\0")
 * @param  bool $base64    是否對資料做 base64 處理
 * @return [type]          [description]
 */
function user_mcrypt_decrypt($content_encrypted, $key, $iv, $cipher = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC, $pkcs7 = true, $base64 = true)
{
    //AES, 128 模式加密資料 CBC
    $content_encrypted = $base64 ? base64_decode($content_encrypted) : $content_encrypted;
    $content           = mcrypt_decrypt($cipher, $key, $content_encrypted, $mode, $iv);
    // 解密後的內容 要根據填充演算法來相應的移除填充數
    $content = $pkcs7 ? stripPKSC7Padding($content) : rtrim($content, "\0");
    return $content;
}

/**
 * PKCS7填充演算法
 * @param string $source
 * @return string
 */
function addPKCS7Padding($source, $cipher = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC)
{
    $source = trim($source);
    $block  = mcrypt_get_block_size($cipher, $mode);
    $pad    = $block - (strlen($source) % $block);
    if ($pad <= $block) {
        $char = chr($pad);
        $source .= str_repeat($char, $pad);
    }
    return $source;
}
/**
 * 移去PKCS7填充演算法
 * @param string $source
 * @return string
 */
function stripPKSC7Padding($source)
{
    $source = trim($source);
    $char   = substr($source, -1);
    $num    = ord($char);
    if ($num == 62) {
        return $source;
    }

    $source = substr($source, 0, -$num);
    return $source;
}

/**
 * NUL("\0")填充演算法
 * @param string $source
 * @return string
 */
function addZeroPadding($source, $cipher = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC)
{
    $source = trim($source);
    // openssl 並沒有提供加密cipher對應的資料塊大小的api這點比較坑
    $block = mcrypt_get_block_size($cipher, $mode);
    $pad   = $block - (strlen($source) % $block);
    if ($pad <= $block) {
        // $source .= str_repeat("\0", $pad);//KISS寫法
        // pack 方法 a 模式使用 NUL("\0") 對內容進行填充  A 模式則使用空白字元填充
        $source .= pack("a{$pad}", ""); //高階寫法
    }
    return $source;
}

/**
 * NUL("\0")填充演算法移除
 * @param string $source
 * @return string
 */
function stripZeroPadding($source)
{
    return rtrim($source, "\0");
}

// 待加密內容
$content = "hello world";

echo '使用 NUL("\0") 填充演算法 不對結果做 base64 處理:' . PHP_EOL;
echo 'mcrypt 加密:' . PHP_EOL;
var_dump($data = user_mcrypt_encrypt($content, $key, $iv, $mcrypt_cipher, $mcrypt_mode, false, false));
echo 'openssl 解密:' . PHP_EOL;
// 需經過 NUL("\0") 填充加密後被 base64_encode 的資料 解密後續手動移除 NUL("\0")
var_dump(stripZeroPadding(openssl_decrypt(base64_encode($data), "AES-128-CBC", $key, OPENSSL_ZERO_PADDING, $iv)));
echo 'openssl 加密:' . PHP_EOL;
// 需對待處理的資料做 NUL("\0") 填充,且返回的資料被 base64_encode 編碼了
var_dump($data = base64_decode(openssl_encrypt(addZeroPadding($content), "AES-128-CBC", $key, OPENSSL_ZERO_PADDING, $iv)));
echo 'mcrypt 解密:' . PHP_EOL;
var_dump(user_mcrypt_decrypt($data, $key, $iv, $mcrypt_cipher, $mcrypt_mode, false, false));
echo PHP_EOL;

echo '使用 NUL("\0") 填充演算法 對結果做 base64 處理:' . PHP_EOL;
echo 'mcrypt 加密:' . PHP_EOL;
var_dump($data = user_mcrypt_encrypt($content, $key, $iv, $mcrypt_cipher, $mcrypt_mode, false, true));
echo 'openssl 解密:' . PHP_EOL;
var_dump(stripZeroPadding(openssl_decrypt($data, "AES-128-CBC", $key, OPENSSL_ZERO_PADDING, $iv)));
echo 'openssl 加密:' . PHP_EOL;
var_dump($data = openssl_encrypt(addZeroPadding($content), "AES-128-CBC", $key, OPENSSL_ZERO_PADDING, $iv));
echo 'mcrypt 解密:' . PHP_EOL;
var_dump(user_mcrypt_decrypt($data, $key, $iv, $mcrypt_cipher, $mcrypt_mode, false, true));
echo PHP_EOL;

echo "使用 pkcs7 填充演算法 不對結果做 base64 處理" . PHP_EOL;
echo 'mcrypt 加密:' . PHP_EOL;
var_dump($data = user_mcrypt_encrypt($content, $key, $iv, $mcrypt_cipher, $mcrypt_mode, true, false));
echo 'openssl 解密:' . PHP_EOL;
var_dump(openssl_decrypt($data, "AES-128-CBC", $key, OPENSSL_RAW_DATA, $iv));
echo 'openssl 加密:' . PHP_EOL;
var_dump($data = openssl_encrypt($content, "AES-128-CBC", $key, OPENSSL_RAW_DATA, $iv));
echo 'mcrypt 解密:' . PHP_EOL;
var_dump(user_mcrypt_decrypt($data, $key, $iv, $mcrypt_cipher, $mcrypt_mode, true, false));
echo PHP_EOL;

echo "使用 pkcs7 填充演算法 對結果做 base64 處理(推薦):" . PHP_EOL;
echo 'mcrypt 加密:' . PHP_EOL;
var_dump($data = user_mcrypt_encrypt($content, $key, $iv, $mcrypt_cipher, $mcrypt_mode, true, true));
echo 'openssl 解密:' . PHP_EOL;
var_dump(openssl_decrypt($data, "AES-128-CBC", $key, 0, $iv));
echo 'openssl 加密:' . PHP_EOL;
var_dump($data = openssl_encrypt($content, "AES-128-CBC", $key, 0, $iv));
echo 'mcrypt 解密:' . PHP_EOL;
var_dump(user_mcrypt_decrypt($data, $key, $iv, $mcrypt_cipher, $mcrypt_mode, true, true));

注意要點:

1、二者使用的何種填充演算法。

2、二者對資料是否有 base64 編碼要求。

3、mcrypt 需固定使用 MCRYPT_RIJNDAEL_128,並通過調整 key 的長度 16, 24,32 來實現 ase-128/192/256 加密演算法,同時 iv 最好固定為 16 位,因為 openssl 的 aes 的 i