敏感數據加密方案及實現

73 篇原創好文~
本文首發於政採雲前端團隊博客: 敏感數據加密方案及實現

前言

如今是大數據時代,須要收集大量的我的信息用於統計。一方面它給咱們帶來了便利,另外一方面一些我的信息數據在無心間被泄露,被非法分子用於推銷和黑色產業。javascript

2018 年 5 月 25 日,歐盟已經強制執行《通用數據保護條例》(General Data Protection Regulation,縮寫做 GDPR)。該條例是歐盟法律中對全部歐盟我的關於數據保護和隱私的規範。這意味着我的數據必須使用假名化或匿名化進行存儲,而且默認使用盡量最高的隱私設置,以免數據泄露。html

相信你們也都不想讓本身在外面「裸奔」。因此,做爲前端開發人員也應該儘可能避免用戶我的數據的明文傳輸,儘量的下降信息泄露的風險。前端

看到這裏可能有人會說如今都用 HTTPS 了,數據在傳輸過程當中是加密的,前端就不須要加密了。其實否則,我能夠在你發送 HTTPS 請求以前,經過谷歌插件來捕獲 HTTPS 請求中的我的信息,下面我會爲此演示。因此前端數據加密仍是頗有必要的。java

數據泄露方式

  • 中間人攻擊

    中間人攻擊是常見的攻擊方式。詳細過程能夠參見這裏。大概的過程是中間人經過 DNS 欺騙等手段劫持了客戶端與服務端的會話。node

    客戶端、服務端之間的信息都會通過中間人,中間人能夠獲取和轉發二者的信息。在 HTTP 下,前端數據加密仍是避免不了數據泄露,由於中間人能夠僞造密鑰。爲了不中間人攻擊,咱們通常採用 HTTPS 的形式傳輸。ios

  • 谷歌插件

    HTTPS 雖然能夠防止數據在網絡傳輸過程當中被劫持,可是在發送 HTTPS 以前,數據仍是能夠從谷歌插件中泄露出去。git

    由於谷歌插件能夠捕獲 Network 中的全部請求,因此若是某些插件中有惡意的代碼仍是能夠獲取到用戶信息的,下面爲你們演示。github

    因此光采用 HTTPS,一些敏感信息若是仍是以明文的形式傳輸的話,也是不安全的。若是在 HTTPS 的基礎上再進行數據的加密,那相對來講就更好了。算法

加密算法介紹

  • 對稱加密

    對稱加密算法,又稱爲共享密鑰加密算法。在對稱加密算法中,使用的密鑰只有一個,發送和接收雙方都使用這個密鑰對數據進行加密和解密。json

    這就要求加密和解密方事先都必須知道加密的密鑰。其優勢是算法公開、計算量小、加密速度快、加密效率高;缺點是密鑰泄露以後,數據就會被破解。通常不推薦單獨使用。根據實現機制的不一樣,常見的算法主要有AESChaCha203DES等。

  • 非對稱加密

    非對稱加密算法,又稱爲公開密鑰加密算法。它須要兩個密鑰,一個稱爲公開密鑰 (public key),即公鑰;另外一個稱爲私有密鑰 (private key),即私鑰。

    他倆是配對生成的,就像鑰匙和鎖的關係。由於加密和解密使用的是兩個不一樣的密鑰,因此這種算法稱爲非對稱加密算法。其優勢是算法強度複雜、安全性高;缺點是加解密速度沒有對稱加密算法快。常見的算法主要有RSAElgamal等。

  • 散列算法

    散列算法又稱散列函數、哈希函數,是把消息或數據壓縮成摘要,使得數據量變小,將數據的格式固定成特定長度的值。通常用於校驗數據的完整性,平時咱們下載文件就能夠校驗 MD5 來判斷下載的數據是否完整。常見的算法主要有 MD4MD5SHA 等。

實現方案

  • 方案一:若是用對稱加密,那麼服務端和客戶端都必須知道密鑰才行。那服務端勢必要把密鑰發送給客戶端,這個過程當中是不安全的,因此單單用對稱加密行不通。
  • 方案二:若是用非對稱加密,客戶端的數據經過公鑰加密,服務端經過私鑰解密,客戶端發送數據實現加密沒問題。客戶端接受數據,須要服務端用公鑰加密,而後客戶端用私鑰解密。因此這個方案須要兩套公鑰和私鑰,須要在客戶端和服務端各自生成本身的密鑰。

  • 方案三:若是把對稱加密和非對稱加密相結合。客戶端須要生成一個對稱加密的密鑰 1,傳輸內容與該密鑰 1進行對稱加密傳給服務端,而且把密鑰 1 和公鑰進行非對稱加密,而後也傳給服務端。服務端經過私鑰把對稱加密的密鑰 1 解密出來,而後經過該密鑰 1 解密出內容。以上是客戶端到服務端的過程。若是是服務端要發數據到客戶端,就須要把響應數據跟對稱加密的密鑰 1 進行加密,而後客戶端接收到密文,經過客戶端的密鑰 1進行解密,從而完成加密傳輸。

  • 總結:以上只是列舉了常見的加密方案。總的來看,方案二比較簡單,可是須要維護兩套公鑰和私鑰,當公鑰變化的時候,必須通知對方,靈活性比較差。方案三相對方案二來講,密鑰 1 隨時能夠變化,而且不須要通知服務端,相對來講靈活性、安全性好點而且方案三對內容是對稱加密,當數據量大時,對稱加密的速度會比非對稱加密快。因此本文采用方案三給予代碼實現。

代碼實現

  • 下面是具體的代碼實現(以登陸接口爲例),主要的目的就是要把明文的我的信息轉成密文傳輸。其中對稱加密庫使用的是 AES,非對稱加密庫使用的是RSA。
  • 客戶端:

    • AES 庫(aes-js):https://github.com/ricmoo/aes-js
    • RSA庫(jsencrypt):https://github.com/travist/js...
    • 具體代碼實現登陸接口

      • 客戶端須要隨機生成一個 aesKey,在頁面加載完的時候須要從服務端請求 publicKey

        let aesKey = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; // 隨機產生
        let publicKey = ""; // 公鑰會從服務端獲取
        
        // 頁面加載完以後,就去獲取公鑰
        window.onload = () => {
          axios({
            method: "GET",
            headers: { "content-type": "application/x-www-form-urlencoded" },
            url: "http://localhost:3000/getPub",
          })
            .then(function (result) {
              publicKey = result.data.data; // 獲取公鑰
            })
            .catch(function (error) {
              console.log(error);
            });
        };
      • aes加密和解密方法

        /**
         * aes加密方法
         * @param {string} text 待加密的字符串
*/
  function aesEncrypt(text, key) {
    const textBytes = aesjs.utils.utf8.toBytes(text); // 把字符串轉換成二進制數據
  
    // 這邊使用CTR-Counter加密模式,還有其餘模式能夠選擇,具體能夠參考aes加密庫
    const aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5));
  
    const encryptedBytes = aesCtr.encrypt(textBytes); // 進行加密
    const encryptedHex = aesjs.utils.hex.fromBytes(encryptedBytes); // 把二進制數據轉成十六進制
  
    return encryptedHex;
  }
  
  /**
   * aes解密方法
   * @param {string} encryptedHex 加密的字符串
   * @param {array} key 加密key
   */
  function aesDecrypt(encryptedHex, key) {
    const encryptedBytes = aesjs.utils.hex.toBytes(encryptedHex); // 把十六進制數據轉成二進制
    const aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5));
  
    const decryptedBytes = aesCtr.decrypt(encryptedBytes); // 進行解密
    const decryptedText = aesjs.utils.utf8.fromBytes(decryptedBytes); // 把二進制數據轉成utf-8字符串
  
    return decryptedText;
  }
  ```
- 請求登陸
  
    ```javascript
    /**
     * 登錄接口
     */
    function submitFn() {
      const userName = document.querySelector("#userName").value;
      const password = document.querySelector("#password").value;
      const data = {
        userName,
        password,
      };
    
      const text = JSON.stringify(data);
      const sendData = aesEncrypt(text, aesKey); // 把要發送的數據轉成字符串進行加密
      console.log("發送數據", text);
    
      const encrypt = new JSEncrypt();
      encrypt.setPublicKey(publicKey);
      const encrypted = encrypt.encrypt(aesKey.toString()); // 把aesKey進行非對稱加密
    
      const url = "http://localhost:3000/login";
      const params = { id: 0, data: { param1: sendData, param2: encrypted } };
    
      axios({
        method: "POST",
        headers: { "content-type": "application/x-www-form-urlencoded" },
        url: url,
        data: JSON.stringify(params),
      })
        .then(function (result) {
          const reciveData = aesDecrypt(result.data.data, aesKey); // 用aesKey進行解密
          console.log("接收數據", reciveData);
        })
        .catch(function (error) {
          console.log("error", error);
        });
    }
    ```
  • 服務端(Node):

    • AES庫(aes-js):https://github.com/ricmoo/aes-js
    • RSA 庫(node-rsa):https://github.com/rzcoder/no...
    • 具體代碼實現登陸接口

      • 引用加密庫

        const http = require("http");
        const aesjs = require("aes-js");
        const NodeRSA = require("node-rsa");
        const rsaKey = new NodeRSA({ b: 1024 }); // key的size爲1024位
        let aesKey = null; // 用於保存客戶端的aesKey
        let privateKey = ""; // 用於保存服務端的公鑰
        
        rsaKey.setOptions({ encryptionScheme: "pkcs1" }); // 設置加密模式
      • 實現login接口

        http
          .createServer((request, response) => {
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setHeader("Access-Control-Allow-Headers", "Content-Type");
            response.setHeader("Content-Type", "application/json");
            switch (request.method) {
              case "GET":
                if (request.url === "/getPub") {
                  const publicKey = rsaKey.exportKey("public");
                  privateKey = rsaKey.exportKey("private");
                  response.writeHead(200);
                  response.end(JSON.stringify({ result: true, data: publicKey })); // 把公鑰發送給客戶端
                  return;
                }
                break;
              case "POST":
                if (request.url === "/login") {
                  let str = "";
                  request.on("data", function (chunk) {
                    str += chunk;
                  });
                  request.on("end", function () {
                    const params = JSON.parse(str);
                    const reciveData = decrypt(params.data);
                    console.log("reciveData", reciveData);
                    // 一系列處理以後
        
                    response.writeHead(200);
                    response.end(
                      JSON.stringify({
                        result: true,
                        data: aesEncrypt(
                          JSON.stringify({ userId: 123, address: "杭州" }), // 這個數據會被加密
                          aesKey
                        ),
                      })
                    );
                  });
                  return;
                }
                break;
              default:
                break;
            }
            response.writeHead(404);
            response.end();
          })
          .listen(3000);
      • 加密和解密方法

        function decrypt({ param1, param2 }) {
          const decrypted = rsaKey.decrypt(param2, "utf8"); // 解密獲得aesKey
          aesKey = decrypted.split(",").map((item) => {
            return +item;
          });
        
          return aesDecrypt(param1, aesKey);
        }
        
        /**
         * aes解密方法
         * @param {string} encryptedHex 加密的字符串
*/
  function aesDecrypt(encryptedHex, key) {
    const encryptedBytes = aesjs.utils.hex.toBytes(encryptedHex); // 把十六進制轉成二進制數據
    const aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5)); // 這邊使用CTR-Counter加密模式,還有其餘模式能夠選擇,具體能夠參考aes加密庫
  
    const decryptedBytes = aesCtr.decrypt(encryptedBytes); // 進行解密
    const decryptedText = aesjs.utils.utf8.fromBytes(decryptedBytes); // 把二進制數據轉成字符串
  
    return decryptedText;
  }
  
  /**
   * aes加密方法
   * @param {string} text 待加密的字符串
   * @param {array} key 加密key
   */
  function aesEncrypt(text, key) {
    const textBytes = aesjs.utils.utf8.toBytes(text); // 把字符串轉成二進制數據
    const aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5));
  
    const encryptedBytes = aesCtr.encrypt(textBytes); // 加密
    const encryptedHex = aesjs.utils.hex.fromBytes(encryptedBytes); // 把二進制數據轉成十六進制
  
    return encryptedHex;
  }
  ```

