RSA加密算法的应用

<?php
/**
 * 生成RSA密钥对(公钥和私钥)的命令:
 * [root@localhost ~]# openssl genrsa -out rsa_private_key.pem 2048
 * [root@localhost ~]# openssl pkcs8 -topk8 -inform PEM -in rsa_private_key.pem -outform PEM -nocrypt -out private_key.pem
 * [root@localhost ~]# openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
 * 命令①:生成原始RSA私钥文件,参数2048为密钥长度,通常使用2048即可,如果对安全性要求较高可以使用更长的,该数字一般为1024的整数倍。
 * 命令②:将原始RSA私钥文件转换为pkcs8格式。
 * 命令③:根据原始RSA私钥文件生成RSA公钥文件。
 * 备注①:公钥发放给客户端用于加密数据,私钥则要留在服务端用于解密客户端发送过来的加密数据。
 * 备注②:本类支持不同长度的密钥的加解密,网上很多代码都是加密按117个字节将数据分割然后分段加密,而解密则按128个字节将密文分割然后分
 *         段解密,这两个数字实际上是针对长度为1024的密钥,如果换成了非1024长度(如2048)的密钥就会出问题。这两个数字是可以根据密钥长
 *         度算出来的,所以不应该在代码中写固定数字,而是获取密钥长度将它们算出来,这样更换密钥就不需要修改代码了。
 */
class RSA
{
    private static $publicKeyPath = __DIR__ . '/rsa_public_key.pem'; // 公钥文件路径

    private static $privateKeyPath = __DIR__ . '/rsa_private_key.pem'; // 私钥文件路径

    private static $signatureAlgo = OPENSSL_ALGO_SHA512; // 签名算法

    private static $padding = OPENSSL_PKCS1_OAEP_PADDING; // 加密算法使用的填充方案,强烈建议使用“OPENSSL_PKCS1_OAEP_PADDING”

    // 使用不同填充方案占用的字节数
    private static $pkcs1 = [
        OPENSSL_PKCS1_PADDING => 11, // 使用OPENSSL_PKCS1_PADDING填充需占用11字节
        OPENSSL_PKCS1_OAEP_PADDING => 42, // 使用OPENSSL_PKCS1_OAEP_PADDING填充需占用42字节
    ];

    /**
     * (使用公钥)加密
     * 备注:相同的数据每次加密产生的密文都是不同的。
     *
     * @param string $data 要加密的数据
     * @return string 数据加密后的密文
     */
    public static function encrypt($data)
    {
        $encrypt = ''; // 用于保存密文的变量

        // 获取公钥的内容(即字符串)
        $publicKeyContents = file_get_contents(self::$publicKeyPath);

        // 获取公钥的资源(获取失败返回FALSE)
        $publicKeyResource = openssl_pkey_get_public($publicKeyContents);

        // 获取密钥长度,并根据密钥长度算出可加密最大数据长度
        $bits = self::getKeyBits($publicKeyResource); // 密钥长度
        $max = $bits / 8 - self::$pkcs1[self::$padding]; // 可加密最大数据长度(受填充方案影响)

        // 按照可加密最大数据长度对数据进行切割,然后分段加密,从而实现超长数据加密
        $dataSplit = str_split($data, $max);
        foreach ($dataSplit as $split) {
            $splitEncrypt = '';
            openssl_public_encrypt($split, $splitEncrypt, $publicKeyResource, self::$padding); // 加密
            $encrypt .= $splitEncrypt;
        }

        self::freeKey($publicKeyResource);

        return base64_encode($encrypt);
    }

    /**
     * (使用私钥)解密
     *
     * @param string $data 要解密的数据
     * @return string 数据解密后的明文
     */
    public static function decrypt($data)
    {
        $decrypt = ''; // 用于保存明文的变量

        // 获取私钥的内容(即字符串)
        $privateKeyContents = file_get_contents(self::$privateKeyPath);

        // 获取私钥的资源(获取失败返回FALSE)
        $privateKeyResource = openssl_pkey_get_private($privateKeyContents);

        // 加密是分段的,解密也要分段解
        $bits = self::getKeyBits($privateKeyResource); // 密钥长度
        $max = $bits / 8;

        $data = base64_decode($data); // 加密方法对密文进行了base64_encode()处理,故这里要进行逆操作
        $dataSplit = str_split($data, $max);
        foreach ($dataSplit as $split) {
            $splitDecrypt = '';
            openssl_private_decrypt($split, $splitDecrypt, $privateKeyResource, self::$padding); // 解密
            $decrypt .= $splitDecrypt;
        }

        self::freeKey($privateKeyResource);

        return $decrypt;
    }

    /**
     * (使用私钥)生成签名
     * 备注:相同的数据每次生成的签名都是相同的。
     *
     * @param string $data 要生成签名的数据
     * @return string 签名字符串
     */
    public static function signature($data)
    {
        // 获取私钥的内容(即字符串)
        $privateKeyContents = file_get_contents(self::$privateKeyPath);

        // 获取私钥的资源(获取失败返回FALSE)
        $privateKeyResource = openssl_pkey_get_private($privateKeyContents);

        $signature = '';

        openssl_sign($data, $signature, $privateKeyResource, self::$signatureAlgo);

        self::freeKey($privateKeyResource);

        return base64_encode($signature);
    }

