@octopus
2019-10-23T08:20:54.000000Z
字数 5954
阅读 3688
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"; // 24var 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>
<?phpopenssl_decrypt("加密后的数据", 'AES-192-CBC', '123456781234567812345678', 0, '1234567812345678');?>// 'AES-192-CBC'是解密方式,根据秘钥位数确定,24位秘钥用此方式
- SRA 公私钥虽安全,但加密长度过大的字符串耗时很长
- AES 虽不安全,但擅长加密长度过大的字符串
前端代码已上传百度云:https://pan.baidu.com/s/1mJh28RFaHgcqUKY2mB6_yQ
后端如下:
<?phpnamespace 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);}}