@octopus
2019-10-23T16:20:54.000000Z
字数 5954
阅读 3420
php
有一个实名认证的需求,要求用户信息提交时不可明文传输,尝试了一天的RSA非对称加解密,苦逼的撞上了所有的坑,做个详细记录。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<!-- 引入加密必要的文件 -->
<script src="http://cdn.bootcss.com/jquery/3.0.0/jquery.min.js"></script>
<script src="https://passport.cnblogs.com/scripts/jsencrypt.min.js"></script>
<!-- 注意,这个文件需要下载后修改 【坑2】-->
<script src="http://www-cs-students.stanford.edu/~tjw/jsbn/base64.js"></script>
<!-- 自定义的文件 -->
<script src="function.js"></script>
<script>
// 1. 构造参数(json格式)
var json = {
"realname":"小王",
"age":18
}
// 2. 准备好公钥(因为是公钥,暴露在js中也完全ok,解密用的私钥在php端才有)
var pubkey='-----BEGIN PUBLIC KEY-----';
pubkey+='MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCvcaNOqy03ZEJyWbbjBZTnSaFkp2F85/Ha1ufAa15Fxp4Oi6pxzBh3YgruxHL8skMM1LjFlfcuyrvOw60kEjCFlfjSYxJZ1rhFnYK3mZFtpvreQrO2Xy2y2aIE6Zs6CMpFrpNvR7PUQ6de3I2j3CDqUV130pgPq/HdPY7jpx4tkQIDAQAB';
pubkey+='-----END PUBLIC KEY-----';
// 2. 调用加密函数,获得加密后的参数字符串
// 此函数封装在 function.js 中 【坑3】
var res = encrypt(pubkey, json);
document.write(res);
// 3. 将参数传给后端
var toPHP = {"data":res};
...
</script>
</body>
</html>
// function.js
// 此函数扩展了 encrypt 类的功能,增加了 encryptLong 成员方法,可以解密长字符串。
// 直接拷贝即可
function encrypt(pubkey, str){
JSEncrypt.prototype.encryptLong = function(string) {
var k = this.getKey();
try {
var lt = "";
var ct = "";
//RSA每次加密117bytes,需要辅助方法判断字符串截取位置
//1.获取字符串截取点
var bytes = new Array();
bytes.push(0);
var byteNo = 0;
var len,c;
len = string.length;
var temp = 0;
for(var i = 0; i < len; i++){
c = string.charCodeAt(i);
if(c >= 0x010000 && c <= 0x10FFFF){
byteNo += 4;
}else if(c >= 0x000800 && c <= 0x00FFFF){
byteNo += 3;
}else if(c >= 0x000080 && c <= 0x0007FF){
byteNo += 2;
}else{
byteNo += 1;
}
if((byteNo % 117) >= 114 || (byteNo % 117) == 0){
if(byteNo-temp >= 114){
bytes.push(i);
temp = byteNo;
}
}
}
//2.截取字符串并分段加密
if(bytes.length > 1){
for(var i=0;i< bytes.length-1; i++){
var str;
if(i == 0){
str = string.substring(0,bytes[i+1]+1);
}else{
str = string.substring(bytes[i]+1,bytes[i+1]+1);
}
var t1 = k.encrypt(str);
ct += t1;
};
if(bytes[bytes.length-1] != string.length-1){
var lastStr = string.substring(bytes[bytes.length-1]+1);
ct += k.encrypt(lastStr);
}
return hex2b64(ct);
}
var t = k.encrypt(string);
var y = hex2b64(t);
return y;
} catch (ex) {
return ex;
}
};
// 利用公钥加密
var encrypt = new JSEncrypt();
encrypt.setPublicKey(pubkey);
var encrypted = encrypt.encryptLong(JSON.stringify(str));
return encrypted;
}
$data = $_POST["data"]; // 获得加密过的数据
$arr = $this->RSAEncode($data); // 解密
$arr = json_decode($arr, true);
print_r($arr);
// 分段解密算法
private function RSAEncode($str){
$private_key = '-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCvcaNOqy03ZEJyWbbjBZTnSaFkp2F85/Ha1ufAa15Fxp4Oi6pxzBh3YgruxHL8skMM1LjFlfcuyrvOw60kEjCFlfjSYxJZ1rhFnYK3mZFtpvreQrO2Xy2y2aIE6Zs6CMpFrpNvR7PUQ6de3I2j3CDqUV130pgPq/HdPY7jpx4tkQIDAQABAoGAXRtVq340dN0EVmM+J6TirQvqVtxtZDhDpfu+6eMRGL6bSuTcA6Boq5Kgcq5Wx7xi0QBjPpijoXV9zfEShb4ReTIUyuaaiCC7zIhOQmH8nZVOiz6YLxvJwVOF3hNWi9rvusz0bBCWpe8XA/AnUR64QM+yzxXavy77yWWrnkbQj/ECQQDgzef24QUznQviQNa+U2nIyV/7eTNAAT6J3A/ObQIzJfdGTKLAC4q88q8viHEd2kItgaiwlD7rhrNQHxCij37XAkEAx8o4Y9maJ9UVuQ7dWdnipJp66deS5ysP9pG+oRRBiiAoKCg6/VjsAURIdST8hnGHMdEo9Mc3OitysqmmMFix1wJAFP8GQEep/bUYTAx0Qhh2U9uDDHC4eazkE2orS8NgpjhQjQ9RUBCD57ve4tiigABHmAp5H+zop4TQMJaGLjiqxwJBALY2VUWjwTeQ3JYB25lF4bHT/kPlS2Kv4Ig1llGcgsWx3E37mwlSnzPsjdThHbqUMgQaSastQc44sAwIM7ymN9MCQGECUqDAs5oHO98V937Br9Ou14tuRN2gw52dqBaBnme8Eszqg2Z2PbioWKogGJwCDHV2JnLHqdBnJ76R7eyrYck=
-----END RSA PRIVATE KEY-----';
$crypto = '';
$str = str_replace(array(" ",'-','_'),array("+",'+','/'), $str); // 关键
foreach (str_split(base64_decode($str), 128) as $chunk)
{
$res = openssl_private_decrypt($chunk, $decryptData, $private_key);
if(!$res){
return false;
}
$crypto .= $decryptData;
}
return str_replace('"','',$crypto);
}
长度越大,解密时间越长,但是长度小的,如 1024,只能加密解密很短的字符串,稍微多几个表单字段就gg,但是选了 2048也治标不治本,无非是比1024多了点长度。解决办法是 `分段加密`,`分段解密`,for循环几个字符一加密,最后拼起来就行啦,解密也是同理,所以密钥长度选最小的 1024 即可,无论字符串多大,我们都能分段加解密。
网上下载的 base64.js 中第一行定义编码字符是这样的:
var b64map="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
但是,亲测php接收到用它编码加密后的参数,再解密大概率失败。原因在于 + 和 / 这两个特殊符号可能解析会出现问题。解决办法:
// 前端修改 base64.js,将上面那个用来编码的字符串改为:
var b64map="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
// 后端转义时先替换:
$str = str_replace(array(" ",'-','_'),array("+",'+','/'), $str);
为什么要增加 encryptLong 这个成员方法呢,这也是对 [坑1] 的补充,即如何分段加密。
<script src="core.js"></script>
<script src="aes.js"></script>
<script>
var send = "hi, world";
var key = "123456781234567812345678"; // 24
var vi = "1234567812345678"; // 16位加密偏移量,与后端一致
var key = CryptoJS.enc.Latin1.parse(key);
var iv = CryptoJS.enc.Latin1.parse(iv);
var aes_send = CryptoJS.AES.encrypt(send, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
adding: CryptoJS.pad.ZeroPadding
}).toString();
console.log(aes_send); // 加密后的数据
</script>
<?php
openssl_decrypt("加密后的数据", 'AES-192-CBC', '123456781234567812345678', 0, '1234567812345678');
?>
// 'AES-192-CBC'是解密方式,根据秘钥位数确定,24位秘钥用此方式
- SRA 公私钥虽安全,但加密长度过大的字符串耗时很长
- AES 虽不安全,但擅长加密长度过大的字符串
前端代码已上传百度云:https://pan.baidu.com/s/1mJh28RFaHgcqUKY2mB6_yQ
后端如下:
<?php
namespace lib\encryption;
class Encryption
{
private $private_key = '-----BEGIN RSA PRIVATE KEY-----
xxxxxxx自己去申请吧xxxxxxxx
-----END RSA PRIVATE KEY-----';
private $aes_vi = '1234567812345678';
public function RSADecode($str){
$crypto = '';
$str = str_replace(array(" ",'-','_'),array("+",'+','/'), $str);
foreach (str_split(base64_decode($str), 128) as $chunk)
{
$res = openssl_private_decrypt($chunk, $decryptData, $this->private_key);
if(!$res){
return false;
}
$crypto .= $decryptData;
}
return str_replace('"','',$crypto);
}
public function AESDecode($key, $data){
return openssl_decrypt($data, 'AES-192-CBC', $key, 0, $this->aes_vi);
}
}