    /**
     * (使用公钥)验证签名
     *
     * @param string $data 数据(必须要和生成签名时的数据一样。PS;签名就是防止伪造数据)
     * @param string $signature 签名字符串
     * @return int 验证结果:1=签名正确,0=签名错误,-1=内部发生错误
     */
    public static function signatureVerify($data, $signature)
    {
        // 获取公钥的内容(即字符串)
        $publicKeyContents = file_get_contents(self::$publicKeyPath);

        // 获取公钥的资源(获取失败返回FALSE)
        $publicKeyResource = openssl_pkey_get_public($publicKeyContents);

        $signature = base64_decode($signature);

        $verify = openssl_verify($data, $signature, $publicKeyResource, self::$signatureAlgo);

        self::freeKey($publicKeyResource);

        return $verify;
    }

    /**
     * 获取密钥(公钥或私钥均可)的长度
     *
     * @param resource $key OpenSSLAsymmetricKey实例(即公钥或私钥资源)
     * @return int|false 密钥(公钥或私钥均可)的长度,若获取失败则返回false
     */
    private static function getKeyBits($key)
    {
        $details = openssl_pkey_get_details($key);
        if ($details === false) {
            return false;
        }

        return $details['bits'];
    }

    /**
     * 释放密钥资源
     *
     * @param resource $key 参数说明
     * @return void
     */
    private static function freeKey($key)
    {
        // openssl_free_key()函数已自 PHP 8.0.0 起被废弃
        if (PHP_VERSION_ID < 80000) {
            openssl_free_key($key);
        }
    }
}

$data = '北国风光,千里冰封,万里雪飘。';
$data .= '望长城内外,惟余莽莽;大河上下,顿失滔滔。';
$data .= '山舞银蛇,原驰蜡象,欲与天公试比高。';
$data .= '须晴日,看红装素裹,分外妖娆。';
$data .= '江山如此多娇,引无数英雄竞折腰。';
$data .= '惜秦皇汉武,略输文采;唐宗宋祖,稍逊风骚。';
$data .= '一代天骄,成吉思汗,只识弯弓射大雕。';
$data .= '俱往矣,数风流人物,还看今朝。';

echo "数据:$data" . PHP_EOL; // 数据:北国风光,千里冰封,万里雪飘。望长城内外,惟余莽莽;大河上下,顿失滔滔。山舞银蛇,原驰蜡……

// 备注:由于这里只是简单演示如何使用RSA加解密,故原始数据简单粗暴地使用一串字符串,实际使用中原始数据都是数组格式,开发者需要按照规
//       则将数组转成字符串,通常规则都是将数组进行参数名ASCII字典序排序,然后转成“参数1=参数值1&参数2=参数值2&参数3=参数值3”格式的
//       字符串,然后将字符串进行base64_encode()处理,最后将base64_encode()得出的字符串加密即可,代码如下:
//       ksort($data);
//       $dataStr = urldecode(http_build_query($data));
//       $dataEncode = base64_encode($dataStr);
//       $encrypt = RSA::encrypt($dataEncode);

// 对原始数据进行加密(备注:相同的数据每次加密产生的密文都是不同的。)
$encrypt = RSA::encrypt($data);
echo "密文:$encrypt" . PHP_EOL; // 密文:HTKJpal5qVXNxI2XBeHPgYF1dUVYNepLlbshQt8CDIRe11doDIf0mjRLQdTeeFQVIBh4mfabrELXs0bbha……

// 对密文进行解密
$decrypt = RSA::decrypt($encrypt);
echo "明文:$decrypt" . PHP_EOL; // 明文:北国风光,千里冰封,万里雪飘。望长城内外,惟余莽莽;大河上下,顿失滔滔。山舞银蛇,原……

// 生成签名(备注:相同的数据每次生成的签名都是相同的。)
$signature = RSA::signature($data);
echo "签名:$signature" . PHP_EOL; // 签名:jJ5bNkNNCF17mOatDkYPGj/nT9FFdg+zmaYWepIgEWOO4QDAy8qLBirsF4Y6IjYwHSJ0X7bDkvFhlbY2……

// 验证签名,验证结果:1=签名正确,0=签名错误,-1=内部发生错误
$verify = RSA::signatureVerify($data, $signature);
echo '验签:' . ($verify === 1 ? '合法' : '非法') . PHP_EOL; // 验签:合法

//========== 总结 ==========//
// 1、使用公钥加密可以通过私钥解密,使用私钥加密也可以通过公钥解密。但是使用公钥加密不能通过公钥解密,如果公钥加密还能公钥解密就不是
//    非对称加密了,而且最根本在于如果能这样做那加密将毫无意义,因为公钥是公开的,这意味着任何人都能解密获得消息原文。
// 2、使用公钥加密每次得到的密文都是不同的,而使用私钥加密每次得到的密文都是相同的。
// 3、公钥和私钥是一对一的关系,即一个公钥有且只有一个与之对应的私钥,一个私钥有且只有一个与之对应的公钥。

Copyright © 2024 码农人生. All Rights Reserved