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("不支持此哈希函數");
}

<span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">return</span> <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">new</span> <span class="hljs-built_in" style="color: #0086b3; line-height: 26px;">Promise</span>(<span class="hljs-function" style="line-height: 26px;"><span class="hljs-params" style="line-height: 26px;">resolve</span> =&gt;</span> {
    <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">const</span> hash = crypto.createHash(algorithm);

    <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">const</span> rs = fs.createReadStream(file);
    rs.on(<span class="hljs-string" style="color: #d14; line-height: 26px;">"readable"</span>, () =&gt; {
        <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">const</span> data = rs.read();
        <span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">if</span> (data) {
            hash.update(data);
        }
    });
    rs.on(<span class="hljs-string" style="color: #d14; line-height: 26px;">"end"</span>, () =&gt; {
        resolve(hash.digest(<span class="hljs-string" style="color: #d14; line-height: 26px;">"hex"</span>));
    });
});

}

// 用法:獲取文件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("不支持此哈希函數");
}

<span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">const</span> hmac = crypto.createHmac(algorithm, key);
hmac.update(data);
<span class="hljs-keyword" style="color: #333; font-weight: bold; line-height: 26px;">return</span> hmac.digest(<span class="hljs-string" style="color: #d14; line-height: 26px;">"hex"</span>);

}

// 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 在區塊鏈中有應用。這篇文章沒有再記錄,感興趣的同學可以去查閱相關資料。

參考鏈接

👇掃碼關注「心譚博客」,查看「前端圖譜」&「算法題解」,堅持分享,共同成長👇


免責聲明!

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



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