敏感數(shù)據(jù)加密方案及實(shí)現(xiàn)
前言
現(xiàn)在是大數(shù)據(jù)時(shí)代,需要收集大量的個(gè)人信息用于統(tǒng)計(jì)。一方面它給我們帶來(lái)了便利,另一方面一些個(gè)人信息數(shù)據(jù)在無(wú)意間被泄露,被非法分子用于推銷和黑色產(chǎn)業(yè)。
2018 年 5 月 25 日,歐盟已經(jīng)強(qiáng)制執(zhí)行《通用數(shù)據(jù)保護(hù)條例》(General Data Protection Regulation,縮寫(xiě)作 GDPR)。該條例是歐盟法律中對(duì)所有歐盟個(gè)人關(guān)于數(shù)據(jù)保護(hù)和隱私的規(guī)范。這意味著個(gè)人數(shù)據(jù)必須使用假名化或匿名化進(jìn)行存儲(chǔ),并且默認(rèn)使用盡可能最高的隱私設(shè)置,以避免數(shù)據(jù)泄露。
相信大家也都不想讓自己在外面“裸奔”。所以,作為前端開(kāi)發(fā)人員也應(yīng)該盡量避免用戶個(gè)人數(shù)據(jù)的明文傳輸,盡可能的降低信息泄露的風(fēng)險(xiǎn)。
看到這里可能有人會(huì)說(shuō)現(xiàn)在都用 HTTPS 了,數(shù)據(jù)在傳輸過(guò)程中是加密的,前端就不需要加密了。其實(shí)不然,我可以在你發(fā)送 HTTPS 請(qǐng)求之前,通過(guò)谷歌插件來(lái)捕獲 HTTPS 請(qǐng)求中的個(gè)人信息,下面我會(huì)為此演示。所以前端數(shù)據(jù)加密還是很有必要的。
數(shù)據(jù)泄露方式
1. 中間人攻擊
中間人攻擊是常見(jiàn)的攻擊方式。詳細(xì)過(guò)程可以參見(jiàn)這里:
https://zh.wikipedia.org/wiki/%E4%B8%AD%E9%97%B4%E4%BA%BA%E6%94%BB%E5%87%BB。大概的過(guò)程是中間人通過(guò) DNS 欺騙等手段劫持了客戶端與服務(wù)端的會(huì)話。
客戶端、服務(wù)端之間的信息都會(huì)經(jīng)過(guò)中間人,中間人可以獲取和轉(zhuǎn)發(fā)兩者的信息。在 HTTP 下,前端數(shù)據(jù)加密還是避免不了數(shù)據(jù)泄露,因?yàn)橹虚g人可以偽造密鑰。為了避免中間人攻擊,我們一般采用 HTTPS 的形式傳輸。
2. 谷歌插件
HTTPS 雖然可以防止數(shù)據(jù)在網(wǎng)絡(luò)傳輸過(guò)程中被劫持,但是在發(fā)送 HTTPS 之前,數(shù)據(jù)還是可以從谷歌插件中泄露出去。
因?yàn)楣雀璨寮梢圆东@ Network 中的所有請(qǐng)求,所以如果某些插件中有惡意的代碼還是可以獲取到用戶信息的,下面為大家演示。

所以光采用 HTTPS,一些敏感信息如果還是以明文的形式傳輸?shù)脑?,也是不安全的。如果?HTTPS 的基礎(chǔ)上再進(jìn)行數(shù)據(jù)的加密,那相對(duì)來(lái)說(shuō)就更好了。
加密算法介紹
1. 對(duì)稱加密
對(duì)稱加密算法,又稱為共享密鑰加密算法。在對(duì)稱加密算法中,使用的密鑰只有一個(gè),發(fā)送和接收雙方都使用這個(gè)密鑰對(duì)數(shù)據(jù)進(jìn)行加密和解密。
這就要求加密和解密方事先都必須知道加密的密鑰。其優(yōu)點(diǎn)是算法公開(kāi)、計(jì)算量小、加密速度快、加密效率高;缺點(diǎn)是密鑰泄露之后,數(shù)據(jù)就會(huì)被破解。一般不推薦單獨(dú)使用。根據(jù)實(shí)現(xiàn)機(jī)制的不同,常見(jiàn)的算法主要有:
- AES(https://zh.wikipedia.org/wiki/%E9%AB%98%E7%BA%A7%E5%8A%A0%E5%AF%86%E6%A0%87%E5%87%86)
- ChaCha20 (https://zh.wikipedia.org/wiki/Salsa20#ChaCha20)、3DES (https://zh.wikipedia.org/wiki/3DES)等。
2. 非對(duì)稱加密
非對(duì)稱加密算法,又稱為公開(kāi)密鑰加密算法。它需要兩個(gè)密鑰,一個(gè)稱為公開(kāi)密鑰 (public key),即公鑰;另一個(gè)稱為私有密鑰 (private key),即私鑰。
他倆是配對(duì)生成的,就像鑰匙和鎖的關(guān)系。因?yàn)榧用芎徒饷苁褂玫氖莾蓚€(gè)不同的密鑰,所以這種算法稱為非對(duì)稱加密算法。其優(yōu)點(diǎn)是算法強(qiáng)度復(fù)雜、安全性高;缺點(diǎn)是加解密速度沒(méi)有對(duì)稱加密算法快。常見(jiàn)的算法主要有:
- RSA (https://zh.wikipedia.org/wiki/RSA%E5%8A%A0%E5%AF%86%E6%BC%94%E7%AE%97%E6%B3%95)
- Elgamal (https://zh.wikipedia.org/wiki/ElGamal%E5%8A%A0%E5%AF%86%E7%AE%97%E6%B3%95)等。
3. 散列算法
散列算法又稱散列函數(shù)、哈希函數(shù),是把消息或數(shù)據(jù)壓縮成摘要,使得數(shù)據(jù)量變小,將數(shù)據(jù)的格式固定成特定長(zhǎng)度的值。一般用于校驗(yàn)數(shù)據(jù)的完整性,平時(shí)我們下載文件就可以校驗(yàn) MD5 來(lái)判斷下載的數(shù)據(jù)是否完整。常見(jiàn)的算法主要有:
- MD4 (https://zh.wikipedia.org/wiki/MD4)
- MD5 (https://zh.wikipedia.org/wiki/MD5)
- SHA (https://zh.wikipedia.org/wiki/SHA%E5%AE%B6%E6%97%8F) 等。
實(shí)現(xiàn)方案
方案一:如果用對(duì)稱加密,那么服務(wù)端和客戶端都必須知道密鑰才行。那服務(wù)端勢(shì)必要把密鑰發(fā)送給客戶端,這個(gè)過(guò)程中是不安全的,所以單單用對(duì)稱加密行不通。
方案二:如果用非對(duì)稱加密,客戶端的數(shù)據(jù)通過(guò)公鑰加密,服務(wù)端通過(guò)私鑰解密,客戶端發(fā)送數(shù)據(jù)實(shí)現(xiàn)加密沒(méi)問(wèn)題??蛻舳私邮軘?shù)據(jù),需要服務(wù)端用公鑰加密,然后客戶端用私鑰解密。所以這個(gè)方案需要兩套公鑰和私鑰,需要在客戶端和服務(wù)端各自生成自己的密鑰。
方案三:如果把對(duì)稱加密和非對(duì)稱加密相結(jié)合??蛻舳诵枰梢粋€(gè)對(duì)稱加密的密鑰 1,傳輸內(nèi)容與該密鑰 1進(jìn)行對(duì)稱加密傳給服務(wù)端,并且把密鑰 1 和公鑰進(jìn)行非對(duì)稱加密,然后也傳給服務(wù)端。服務(wù)端通過(guò)私鑰把對(duì)稱加密的密鑰 1 解密出來(lái),然后通過(guò)該密鑰 1 解密出內(nèi)容。以上是客戶端到服務(wù)端的過(guò)程。如果是服務(wù)端要發(fā)數(shù)據(jù)到客戶端,就需要把響應(yīng)數(shù)據(jù)跟對(duì)稱加密的密鑰 1 進(jìn)行加密,然后客戶端接收到密文,通過(guò)客戶端的密鑰 1 進(jìn)行解密,從而完成加密傳輸。
總結(jié):
以上只是列舉了常見(jiàn)的加密方案。總的來(lái)看,方案二比較簡(jiǎn)單,但是需要維護(hù)兩套公鑰和私鑰,當(dāng)公鑰變化的時(shí)候,必須通知對(duì)方,靈活性比較差。方案三相對(duì)方案二來(lái)說(shuō),密鑰 1 隨時(shí)可以變化,并且不需要通知服務(wù)端,相對(duì)來(lái)說(shuō)靈活性、安全性好點(diǎn)并且方案三對(duì)內(nèi)容是對(duì)稱加密,當(dāng)數(shù)據(jù)量大時(shí),對(duì)稱加密的速度會(huì)比非對(duì)稱加密快。所以本文采用方案三給予代碼實(shí)現(xiàn)。
代碼實(shí)現(xiàn)
下面是具體的代碼實(shí)現(xiàn)(以登錄接口為例),主要的目的就是要把明文的個(gè)人信息轉(zhuǎn)成密文傳輸。其中對(duì)稱加密庫(kù)使用的是 AES,非對(duì)稱加密庫(kù)使用的是RSA。
客戶端:
- AES 庫(kù)(aes-js):https://github.com/ricmoo/aes-js
- RSA庫(kù)(jsencrypt):https://github.com/travist/jsencrypt
- 具體代碼實(shí)現(xiàn)登錄接口
(1) 客戶端需要隨機(jī)生成一個(gè) aesKey,在頁(yè)面加載完的時(shí)候需要從服務(wù)端請(qǐng)求 publicKey
- let aesKey = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; // 隨機(jī)產(chǎn)生
- let publicKey = ""; // 公鑰會(huì)從服務(wù)端獲取
- // 頁(yè)面加載完之后,就去獲取公鑰
- 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);
- });
- };
2. aes 加密和解密方法
- /**
- * aes加密方法
- * @param {string} text 待加密的字符串
- * @param {array} key 加密key
- */
- function aesEncrypt(text, key) {
- const textBytes = aesjs.utils.utf8.toBytes(text); // 把字符串轉(zhuǎn)換成二進(jìn)制數(shù)據(jù)
- // 這邊使用CTR-Counter加密模式,還有其他模式可以選擇,具體可以參考aes加密庫(kù)
- const aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5));
- const encryptedBytes = aesCtr.encrypt(textBytes); // 進(jìn)行加密
- const encryptedHex = aesjs.utils.hex.fromBytes(encryptedBytes); // 把二進(jìn)制數(shù)據(jù)轉(zhuǎn)成十六進(jìn)制
- return encryptedHex;
- }
- /**
- * aes解密方法
- * @param {string} encryptedHex 加密的字符串
- * @param {array} key 加密key
- */
- function aesDecrypt(encryptedHex, key) {
- const encryptedBytes = aesjs.utils.hex.toBytes(encryptedHex); // 把十六進(jìn)制數(shù)據(jù)轉(zhuǎn)成二進(jìn)制
- const aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5));
- const decryptedBytes = aesCtr.decrypt(encryptedBytes); // 進(jìn)行解密
- const decryptedText = aesjs.utils.utf8.fromBytes(decryptedBytes); // 把二進(jìn)制數(shù)據(jù)轉(zhuǎn)成utf-8字符串
- return decryptedText;
- }
3. 請(qǐng)求登錄
- /**
- * 登陸接口
- */
- 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); // 把要發(fā)送的數(shù)據(jù)轉(zhuǎn)成字符串進(jìn)行加密
- console.log("發(fā)送數(shù)據(jù)", text);
- const encrypt = new JSEncrypt();
- encrypt.setPublicKey(publicKey);
- const encryptencrypted = encrypt.encrypt(aesKey.toString()); // 把a(bǔ)esKey進(jìn)行非對(duì)稱加密
- 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進(jìn)行解密
- console.log("接收數(shù)據(jù)", reciveData);
- })
- .catch(function (error) {
- console.log("error", error);
- });
- }
服務(wù)端(Node):
- AES庫(kù)(aes-js):https://github.com/ricmoo/aes-js
- RSA 庫(kù)(node-rsa):https://github.com/rzcoder/node-rsa
- 具體代碼實(shí)現(xiàn)登錄接口
(1) 引用加密庫(kù)
- 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 = ""; // 用于保存服務(wù)端的公鑰
- rsaKey.setOptions({ encryptionScheme: "pkcs1" }); // 設(shè)置加密模式
(2) 實(shí)現(xiàn) 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 })); // 把公鑰發(fā)送給客戶端
- 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: "杭州" }), // 這個(gè)數(shù)據(jù)會(huì)被加密
- aesKey
- ),
- })
- );
- });
- return;
- }
- break;
- default:
- break;
- }
- response.writeHead(404);
- response.end();
- })
- .listen(3000);
3. 加密和解密方法
- 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 加密的字符串
- * @param {array} key 加密key
- */
- function aesDecrypt(encryptedHex, key) {
- const encryptedBytes = aesjs.utils.hex.toBytes(encryptedHex); // 把十六進(jìn)制轉(zhuǎn)成二進(jìn)制數(shù)據(jù)
- const aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5)); // 這邊使用CTR-Counter加密模式,還有其他模式可以選擇,具體可以參考aes加密庫(kù)
- const decryptedBytes = aesCtr.decrypt(encryptedBytes); // 進(jìn)行解密
- const decryptedText = aesjs.utils.utf8.fromBytes(decryptedBytes); // 把二進(jìn)制數(shù)據(jù)轉(zhuǎn)成字符串
- return decryptedText;
- }
- /**
- * aes加密方法
- * @param {string} text 待加密的字符串
- * @param {array} key 加密key
- */
- function aesEncrypt(text, key) {
- const textBytes = aesjs.utils.utf8.toBytes(text); // 把字符串轉(zhuǎn)成二進(jìn)制數(shù)據(jù)
- const aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5));
- const encryptedBytes = aesCtr.encrypt(textBytes); // 加密
- const encryptedHex = aesjs.utils.hex.fromBytes(encryptedBytes); // 把二進(jìn)制數(shù)據(jù)轉(zhuǎn)成十六進(jìn)制
- return encryptedHex;
- }
完整的示例代碼:https://github.com/Pulset/FrontDataEncrypt
演示效果

總結(jié)
本文主要介紹了一些前端安全方面的知識(shí)和具體加密方案的實(shí)現(xiàn)。為了保護(hù)客戶的隱私數(shù)據(jù),不管是 HTTP 還是 HTTPS,都建議密文傳輸信息,讓破解者增加一點(diǎn)攻擊難度吧。當(dāng)然數(shù)據(jù)加解密也會(huì)帶來(lái)一定性能上的消耗,這個(gè)需要各位開(kāi)發(fā)者各自衡量了。