PHP 迁移 Mcrypt 至 OpenSSL 加密算法详解

更新日期: 2019-03-03阅读: 1.8k标签: 算法

对称加解密算法中,当前最为安全的是 AES 加密算法(以前应该是是 DES 加密算法),php 提供了两个可以用于 AES 加密算法的函数簇:Mcrypt 和 OpenSSL。

其中 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


填充算法

AES 是按数据块大小(128/192/256)对待加密内容进行分块处理的,会经常出现最后一段数据长度不足的场景,这时就需要填充数据长度到加密算法对应的数据块大小。

主要的填充算法有填充 NUL("0") 和 PKCS7,Mcrypt 默认使用的 NUL("0") 填充算法,当前已不被推荐,OpenSSL 则默认模式使用 PKCS7 对数据进行填充并对加密后的数据进行了 base64encode 编码,所以建议开发中使用 PKCS7 对待加密数据进行填充,已保证通用性(alipay sdk 中虽然使用了 Mcrypt 加密簇,但使用 PKCS7 算法对数据进行了填充,这样在一定程度上亲和了 OpenSSL 加密算法)。


NUL("\0") 填充算法

Mcrypt 的默认填充算法。NUL 即为 Ascii 表的编号为 0 的元素,即空元素,转移字符是 "\0",PHP 的 pack 打包函数在 'a' 模式下就是以 NUL 字符对内容进行填充的,当然,使用 "\0" 手动拼接也是可以的。

/**
 * 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");
}


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 加解密

AES-128-CBC = mcrpyt(cipher MCRYPT_RIJNDAEL_128 + key 16位 + iv 16 位 + mode MCRYPT_MODE_CBC)
AES-192-CBC = mcrpyt(cipher MCRYPT_RIJNDAEL_128 + key 24位 + iv 16 位 + mode MCRYPT_MODE_CBC)
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

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 长度都应该是。

来自:https://segmentfault.com/a/1190000018371544


链接: https://www.fly63.com/article/detial/2193

js洗牌算法:javascript数组随机打乱顺序的实现方法

有一个数组,我们需要通过js对数组的元素进行随机排序,然后输出,这其实就是洗牌算法,首页需要从元素中随机取一个和第一元进行交换,然后依次类推,直到最后一个元素。

程序员必须知道的10大基础实用算法及其讲解

程序员必须知道的10大算法:快速排序算法、堆排序算法、归并排序、二分查找算法、BFPRT(线性查找算法)、DFS(深度优先搜索)、BFS(广度优先搜索)、Dijkstra算法、动态规划算法、朴素贝叶斯分类算法

js从数组取出 连续的 数字_实现一维数组中连续数字分成几个连续的数字数组

使用原生js将一维数组中,包含连续的数字分成一个二维数组,这篇文章分2种情况介绍如何实现?1、过滤单个数字;2、包含单个数字。

原生Js获取数组中最长的连续数字序列的方法

给定一个无序的整数序列, 找最长的连续数字序列。例如:给定[100, 4, 200, 1, 3, 2],最长的连续数字序列是[1, 2, 3, 4]。此方法不会改变传入的数组,会返回一个包含最大序列的新数组。

Tracking.js_ js人脸识别前端代码/算法框架

racking.js 是一个独立的JavaScript库,实现多人同时检测人脸并将区域限定范围内的人脸标识出来,并保存为图片格式,跟踪的数据既可以是颜色,也可以是人,也就是说我们可以通过检测到某特定颜色,或者检测一个人体/脸的出现与移动,来触发JavaScript 事件。

JS常见算法题目

JS常见算法题目:xiaoshuo-ss-sfff-fe 变为驼峰xiaoshuoSsSfffFe、数组去重、统计字符串中出现最多的字母、字符串反序、深拷贝、合并多个有序数组、约瑟夫环问题

RSA算法详解

这篇文章主要是针对一种最常见的非对称加密算法——RSA算法进行讲解。其实也就是对私钥和公钥产生的一种方式进行描述,RSA算法的核心就是欧拉定理,根据它我们才能得到私钥,从而保证整个通信的安全。

PageRank算法的定义与来源、以及PageRank算法原理

PageRank,网页排名,又称网页级别、Google左侧排名或佩奇排名,是一种由 根据网页之间相互的超链接计算的技术,而作为网页排名的要素之一,以Google公司创办人拉里·佩奇(Larry Page)之姓来命名。

js算法_js判断一个字符串是否是回文字符串

什么是回文字符串?即字符串从前往后读和从后往前读字符顺序是一致的。例如:字符串aba,从前往后读是a-b-a;从后往前读也是a-b-a

js之反转整数算法

将一个整数中的数字进行颠倒,当颠倒后的整数溢出时,返回 0 ;当尾数为0时候需要进行舍去。解法:转字符串 再转数组进行操作,看到有人用四则运算+遍历反转整数。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!