RSA加密:使用 phpseclib 和 NodeRSA 做到前后端加密通信

目前,要在 php 中使用 RSA 对数据加密,大家一般采用比较成熟的开源框架 phpseclib。而有些时候,为了确保数据安全,我们需要在前端进行数据加密,传到后台再解密,这种情况下,前端加密框架和后台框架必须要配合好才行。
我的博客主要就是探讨配合的问题。

目录

  • 需要的环境
  • 开始
    • 使用 Composer 下载 phpseclib
    • 使用 NPM 下载 JQuery, NodeRSA 和 browserify
    • 创建需要用到的文档
  • 使用 phpseclib 生成密钥对
  • 在前端进行加密
  • 在后台进行解密
  • 使用 browserify 打包 js 文件
  • NodeRSA 和 phpseclib 配合的难点总结
  • Q & A

需要的环境

  • Node.js
  • Composer
  • browserify(考虑到来看这篇教程的人一般不会用 Node.js 搭建服务器,所以我用 browserify 打包来让代码可以作为普通 js 代码使用)

开始

使用 Composer 下载 phpseclib

下载了 composer 后,先在项目文件夹内创建 composer.json,内容如下:

{
    "require":{
        "phpseclib/phpseclib":"~2.0" // "包名称":"版本",具体用法请参见官方文档
    }
}

然后在控制台中输入(本文所有控制台指令均须在项目文件夹目录下进行,运行这些指令前请先在控制台中转到项目文件夹目录):

composer install

这样 composer 就会自动下载好 phpseclib 和它的所有依赖了。

使用 NPM 下载 JQuery, NodeRSA 和 browserify

打开控制台,先初始化项目文件夹:

npm init

然后会选择一系列跟包有关的配置,没有特殊需求直接按 Enter 跳过就行。
然后下载 JQuery(出于方便考虑):

npm install jquery

和 NodeRSA:

npm install node-rsa

以及 browserify。browserify 比较特殊,需要全局安装:

npm install -g browserify

到这里,环境配置结束。

创建需要用到的文档

出于演示目的,我创建了以下几个文档:

  • inde.html,这是主页,只有 HTML 代码
  • js.js,这是前端要用的 JS 代码
  • rsa.php,后台代码

此时我的项目文件是这样的:

├─ node_modules     // 这是 NPM 生成的文件夹,它下载的东西(除了全局下载)都在里面
├─ vendor           // 这是 Composer 生成的文件夹,同上
├─ composer.json
├─ composer.lock
├─ index.html
├─ js.js
├─ package.json
├─ pakage-lock.json
└─ rsa.php

其中,index.html:




    
    
    TestRSA





使用 phpseclib 生成密钥对

虽然 NodeRSA 也有生成密钥对的功能,但是出于安全考虑,建议始终在后台生成密钥对,然后前端通过 AJAX 拿到公钥再加密。
要在后台使用 phpseclib,先要在文件中包含 autoload.php 这个文件——这个文件由 Composer 下载,在 /vendor 目录下——然后引用 RSA 这个类:


完成后,新建一个 RSA 对象,使用 createKey($bits = 1024, $timeout = false, $partial = array()) 这个方法来创建密钥对。三个参数分别指示密钥的大小、是否延迟生成、延迟生成密钥的配置。后两个参数本文中不会使用。密钥大小现在一般使用2048位,使用这个方法会返回包含
创建密钥如下:

createKey(2048);
echo "Public Key:
" . $keyPair['publickey'] . "
Private Key:
" . $keyPair['privatekey']; ?>

在浏览器中运行一下,就得到了密钥对:

Public Key:
-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC1AJIs2Clk9hN00YxyH1tabFFa a5eytymq7fqGam+SToPfMrAZ1yu+N7O02L7QCftugz3bzS9B44yQ5/fc/qNIqPJt Gvwu1su7inA5peif19sA7QxDzFxH4wZWMXHUBxzP/iJC5pa3+2ufvpCIUrpMc/Qd JeN9l92DKYQvVoJqLwIDAQAB -----END PUBLIC KEY-----
Private Key:
-----BEGIN RSA PRIVATE KEY----- MIICXAIBAAKBgQC1AJIs2Clk9hN00YxyH1tabFFaa5eytymq7fqGam+SToPfMrAZ 1yu+N7O02L7QCftugz3bzS9B44yQ5/fc/qNIqPJtGvwu1su7inA5peif19sA7QxD zFxH4wZWMXHUBxzP/iJC5pa3+2ufvpCIUrpMc/QdJeN9l92DKYQvVoJqLwIDAQAB AoGAUjYjLrkz/AaFCc9P8lnpvWVrgh1SdrsY8ulKnBjl+lctMIiuUd5YoPpd5mt4 J7gQ+r4jr50/tLatFvg1rav+73M5VGBoyRGY+8GQn1prHqYaLDfmEvzhIRJMWR28 EoCWeZz3EesJzS28L/VL4g/xU0fC+eOgIvMP/9p5cjQK1KkCQQDYFQFj6tma4CYY DlZAiyvt8JmnyRKaf35Bfn5jhsIrSIB/JKPzfdPOUXUx056h3AgQfJFmiHhukKFd GVI9oEVbAkEA1nCQU/4q8aYrSmxE8mT8ZVZtRdhCWAUsmy4dQh8dnjWpiR2j4piT VsZ4744FmYzJoSuyHbdHAsN6f5BqIpWCvQJAPy+FRI58K0m4Wfh3SFBp/B4LEIE/ q2X0qXovwzK4vKdqy8MPHjiBIye2uWdET9hjk06Zxf3KGaq5RNwOrG6dWQJAQ8sc ZeFx1Cmzf/jQp00z2hnDvBQNjWQ9YOXoTBIoO+89JOMg/686zXE7pIhiztOKnxCA ZBKgOYhxPsj6NOrNhQJBAIBQX2TkpnzSQPxTwFOTE1We9Cc7/gald58lbHkpXHwC eU7D80kQAxDO2bxQTKTJgHUvylmGfdNwuYF/Rj9KHBM= -----END RSA PRIVATE KEY-----

