PHP 將 mcrypt_encrypt 遷移至 openssl_encrypt 的方法

注:php 的 mcrypt_簇在 7.1.0 版本中開始 deprecated,並在 7.2.0 版本中完全廢棄。其實在 2015 就已經開始建議你們使用 openssl_encrypt/openssl_decrypt來代替 mcrypt_encrypt/mcrypt_decrypt,緩衝了 N 久,這一天終於在 7.2.0 版本上到來了。

爲何要說起遷移,好比 A & B 兩套系統使用AES加密作數據傳輸,A 做爲客戶端,B 則是第三方的服務,且 A 已經在使用 7.2.0+ 版本,而 B 做爲長期運行的服務仍在使用 7.1.0-,那咱們能作的就是在 A 上使用 openssl_簇 原樣的實現mcrypt_簇的加解密功能,以便兼容 B 服務,且 mcrypt_簇 是有一些須要多加註意的地方,不然遷移之路略微坎坷。php

mcrypt_簇 雖然說被遺棄了,但 文檔頁 上依然有不少值得注意的文檔貢獻,有助於咱們將 mcrypt_簇 遷移至openssl_簇,你們應該仔細看一下。java

1.If you're writing code to encrypt/encrypt data in 2015, you should use openssl_encrypt() and openssl_decrypt(). The underlying library (libmcrypt) has been abandoned since 2007, and performs far worse than OpenSSL (which leverages AES-NI on modern processors and is cache-timing safe).

2.Also, MCRYPT_RIJNDAEL_256 is not AES-256, it's a different variant of the Rijndael block cipher. If you want AES-256 in mcrypt, you have to use MCRYPT_RIJNDAEL_128 with a 32-byte key. OpenSSL makes it more obvious which mode you are using (i.e. 'aes-128-cbc' vs 'aes-256-ctr').算法

3.OpenSSL also uses PKCS7 padding with CBC mode rather than mcrypt's NULL byte padding. Thus, mcrypt is more likely to make your code vulnerable to padding oracle attacks than OpenSSL.安全

一、即刻起,應儘量的使用openssl_簇代替mcrypt_簇來實現數據的加密功能。oracle

二、MCRYPT_RIJNDAEL_256並非AES-256,若是想使用mcrypt_簇 實現AES-256,則你應該使用 MCRYPT_RIJNDAEL_128 算法 + 32位的 key,openssl_簇 則更爲清晰的明確了各類模式。這裏我整理了一下對應關係供你們參考:dom

MCRYPT_RIJNDAEL_128 & MCRYPT_MODE_CBC + 16位Key = openssl_encrypt(AES-128-CBC, 16位Key) = AES-128
MCRYPT_RIJNDAEL_128 & MCRYPT_MODE_CBC + 24位Key = openssl_encrypt(AES-192-CBC, 24位Key) = AES-192
MCRYPT_RIJNDAEL_128 & MCRYPT_MODE_CBC + 32位Key = openssl_encrypt(AES-256-CBC, 32位Key) = AES-256

(注:AES-128, 192 and 256 的加密 key 的長度分別爲 16, 24 and 32 位)函數

openssl_簇的確更爲準確,並且mcrypt_get_key_size獲得的key長度都是 32 位,因此不太靠譜。工具

iv到是會根據cipher變動 16 、2四、32,但openssl_簇AES cipheriv長度固定16位。編碼

因此,咱們爲了最大的適配,即使如今不會再用,也要知道mcrypt_簇實現 AES-128/192/256 的標準方式爲:加密

  1. cipher 固定選用 MCRYPT_RIJNDAEL_128
  2. 根據 cipher 和 mode 生成 iv(mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128 , MCRYPT_MODE_CBC))),因 cipher 咱們固定爲 MCRYPT_RIJNDAEL_128 因此 iv 固定爲 16 位,同 openssl 兼容。
  3. 根據實際業務來肯定 key 長度: AES-128 16位 / AES-192 24位 / AES-256 32位,而不是使用 mcrypt_get_key_size(MCRYPT_RIJNDAEL_128 下使用此方法獲取的 key 長度固定爲 16 位,不符合要求)

三、這一點其實蠻重要的,涉及加密算法數據塊&填充算法PKCS7的概念。我在支付寶alipay SDK中有看到此算法的實現,雖然 sdk 中仍然使用的mcrypt_簇,但已結合了PKCS7填充算法,爲何要這樣作呢?其一是爲了安全&兼容,php mcrypt會默認使用null('\000')對數據塊進行填充,java/.net則默認使用PKCS7。其二則是爲後期遷移至openssl_簇的準備,openssl的默認填充算法也是PKCS7(固然也能夠指定使用null('\000')填充模式,但極力不推薦的)。

