PHP生成RSA密钥及加解密的实现

发布时间 2023-10-13 15:24:54作者: azsd

学习记录 留作参考
祝君好运

使用PHP在win下生成私钥有诸多问题,需谨慎使用。

RSA算法单次加密的明文长度 <= 私钥长度。以1024bit私钥长度举例,其单次最多可加密的数据 1024/8=128byte

当明文长度小于私钥长度时,就需要使用padding。PHP默认使用的是 PKCS1Padding,长度占用11字节。

若使用padding,1024bit私钥单次最多可加密数据 128-11=117byte。2048bit私钥单次最多可加密数据 2048/8-11=245byte

加密生成的密文长度等于私钥长度。

如果明文长度超过了单次最大可加密数据的长度,就需要分段加密,拼接后再输出。同时也需要分段解密,拼接后即为明文。

加密时的单次最多可加密数据的长度可由私钥长度及padding类型计算得出。而解密的分段长度则是 密钥长度/8 byte

参考内容:
知乎:RSA算法明文长度介绍
PHP官方文档内的第一个 User Contributed Notes

<?php

new_key();
function new_key()
{
    /**
     * 在生成公钥私钥时,需要给一些基础参数,比如加密摘要算法、私钥长度位数。
     * 生成参数可参考:https://www.php.net/manual/zh/function.openssl-csr-new.php
     */
    $config = [
        "digest_alg" => "sha256",
        "private_key_bits" => 1024, // 私钥位数 1024 2048 4096 等
    ];

    /**
     * windows环境还需要补充自定义openssl.conf文件的路径。
     * PHP版本不同,配置起来也不同。
     * 
     * 具体的openssl安装,PHP官方文档给了解答:https://www.php.net/manual/zh/openssl.installation.php
     * 文档指出,从PHP7.4.0开始,openssl默认配置文件的路径已经发生改变。
     * 实测 PHP7.3 PHP5.4 即使配置了openssl.conf的路径也无法生成私钥。
     * 实测 PHP8.0 将 php安装路径\extras\ssl\openssl.cnf 复制到 C:\Program Files\Common Files\SSL之后可正常使用,并且不用配置config变量
     * 
     * 总之,在win下生成私钥可能问题比较多,谨慎使用。
     */
    if (PHP_OS === 'WINNT') {
        $config['config'] = 'C:\phpstudy_pro\Extensions\php\php7.3.4nts\extras\ssl\openssl.cnf';
    }

    $res = openssl_pkey_new($config); // 生成一个新的私钥

    if (false === $res) {
        exit("generate key fail.");
    }

    // 获取私钥
    openssl_pkey_export($res, $private_key);
    if (!$private_key) {
        exit("private key is null.");
    }

    // 获取公钥
    $public_key = openssl_pkey_get_details($res)['key'];
    var_dump($public_key);

    // 待加密数据
    $string = "使用PHP生成公钥和私钥分别实现加解密。传输数据时,使用公钥加密私钥解密。验证证书时,使用私钥加密公钥解密。";

    $encrypt_block_size = 117; // 加密时,单次(最大)加密数据字节数
    $decrypt_block_size = 128; // 解密时,单次解密数据字节数


    // 加密 --START
    $encrypted = '';
    $primary_data_arr = str_split($string, $encrypt_block_size); // 将待加密字符串按照长度分割成数组
    foreach ($primary_data_arr as $item) {
        // 使用私钥逐段加密,然后拼接。加密后的数据长度为密钥长度
        $bool = openssl_private_encrypt($item, $part_encrypted, $private_key, OPENSSL_PKCS1_PADDING);
        if (false === $bool) {
            exit('encrypt is error.');
        }
        $encrypted .= $part_encrypted;
    }

    // 加密后的内容通常含有特殊字符,需要base64编码转换下
    $encrypted = base64_encode($encrypted);
    echo "私钥加密后的数据:{$encrypted}\r\n";
    // 加密 --END


    // 解密 --START
    // 因为一般待解密的数据是经过base64转码的,所以需要再base64转回去来获取原始加密数据
    // 因为加密数据可能是多次加密结果拼接而成,所以也需要分段解密再拼接成原始数据
    // 解密时每段数据的长度(即单次加密的密文长度)等于密钥长度。如:密钥长度1024位,单次解密128字节
    $encrypted_data_arr = str_split(base64_decode($encrypted), $decrypt_block_size);

    $decrypted = '';
    foreach ($encrypted_data_arr as $val) {
        //公钥解密
        openssl_public_decrypt($val, $part_decrypted, $public_key);
        $decrypted .= $part_decrypted;
    }
    echo "公钥解密后的数据:{$decrypted}\r\n";
    // 解密 --END


    //// 公钥加密,私钥解密 方法一致:加密的时候分段,解密的时候也分段
}