将这个密钥对保存在后台,当前端需要使用公钥时,通过 AJAX 请求,再将公钥传递过去。

在前端进行加密

首先,我们要引用 NodeRSAJQuery 这两个包:

const NodeRSA = require("node-rsa");
const $ = require("jquery");

然后使用 AJAX 得到公钥,并使用这个公钥加密数据:

const NodeRSA = require("node-rsa");
const $ = require("jquery");
/******/
var plaintext = "123456"; // 待加密的数据,即明文
$.ajax({
    ... // 假设已获取到公钥
    "success":function(returnedData){
        var publicKey = returnedData; // 这就是公钥
    }
});

在加密之前,我们要知道以下几点:

  • AJAX 请求是异步的,且它自成一个作用域,也就是通过 AJAX 获取数据后,必须要在 success 的那个方法里面进行以后的操作,否则是得不到 publicKey 这个变量的
  • NodeRSA 默认的加密模式是 pkcs1-oaep,而 phpseclib 默认生成密钥采用的加密模式是 pkcs1,所以要先设置 NodeRSA 的加密模式
  • NodeRSA 加密后生成的是二进制文本,要先将其编码成 base64 再传递到后台
  • 使用 GET 方法传递密文时,由于该方法会将密文放在 URL 中,且 Base64 编码含有特殊字符,所以为了传参时不会丢失数据,需对加密后的密文进行 URL Encode
  • phpseclib 生成的公钥是 Base64 编码,采用 pkcs8 的 Padding 格式,在 NodeRSA 中对应 pkcs8-public-pem 这个格式

明确这几点后,就可以开始写加密用代码了:

const NodeRSA = require("node-rsa");
const $ = require("jquery");
/******/
var plaintext = "123456"; // 待加密的数据,即明文
$.ajax({
    ... // 假设已获取到公钥
    "success":function(returnedData){
        var publicKey = returnedData; // 这就是公钥
        /*******/
        var rsa = new NodeRSA();
        rsa.setOptions({"encryptionSchema":"pkcs1"}); // 设置加密模式
        rsa.loadKey(publicKey, "pkcs8-public-pem"); // 记得设置公钥格式
        var ciphertext = rsa.encrypt(plaintext, "base64"); // 加密,第二个参数就是编码方式
        ciphertext = encodeURIComponent(ciphertext); // URL 编码
    }
});

此时我们就完成了加密过程。现在我们把加密后的数据发送到后台进行解密。使用 AJAX 发送数据时,推荐使用 POST 方法,因为有时候得到的密文十分长,会超过 GET 方法的最大长度限制:

const NodeRSA = require("node-rsa");
const $ = require("jquery");
/******/
var plaintext = "123456"; // 待加密的数据,即明文
$.ajax({
    ... // 假设已获取到公钥
    "success":function(returnedData){
        var publicKey = returnedData; // 这就是公钥
        /*******/
        var rsa = new NodeRSA();
        rsa.setOptions({"encryptionSchema":"pkcs1"}); // 设置加密模式
        rsa.importKey(publicKey, "pkcs8-public-pem"); // 记得设置公钥格式
        var ciphertext = rsa.encrypt(plaintext, "base64"); // 加密,第二个参数就是编码方式
        ciphertext = encodeURIComponent(ciphertext); // URL 编码
        /********/
        $.ajax({
            "url":"rsa.php",
            "type":"POST",
            "data":{
                "ciphertext":ciphertext
            }
        });
    }
});

在后台进行解密

解密之前,我们也要先明确几点:

  • phpseclib 解密方法的参数(即密文)要是一个二进制字符串
  • 传递过去的密文要先进行 URL Decode

然后我们可以进行解密了:

loadKey($privateKey); // 加载密钥
$plaintext = $rsa->decrypt($ciphertext); // 解密
echo $plaintext;
?>