mcrypt_encrypt / mcrypt_decrypt

相關的支持函數

// 支持的算法 rijndael-128|rijndael-192|rijndael-256(此算法並不是AES-256,需使用rijndael-128 + key32byte實現)
mcrypt_list_algorithms()
// 支持的模式 cbc ecb 等
mcrypt_list_modes()
// 算法所對應的 key 長度:AES-128, 192 and 256 的加密 key 的長度分別爲 16, 24 and 32 位
mcrypt_get_key_size(string $cipher , string $mode)
// 算法所對應的加密向量 iv 的長度
mcrypt_get_iv_size(string $cipher , string $mode)
// 生成 iv
mcrypt_create_iv(mcrypt_get_iv_size(string $cipher , string $mode))
// 加密算法數據塊的大小 主要用於填充算法
mcrypt_get_block_size(string $cipher , string $mode)

PKCS7 填充算法的實現

/**
 * 填充算法
 * @param string $source
 * @return string
 */
function addPKCS7Padding($source, $cipher = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC)
{
    $source     = trim($source);
    // 獲取加密算法數據塊大小 用於計算須要填充多少位
    $block_size = mcrypt_get_block_size($cipher, $mode);
    $pad        = $block_size - (strlen($source) % $block_size);
    if ($pad <= $block_size) {
        $char = chr($pad);
        $source .= str_repeat($char, $pad);
    }
    return $source;
}

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

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

openssl_encrypt/openssl_decrypt

簡單講解一下平常開發中用到的參數

/**
 * $data 待加密內容
 * $method 加密算法
 * $key 加密key
 * $options 數據塊填充模式
 * $iv 加密向量
**/
openssl_encrypt(string $data, string $method, string $key[, int $options = 0[, string $iv = ""
[, string &$tag = NULL[, string $aad = ""[, int $tag_length = 16 ]]]]]): string

openssl_decrypt(string $data, string $method, string $key[, int $options = 0[, string $iv = ""
[, string $tag = "" [, string $aad = "" ]]]] ) : string

這裏須要特別注意的就是 options 選項,不少人 mcrypt_簇 遷移至 openssl_簇 時兩者加密結果內容不一致,大都是此處沒有搞清楚的緣由。options 共 3 個值可選

0 默認值 使用 PKCS7 填充算法,不對加密結果進行 base64encode
1 OPENSSL_RAW_DATA 使用 PKCS7 填充算法,且對加密結果進行 base64encode
2 OPENSSL_ZERO_PADDING 使用 null('0') 進行填充,且對加密結果進行 base64encode

因此要注意填充算法及對結果是否進行了 base64encode 編碼。

mcrypt_簇 遷移至 openssl_簇

mcrypt_簇

/**
 * 加密算法
 * @param  string $content    待加密數據
 * @param  string $key    加密key
 * @param  string $iv     加密向量
 * @param  string $cipher 加密算法
 * @param  string $mode   加密模式
 * @return string         加密後的內容且base64encode
 */
function encrypt($content, $key, $iv, $cipher = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC)
{
    //AES, 128 模式加密數據 CBC
    $content           = addPKCS7Padding($content);
    $content_encrypted = mcrypt_encrypt($cipher, $key, $content, $mode, $iv);
    return base64_encode($content_encrypted);
}

/**
 * 解密算法
 * @param  [type] $content [description]
 * @param  [type] $key     [description]
 * @param  [type] $iv      [description]
 * @param  [type] $cipher  [description]
 * @param  [type] $mode    [description]
 * @return [type]          [description]
 */
function decrypt($content_encrypted, $key, $iv, $cipher = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC)
{
    //AES, 128 模式加密數據 CBC
    $content_encrypted = base64_decode($content_encrypted);
    $content           = mcrypt_decrypt($cipher, $key, $content_encrypted, $mode, $iv);
    $content           = stripPKSC7Padding($content);
    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;
}

openssl_簇

轉換實例

以 AES-128 爲例

// 固定使用此算法 而後經過 key 的長度來決定具體使用的是何種 AES
$cipher = MCRYPT_RIJNDAEL_128;
$mode   = MCRYPT_MODE_CBC;

// openssl_簇 iv 固定爲 16 位,mcrypt_簇 MCRYPT_RIJNDAEL_128 是 16位
// 但改成 MCRYPT_RIJNDAEL_192/256 就是 24/32 位了,會不兼容 openssl_簇
// 因此務必注意向量長度統一固定 16 位方便兩套算法對齊
// $iv = mcrypt_create_iv(mcrypt_get_iv_size($cipher, $mode), MCRYPT_RAND);

// 根據須要自行定義相應的 key 長度 aes-128=16 aes-192=24 aes-256=32
$key = '0123456789012345';
// 固定爲 16 位
$iv  = '0123456789012345';

