zoukankan      html  css  js  c++  java
  • [Node.js] 使用node-forge保障Javascript应用的传输安全

    原文地址:http://www.moye.me/2015/12/19/protect_jsapp_tsl_by_using_node-forge/

    引子

    半年前的最后一次更新(惭愧  ),提到了对称与非对称的混合加解密系统,点到为止而未涉及实践,今次将就此展开,再续爱丽丝与鲍伯的前缘。

    Asymmetric_cryptography_-_step_2.svg

    为什么

    Javascript应用的安全传输,这事不是有SSL吗,为什么要多此一举呢? 好问题,自己实现TSL是因为:

    • SSL是基于浏览器的,是浏览器负责的安全性,这意味着,非浏览器应用得不到保护,即使你的服务器上安装了证书
    • SSL需要有明确的CA受信端,自颁发证书的对等端应用,显然是使用不能

    那么,这样的场景是需要自实现 TSL 的:一台node服务器 和 一个electron桌面应用 之间的安全通信

    怎么做

    SSL是怎么做的呢? 它的流程和之前提到的混合密码系统是一样:公私钥和对称算法混合应用,在安全和性能之间取得平衡(更为具体的流程请参见 HTTPS/SSL原理及Ruby实现)。

    用啥做

    既然要保障的是Javascript应用的传输安全,那自然需要在npm上网罗一下,感谢社区,让我找到了 node-forge 。它是这么介绍自己的:

    JavaScript implementations of network transports, cryptography, ciphers, PKI, message digests, and various utilities.

    功能似乎很全,然后,关于性能,这货在评测中表现颇为优秀:

    任何加解密都需要操作 bytes,在node-forge里,也提供了一个byte buffer的实现:ByteStringBuffer,inspect一下,是不是很眼熟:

    forge-ByteStringBuffer

    更多的介绍可参见 官方repo文档 和 Node.js加解密

    动手实践

    需求

    有一个页面,通过ajax的方式与node的后端通信,我希望能把各种请求(GET/POST/PUT/DELETE)里的某个参数加密(假设为id好了)

    准备工作

    颁发一对公私钥:

    openssl genrsa -out prv.key 
    openssl rsa -in prv.key -pubout > pub.key

    公钥给页面用来做加密:

    -----BEGIN PUBLIC KEY-----
    MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKPLxKHePPC3OusMSYqPymDuuUpojM00
    OF66DkWizWoein2b7n/lTUqwtiEkwY0ZZkDvktfjijpiDJMp4xscN18CAwEAAQ==
    -----END PUBLIC KEY-----

    私钥给后端用来解密:

    -----BEGIN RSA PRIVATE KEY-----
    MIIBPAIBAAJBAKPLxKHePPC3OusMSYqPymDuuUpojM00OF66DkWizWoein2b7n/l
    TUqwtiEkwY0ZZkDvktfjijpiDJMp4xscN18CAwEAAQJBAJsMZ3zmV39xoxcekXrV
    dDhfogw6fZY96WJZ8uqeGp5o+E8kIiwSvVPJJ/ktntSeGdz82BKip6CB7Pw28iuM
    CiECIQDZESt+cflsbSLydOX8Ioo4PGDw7ftJT4YMlUHvIaOZDwIhAMEsm4v0CSm5
    4sXODT546WrnrCECk7Yi1pAwqcSmIlyxAiAyvejE7i+4QOricqEwh4J4EuU2bOtI
    /+X+GwYGuH5d0QIhAITnDMk4B4nWoweWIRSHGYh8hbdcT4Xy6A3h/RsXdfKxAiEA
    luBD4h2dSlbNwjFyb3bRW+1Kc4PbMFOPCX6ip5PGFQ4=
    -----END RSA PRIVATE KEY-----

    页面端

    先clone一份node-forge源码,安装完依赖,再生成bundle:

    git clone https://github.com/digitalbazaar/forge && cd forge
    npm install
    npm run bundle

    然后就能得到一个完整的forge包:js/forge.bundle.js,在页面中引用它:

    <script src="js/forge.bundle.js"></script>
    <script src="js/jquery.min.js"></script> //jQuery 假定你也是用的

    一如之前设计的,我们要山寨的TSL是基于RSA+AES的混合方案,纵然有node-forge这么老卵的包,也还是需要自己写点工具方法的:

    //aes对称加密,返回: 密文/key/iv
    function _encrypt_by_aes(message) {
            var key = forge.random.getBytesSync(32);
            var iv = forge.random.getBytesSync(32);
            var cipher = forge.cipher.createCipher('AES-CBC', key);
            cipher.start({iv: iv});
            cipher.update(forge.util.createBuffer(message));
            cipher.finish();
            var encrypted = cipher.output;
            return {encrypted: encrypted, key: key, iv: iv};
    }
    //rsa公钥加密,传入: 公钥PEM形式
    function _encrypt_by_rsa(message, pubkey) {
            var pki = forge.pki;
            var publicKey = pki.publicKeyFromPem(pubkey);
            return publicKey.encrypt(message);
    }
    //序列化:把密文binary形式转成能够传输的hex形式
    function _serialize(obj) {
            return forge.util.bytesToHex(obj);
    }

    页面与后端的传输逻辑也就相对容易实施了:

    function _normalize(url, kvset) {
            var api_url = url + '?';
            for (var k in kvset) {
                api_url += k + '=' + kvset[k] + '&';
            }
            return api_url;
    } 
    function _transfer_wrapper(type) {
            return function(id, pubkey, url, cb_success, cb_err) {
                var _cipher = _encrypt_by_aes(id);
                var _cipher_id = _serialize(_cipher.encrypted);
                //后端对称解密id原文需要用到的的key和iv,被rsa加密后序列化传输:
                var _cipher_key = _serialize(_encrypt_by_rsa(_serialize(_cipher.key), pubkey));   
                var _cipher_iv = _serialize(_encrypt_by_rsa(_serialize(_cipher.iv), pubkey));
    
                var api_url = _normalize(access_url, {
                    key: _cipher_key,
                    iv: _cipher_iv,
                    id: _cipher_id
                });
    
                $.ajax({type: type, url: api_url, data: {}})
                    .done(cb_success)
                    .fail(cb_err);
            };
    }
    
    $.fn.API_GET = _transfer_wrapper('GET');
    $.fn.API_POST = _transfer_wrapper('POST');
    $.fn.API_PUT = _transfer_wrapper('PUT');
    $.fn.API_DELETE = _transfer_wrapper('DELETE');

    页面上如此调用:

    var pubkey = `-----BEGIN PUBLIC KEY-----
    MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKPLxKHePPC3OusMSYqPymDuuUpojM00
    OF66DkWizWoein2b7n/lTUqwtiEkwY0ZZkDvktfjijpiDJMp4xscN18CAwEAAQ==
    -----END PUBLIC KEY-----`;
    var id = 'b5a98f0a-73a2-403a-b6fe-a7cc712169a8'; //被加密的id
    var url = 'http://example.org/api';
    
    function success(data){
         console.log('SUCCESS', data);
    }
    function error(err){
         console.log('ERR', err);
    }
    
    $.fn.API_POST(id, pubkey, url, success, error);

    node.js 后端

    后端一切从简,假定web框架用的express 4.x,依然需要先安装node-forge包:

    npm install node-forge --save

    纵然老卵,工具方法还是要自己写的:

    var forge = require('node-forge');
    
    function _decrypt_by_aes(encrypted, key, iv) {
        var decipher = forge.cipher.createDecipher('AES-CBC', key);
        decipher.start({iv: iv});
        decipher.update(encrypted);
        decipher.finish();
        return decipher.output;    
    }
    function _decrypt_by_rsa(encrypted, prvkey) {
        var pki = forge.pki;
        var privateKey = pki.privateKeyFromPem(prvkey);
        return privateKey.decrypt(encrypted);
    }
    function _deserialize(hex) {
        var buffer = forge.util.hexToBytes(hex);
        return forge.util.createBuffer(buffer, 'raw');
    }
    
    var private_key = `-----BEGIN RSA PRIVATE KEY-----
    MIIBPAIBAAJBAKPLxKHePPC3OusMSYqPymDuuUpojM00OF66DkWizWoein2b7n/l
    TUqwtiEkwY0ZZkDvktfjijpiDJMp4xscN18CAwEAAQJBAJsMZ3zmV39xoxcekXrV
    dDhfogw6fZY96WJZ8uqeGp5o+E8kIiwSvVPJJ/ktntSeGdz82BKip6CB7Pw28iuM
    CiECIQDZESt+cflsbSLydOX8Ioo4PGDw7ftJT4YMlUHvIaOZDwIhAMEsm4v0CSm5
    4sXODT546WrnrCECk7Yi1pAwqcSmIlyxAiAyvejE7i+4QOricqEwh4J4EuU2bOtI
    /+X+GwYGuH5d0QIhAITnDMk4B4nWoweWIRSHGYh8hbdcT4Xy6A3h/RsXdfKxAiEA
    luBD4h2dSlbNwjFyb3bRW+1Kc4PbMFOPCX6ip5PGFQ4=
    -----END RSA PRIVATE KEY-----`;

    写个中间件,尝试解密请求并判定是否合法:

    function _check_api_request(req, res, next) {
         //...blabla
         try { //出错必定为非法请求
              var key = _deserialize(req.query.key);
              var iv = _deserialize(req.query.iv);
              var cipher_id = _deserialize(req.query.id);
              var cipher_k = _deserialize(_decrypt_by_rsa(key.data, private_key));
              var cipher_v = _deserialize(_decrypt_by_rsa(iv.data, private_key));
              //注入id
              req.id = _decrypt_by_aes(cipher_id, cipher_k, cipher_v);
         } catch(err) { return res.sendStatus(401); }
         
         return next();
    }

    小结

    至此,关于Javascript的TSL实践告一段落,其实上面的例子可以做得更鲁棒一些,加上CheckSum也是好的选择,有时间的话,择日将再整理一版跨语言的加解密方案。

    参考

    1. Node.js加解密 http://www.jianshu.com/p/85f152944527
    2. HTTPS/SSL原理及Ruby实现 http://foobar.me/2011/05/19/https-ssl-yuan-li-ji-ruby-shi-xian/

    更多文章请移步我的blog新地址: http://www.moye.me/ 

  • 相关阅读:
    silo 主机 报找不到 grain 实现错误的一个注意
    转:CRT注册
    Maven生命周期
    Maven学习笔记
    Java内存回收机制
    Selenium2.0和1.0的区别
    关于使用Selenium RC无法打开指定页面问题
    四儿子购买手册
    Objective-C 宏定义的收集
    设计模式:适配器模式
  • 原文地址:https://www.cnblogs.com/moye/p/protect_jsapp_tsl_by_using_node-forge.html
Copyright © 2011-2022 走看看