到此,整个解密过程结束。*而如果解密失败,phpseclib 似乎只是单纯地返回一个空字符串(未证实)

使用 browserify 打包 js 文件

现在所有代码已经完成,但 js.js 中包含只有在 Node.js 服务器中才能使用的代码,为了能让它在普通服务器中也能使用,我们使用 browserify 进行打包:

browserify js.js -o js.bundle.js

控制台运行这段代码,browserify 会找到目录下的 js.js,根据引用代码,将 js.js 和它所引用的包一起打包成 js.bundle.js 并生成到当前目录。-o 之前是要打包的文件,之后是打包后生成的文件,两项都是相对路径。
运行完成后,在 index.html 中引用打包好的文件:


就可以像平常使用其他 js 文件一样使用刚刚写好的代码了。

NodeRSA 和 phpseclib 配合的难点总结

根据以上内容,可以看出 NodeRSA 和 phpseclib 之间不同的有这几点:

  • NodeRSA 默认采用 pkcs1-oaep 加密模式,而 phpseclib 默认采用 pkcs1 模式。本文是改变了 NodeRSA 的设置,理论上也可以改变 phpseclib 设置

  • phpseclib 生成的密钥对中,公钥使用的是 pkcs8 格式,私钥使用 pkcs1 格式,但文档中对它们的描述都是 pkcs1 格式,而 NodeRSA 对这两个格式是严格区分的,需要明确

  • 由于无论是密钥还是文本中都可能有特殊符号,传递参数前必须要 URL Encode,传参后必须要 URL Decode,否则部分数据会丢失,当初这个问题困扰了我很久

  • NodeRSA 可自由设置加密后文本的编码方式,但设置成 binary 时,生成的并不是二进制文本,而是直接的二进制。目前我还没有找到 AJAX 传递二进制的方法

  • NodeRSA 导入密钥时一定要指定格式。关于格式的详细信息在 NodeRSA 的 Github 页面上有,我这里转载翻译一下:

    Format string syntax
    Format string composed of several parts: scheme-[key_type]-[output_type]


    Scheme — NodeRSA supports multiple format schemes for import/export keys:
    'pkcs1' — public key starts from '-----BEGIN RSA PUBLIC KEY-----' header and private key starts from '-----BEGIN RSA PRIVATE KEY-----' header
    'pkcs8' — public key starts from '-----BEGIN PUBLIC KEY-----' header and private key starts from '-----BEGIN PRIVATE KEY-----' header
    'components' — use it for import/export key from/to raw components (see example below). For private key, importing data should contain all private key components, for public key: only public exponent (e) and modulus (n). All components (except e) should be Buffer, e could be Buffer or just normal Number.


    Key type — can be 'private' or 'public'. Default 'private'


    Output type — can be:
    'pem' — Base64 encoded string with header and footer. Used by default.
    'der' — Binary encoded key data.

    Notice: For import, if keyData is PEM string or buffer containing string, you can do not specify format, but if you provide keyData as DER you must specify it in format string.

    格式文本语法
    格式文本由几部分组成:scheme-[key_type]-[output_type]


    Scheme — NodeRSA 支持多种导入/导出密钥的方案:
    'pkcs1' — 公钥以 '-----BEGIN RSA PUBLIC KEY-----' 头部开始,私钥以 '-----BEGIN RSA PRIVATE KEY-----' 头部开始
    'pkcs8' — 公钥以 '-----BEGIN PUBLIC KEY-----' 头部开始,私钥以 '-----BEGIN PRIVATE KEY-----' 头部开始
    'components' — 当导入/导出一个原始键组[1](参照下方例子)[2]时使用该方案。对于私钥,导入的数据应该包含所有私钥原始键,对于公钥,只需提供指数(e)和系数(n)。所有原始键(除了指数)都应该是 Buffer 类型,指数可以是 Buffer 也可以是普通的数字。


    Key type — 可以是 'private' 或 'public',默认是 'private'


    Output type — 可以是:
    'pem' — Base64编码的文本,带有密钥头部和尾部。默认使用该类型。
    'der' — 二进制编码的密钥数据。

    注意:导入的时候,如果密钥数据是 PEM 文本或含有 buffer 的文本,你可以不指定格式,但是如果你使用二进制提供密钥数据则你必须在格式文本中指定。

Q & A

你在评论区的问题我会更新在这里,并通过评论通知你
全文完。


  1. 译注:原始键组(Raw Components)这个译名并没有经过论证,不保证准确性和严谨性。就原文而言,它指的是 RSA 算法步骤中的6个关键数字。具体请参照:RSA算法原理(二)- 阮一峰 七、RSA 算法的可靠性。 ↩

  2. 译注:本文并没有转载该例,如有需要,请移步 Github • rzcoder/node-rsa: Format String Syntax ↩

你可能感兴趣的:(RSA加密:使用 phpseclib 和 NodeRSA 做到前后端加密通信)