$content = "hello world";

// mcrypt 加解密
$mcrypt_data = encrypt($content, $key, $iv, $cipher, $mode);
var_dump($mcrypt_data);
$content = decrypt($mcrypt_data, $key, $iv, $cipher, $mode);
var_dump($content);

// mcrypt 時使用了 PKCS7 填充 並對結果 base64encode
// 若是 +PKCS7 +base64encode 則 option = 0
// 若是 +PKCS7 -base64encode 則 option = 1
// 若是 -PKCS7 +base64encode 則 option = 2
$openssl_data = openssl_encrypt($content, "AES-128-CBC", $key, 0, $iv)
var_dump($openssl_data);
$content = openssl_decrypt($openssl_data, "AES-128-CBC", $key, 0, $iv)
var_dump($content);

// 相互轉換
$content = openssl_decrypt($mcrypt_data, "AES-128-CBC", $key, 0, $iv)
var_dump($content);
$content = decrypt($openssl_data, $key, $iv, $cipher, $mode);
var_dump($content);

總結

一、PKCS7 填充算法。
二、openssl_encrypt / openssl_decrypt 三種模式所表示的 PKCS7/base64encode。
三、mcrypt_簇 的 cipher/mode 同 openssl_簇 的轉換。

<?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 padding, 返回的數據通過 base64 編碼.
 * 1 : OPENSSL_RAW_DATA, 自動對明文進行 pkcs7 padding, 但返回的結果未通過 base64 編碼
 * 2 : OPENSSL_ZERO_PADDING, 自動對明文進行 null('0') 填充, 同 mcrpty 一致,且返回的結果通過 base64 編碼, openssl 不推薦 0 填充的方式, 即便選擇此項也不會自動進行 padding, 仍需手動 padding
 * --------------------------------------------------------------------------------------------------------
 * mcrypt 默認是用 0 填充,爲保持良好的兼容性建議使用 pkcs7 填充數據 openssl 0|1 都使用的 pkcs7
 * pkcs7 填充
 * 加密工具類
 */

// 隨機字符串
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));
}

// 固定使用此算法 而後經過 key 的長度來決定具體使用的是何種 AES
$mcrypt_cipher = MCRYPT_RIJNDAEL_128;
$mcrypt_mode   = MCRYPT_MODE_CBC;
// openssl_簇 AES iv 固定爲 16 位,mcrypt_簇只有在 MCRYPT_RIJNDAEL_128 爲 16 位 需注意保持一致
$iv = mcrypt_create_iv(mcrypt_get_iv_size($mcrypt_cipher, $mcrypt_mode), MCRYPT_RAND);
// aes-128=16 aes-192=24 aes-256=32
$key_size = 16;
$key      = get_random_str($key_size);
// openssl_ AES 向量長度固定 16 位 這裏爲
$iv = get_random_str(16);

/**
 * 加密算法
 * @param  string $content    待加密數據
 * @param  string $key    加密key
 * @param  string $iv     加密向量
 * @param  string $cipher 加密算法
 * @param  string $mode   加密模式
 * @return string         加密後的內容且base64encode
 */
function encrypt($content, $key, $iv, $cipher = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC)
{
    //AES, 128 模式加密數據 CBC
    $content           = addPKCS7Padding($content);
    $content_encrypted = mcrypt_encrypt($cipher, $key, $content, $mode, $iv);
    return base64_encode($content_encrypted);
}

/**
 * 解密算法
 * @param  [type] $content [description]
 * @param  [type] $key     [description]
 * @param  [type] $iv      [description]
 * @param  [type] $cipher  [description]
 * @param  [type] $mode    [description]
 * @return [type]          [description]
 */
function decrypt($content_encrypted, $key, $iv, $cipher = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC)
{
    //AES, 128 模式加密數據 CBC
    $content_encrypted = base64_decode($content_encrypted);
    $content           = mcrypt_decrypt($cipher, $key, $content_encrypted, $mode, $iv);
    $content           = stripPKSC7Padding($content);
    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;
}

$content = "hello world";

var_dump($data = encrypt($content, $key, $iv, $mcrypt_cipher, $mcrypt_mode));
var_dump(decrypt($data, $key, $iv, $mcrypt_cipher, $mcrypt_mode));
var_dump($openssl_data = openssl_encrypt($content, "AES-128-CBC", $key, 0, $iv));
var_dump(openssl_decrypt($openssl_data, "AES-128-CBC", $key, 0, $iv));

// var_dump(openssl_cipher_iv_length('AES-256-CBC'));
// var_dump(mcrypt_get_key_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC));
相關文章
相關標籤/搜索