敏感數據加密方案及實現


https://segmentfault.com/a/1190000037513330

前言

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

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

相信大家也都不想讓自己在外面“裸奔”。所以,作為前端開發人員也應該盡量避免用戶個人數據的明文傳輸,盡可能的降低信息泄露的風險。

看到這里可能有人會說現在都用 HTTPS 了,數據在傳輸過程中是加密的,前端就不需要加密了。其實不然,我可以在你發送 HTTPS 請求之前,通過谷歌插件來捕獲 HTTPS 請求中的個人信息,下面我會為此演示。所以前端數據加密還是很有必要的。

數據泄露方式

  • 中間人攻擊

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

    客戶端、服務端之間的信息都會經過中間人,中間人可以獲取和轉發兩者的信息。在 HTTP 下,前端數據加密還是避免不了數據泄露,因為中間人可以偽造密鑰。為了避免中間人攻擊,我們一般采用 HTTPS 的形式傳輸。

  • 谷歌插件

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

    因為谷歌插件可以捕獲 Network 中的所有請求,所以如果某些插件中有惡意的代碼還是可以獲取到用戶信息的,下面為大家演示。

    所以光采用 HTTPS,一些敏感信息如果還是以明文的形式傳輸的話,也是不安全的。如果在 HTTPS 的基礎上再進行數據的加密,那相對來說就更好了。

加密算法介紹

  • 對稱加密

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

    這就要求加密和解密方事先都必須知道加密的密鑰。其優點是算法公開、計算量小、加密速度快、加密效率高;缺點是密鑰泄露之后,數據就會被破解。一般不推薦單獨使用。根據實現機制的不同,常見的算法主要有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,都建議密文傳輸信息,讓破解者增加一點攻擊難度吧。當然數據加解密也會帶來一定性能上的消耗,這個需要各位開發者各自衡量了。


免責聲明!

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



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