[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/ 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM