Nodejs實戰系列:資料加密與crypto模組
部落格地址:《NodeJS模組研究 - crypto》
Github :https://github.com/dongyuanxin/blog
nodejs 中的 crypto 模組提供了各種各樣加密演算法的 API。這篇文章記錄了常用加密演算法的種類、特點、用途和程式碼實現。其中涉及演算法較多,應用面較廣,每類演算法都有自己適用的場景。為了使行文流暢,列出了本文記錄的幾類常用演算法:
內容摘要:雜湊(Hash)演算法 內容摘要:HMac 演算法 內容加解密:對稱加密(AES)與非對稱加密解密(RSA) 內容簽名:簽名和驗證演算法
雜湊(Hash)演算法
雜湊函式(英語:Hash function)又稱雜湊演算法、雜湊函式,是一種從任何一種資料中建立小的數字“指紋”的方法。基本原理是將任意長度資料輸入,最後輸出固定長度的結果。
hash 演算法具有以下特點:
不能從 hash 值倒推原資料 不同的輸入,會有不同的輸出 好的 hash 演算法衝突概率更低
正因為 hash 演算法的這些特點,因此 hash 演算法主要用於:加密、資料檢驗、版本標識、負載均衡、分散式(一致性 hash)。
下面實現了一個獲取檔案標識的函式:
const crypto = require("crypto"); const fs = require("fs"); function getFileHash(file, algorithm) { if (!crypto.getHashes().includes(algorithm)) { throw new Error("不支援此雜湊函式"); } return new Promise(resolve => { const hash = crypto.createHash(algorithm); const rs = fs.createReadStream(file); rs.on("readable", () => { const data = rs.read(); if (data) { hash.update(data); } }); rs.on("end", () => { resolve(hash.digest("hex")); }); }); } // 用法:獲取檔案md5 getFileHash("./db.json", "md5").then(val => { console.log(val); });
HMac 演算法
攻擊者可以藉助“彩虹表”來破解雜湊表。應對彩虹表的方法,是給密碼加鹽值(salt),將 pwd 和 salt 一起計算 hash 值。其中,salt 是隨機生成的,越長越好,並且需要和使用者名稱、密碼對應儲存在資料表中。
雖然通過加鹽,實現了雜湊長度擴充套件,但是攻擊者通過提交密碼和雜湊值也可以破解攻擊。伺服器會把提交的密碼和 salt 構成字串,然後和提交的雜湊值對比。如果系統不能提交雜湊值,不會受到此類攻擊。
顯然,沒有絕對安全的方法。但是不推薦使用密碼加鹽,而是 HMac 演算法。它可以使用任意的 Hash 函式,例如 md5 => HmacMD5、sha1 => HmacSHA1。
下面是利用 Hmac 實現加密資料的函式:
const crypto = require("crypto");
function encryptData(data, key, algorithm) {
if (!crypto.getHashes().includes(algorithm)) {
throw new Error("不支援此雜湊函式");
}
const hmac = crypto.createHmac(algorithm, key);
hmac.update(data);
return hmac.digest("hex");
}
// output: 30267bcf2a476abaa9b9a87dd39a1f8d6906d1180451abdcb8145b384b9f76a5
console.log(encryptData("root", "7(23y*&745^%I", "sha256"));
對稱加密(AES)與非對稱加密解密(RSA)
有很多資料需要加密儲存,並且需要解密後進行使用。這和前面不可逆的雜湊函式不同。此類演算法一共分為兩類:
對稱加密(AES):加密和解密使用同一個金鑰 非對稱加密解密(RSA):公鑰加密,私鑰解密
對稱加密(AES)
檢視 nodejs 支援的所有加密演算法:
crypto.getCiphers();
Nodejs 提供了 Cipher 類和 Decipher 類,分別用於加密和解密。兩者都繼承 Transfrom Stream,API 的使用方法和雜湊函式的 API 使用方法類似。
下面是用 aes-256-cbc 演算法對明文進行加密:
const crypto = require("crypto");
const secret = crypto.randomBytes(32); // 金鑰
const content = "hello world!"; // 要加密的明文
const cipher = crypto.createCipheriv(
"aes-256-cbc",
secret,
Buffer.alloc(16, 0)
);
cipher.update(content, "utf8");
// 加密後的結果:e2a927165757acc609a89c093d8e3af5
console.log(cipher.final("hex"));
注意:在使用加密演算法的時候,給定的金鑰長度是有要求的,否則會爆出this[kHandle].initiv(cipher, credential, iv, authTagLength); Error: Invalid key length...
的錯誤。以 aes-256-cbc 演算法為例,需要 256 bits = 32 bytes 大小的金鑰。同樣地,AES 的 IV 也是有要求的,需要 128bits。(請參考“參考連結”部分)
使用 32 個連續I
作為金鑰,用 aes-256-cbc 加密後的結果是 a061e67f5643d948418fdb150745f24d。下面是逆向解密的過程:
const secret = "I".repeat(32);
const decipher = crypto.createDecipheriv(
"aes-256-cbc",
secret,
Buffer.alloc(16, 0)
);
decipher.update("a061e67f5643d948418fdb150745f24d", "hex");
console.log(decipher.final("utf8")); // 解密後的結果:hello world!
非對稱加密解密(RSA)
藉助 openssl 生成私鑰和公鑰:
# 生成私鑰
openssl genrsa -out privatekey.pem 1024
# 生成公鑰
openssl rsa -in privatekey.pem -pubout -out publickey.pem
對 hello world!
加密和解密的程式碼如下:
const crypto = require("crypto");
const fs = require("fs");
const privateKey = fs.readFileSync("./privatekey.pem");
const publicKey = fs.readFileSync("./publickey.pem");
const content = "hello world!"; // 待加密的明文內容
// 公鑰加密
const encodeData = crypto.publicEncrypt(publicKey, Buffer.from(content));
console.log(encodeData.toString("base64"));
// 私鑰解密
const decodeData = crypto.privateDecrypt(privateKey, encodeData);
console.log(decodeData.toString("utf8"));
簽名和驗證演算法
除了不可逆的雜湊演算法、資料加密演算法,還有專門用於簽名和驗證的演算法。這裡也需要用 openssl 生成公鑰和私鑰。
程式碼示範如下:
const crypto = require("crypto");
const fs = require("fs");
const assert = require("assert");
const privateKey = fs.readFileSync("./privatekey.pem");
const publicKey = fs.readFileSync("./publickey.pem");
const data = "傳輸的資料";
// 第一步:用私鑰對傳輸的資料,生成對應的簽名
const sign = crypto.createSign("sha256");
// 新增資料
sign.update(data, "utf8");
sign.end();
// 根據私鑰,生成簽名
const signature = sign.sign(privateKey, "hex");
// 第二步:藉助公鑰驗證簽名的準確性
const verify = crypto.createVerify("sha256");
verify.update(data, "utf8");
verify.end();
assert.ok(verify.verify(publicKey, signature, "hex"));
從前面這段程式碼可以看到,利用私鑰進行加密,得到簽名值;最後利用公鑰進行驗證。
總結
之前一直是一知半解,一些概念很模糊,經常混淆雜湊演算法和加密演算法。整理完這篇筆記,我才理清楚了常見的加密演算法的功能和用途。
除此之外,crypto 模組還提供了其他演算法工具,例如 ECDH 在區塊鏈中有應用。這篇文章沒有再記錄,感興趣的同學可以去查閱相關資料。
參考連結
NodeJS docs: crypto 推薦:Node.js 加密演算法庫 Crypto 推薦:什麼是 hash? - 騰訊技術工程的回答 - 知乎 Wiki:雜湊函式 Store and validate hashed password Wiki: 彩虹表 Nodejs 6.10.2 crypto AES Invalid key length Encrypting using AES-256, can I use 256 bits IV? Crypto 加密與解密