演示效果

總結

本文主要介紹了一些前端安全方面的知識和具體加密方案的實現。爲了保護客戶的隱私數據,不論是 HTTP 仍是HTTPS,都建議密文傳輸信息,讓破解者增長一點攻擊難度吧。固然數據加解密也會帶來必定性能上的消耗,這個須要各位開發者各自衡量了。

參考文獻

看完這篇文章,我奶奶都懂了https的原理

中間人攻擊

招賢納士

政採雲前端團隊(ZooTeam),一個年輕富有激情和創造力的前端團隊,隸屬於政採雲產品研發部,Base 在風景如畫的杭州。團隊現有 40 餘個前端小夥伴,平均年齡 27 歲,近 3 成是全棧工程師,妥妥的青年風暴團。成員構成既有來自於阿里、網易的「老」兵,也有浙大、中科大、杭電等校的應屆新人。團隊在平常的業務對接以外,還在物料體系、工程平臺、搭建平臺、性能體驗、雲端應用、數據分析及可視化等方向進行技術探索和實戰,推進並落地了一系列的內部技術產品,持續探索前端技術體系的新邊界。

若是你想改變一直被事折騰,但願開始能折騰事;若是你想改變一直被告誡須要多些想法,卻無從破局;若是你想改變你有能力去作成那個結果,卻不須要你;若是你想改變你想作成的事須要一個團隊去支撐,但沒你帶人的位置;若是你想改變既定的節奏,將會是「5 年工做時間 3 年工做經驗」;若是你想改變原本悟性不錯,但老是有那一層窗戶紙的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但願參與到隨着業務騰飛的過程,親手推進一個有着深刻的業務理解、完善的技術體系、技術創造價值、影響力外溢的前端團隊的成長曆程,我以爲咱們該聊聊。任什麼時候間,等着你寫點什麼,發給 ZooTeam@cai-inc.com

相關文章
相關標籤/搜索