原文出自:https://www.pandashen.com
加密簡介
加密是以某種算法改變原有的信息數據,使得未授權用戶即使獲得了已加密信息,因不知解密的方法,無法得知信息真正的含義,通過這種方式提高網絡數據傳輸的安全性,加密算法常見的有哈希算法、HMAC 算法、簽名、對稱性加密算法和非對稱性加密算法,加密算法也分爲可逆和不可逆,比如 md5
就是不可逆加密,只能暴力破解(撞庫),我們在 NodeJS 開發中就是直接使用這些加密算法,crypto
模塊提供了加密功能,包含對 OpenSSL
的哈希、HMAC、加密、解密、簽名以及驗證功能的一整套封裝,核心模塊,使用時不需安裝。
哈希算法
哈希算法也叫散列算法,用來把任意長度的輸入變換成固定長度的輸出,常見的有 md5
、sha1
等,這類算法實現對原數據的轉化過程是否能被稱爲加密備受爭議,爲了後面敘述方便我們姑且先叫做加密。
// 查看哈希加密算法的種類
const crypto = require("crypto");
// getHashes 方法用於查看支持的加密算法
console.log(crypto.getHashes());
// [ 'DSA', 'DSA-SHA', 'DSA-SHA1', 'DSA-cSHA1-old',
// 'RSA-MD4', 'RSA-MD5', 'RSA-MDC2', 'RSA-RIPEMD160',
// 'RSA-SHA', 'RSA-SHA1', 'RSA-SHA1-2', 'RSA-SHA224',
// 'RSA-SHA256', 'RSA-SHA384', 'RSA-SHA512',
// 'dsaEncryption', 'dsaWithSHA', 'dsaWithSHA1', 'dss1',
// 'ecdsa-with-SHA1', 'md4', 'md4WithRSAEncryption',
// 'md5', 'md5WithRSAEncryption', 'mdc2', 'mdc2WithRSA',
// 'ripemd', 'ripemd160', 'ripemd160WithRSA', 'rmd160',
// 'sha', 'sha1', 'sha1WithRSAEncryption', 'sha224',
// 'sha224WithRSAEncryption', 'sha256',
// 'sha256WithRSAEncryption', 'sha384',
// 'sha384WithRSAEncryption', 'sha512',
// 'sha512WithRSAEncryption', 'shaWithRSAEncryption',
// 'ssl2-md5', 'ssl3-md5', 'ssl3-sha1', 'whirlpool' ]
md5
是開發中經常使用的算法之一,官方稱爲摘要算法,具有以下幾個特點:
- 不可逆;
- 不管加密的內容多長,最後輸出的結果長度都是相等的;
- 內容不同輸出的結果完全不同,內容相同輸出的結果完全相同。
由於相同的輸入經過 md5
加密後返回的結果完全相同,所以破解時通過 “撞庫” 進行暴力破解,當連續被 md5
加密 3
次以上時就很難被破解了,所以使用 md5
一般會進行多次加密。
// md5 加密 —— 返回 Buffer
const crytpo = require("crytpo");
let md5 = crytpo.createHash("md5"); // 創建 md5
let md5Sum = md5.update("hello"); // update 加密
let result = md5Sum.digest(); // 獲取加密後結果
console.log(result); // <Buffer 5d 41 40 2a bc 4b 2a 76 b9 71 9d 91 10 17 c5 92>
digest
方法參數用於指定加密後的返回值的格式,不傳參默認返回加密後的 Buffer,常用的參數有 hex
和 Base64
,hex
代表十六進制,加密後長度爲 32
,Base64
的結果長度爲 24
,以 ==
結尾。
// md5 加密 —— 返回十六進制
const crypto = require("crypto");
let md5 = crypto.createHash("md5");
let md5Sum = md5.update("hello");
let result = md5Sum.digest("hex");
console.log(result); // 5d41402abc4b2a76b9719d911017c592
// md5 加密 —— 返回 Base64
const crypto = require("crypto");
let md5 = crypto.createHash("md5");
let md5Sum = md5.update("hello");
let result = md5Sum.digest("Base64");
console.log(result); // XUFAKrxLKna5cZ2REBfFkg==
update
方法的返回值就是 this
,即當前實例,所以支持鏈式調用,較長的信息也可以多次調用 update
方法進行分段加密,調用 digest
方法同樣會返回整個加密後的值。
// 鏈式調用和分段加密
const crypto = require("crypto");
let result = crypto
.createHash("md5")
.update("he")
.update("llo")
.digest("hex");
console.log(result); // 5d41402abc4b2a76b9719d911017c592
由於可以使用 update
進行分段加密,就可以結合流來使用,其實 crypto
的本質是創建 Transform
類型的轉化流,可以將可讀流轉化成可寫流。
// 對可讀流讀取的數據進行 md5 加密
const crypto = require("crypto");
let fs = require("fs");
let md5 = crypto.createHash("md5");
let rs = fs.createReadSteam("./readme.txt", {
highWaterMark: 3
});
// 讀取數據並加密
rs.on("data", data => md5.update(data));
rs.on("end", () => {
let result = md5.digest("hex");
console.log(result);
});
使用場景 1:經常被使用在數據的校驗,比如服務器與服務器之間進行通信發送的明文摘要加 md5
加密摘要後的暗文,接收端拿到數據以後將明文摘要按照相同的 md5
算法加密後與暗文摘要對比驗證,目的是防止數據傳輸過程中被劫持並篡改。
使用場景 2:在瀏覽器緩存策略中,可以通過對靜態資源的信息摘要使用 md5
加密,每次向服務器發送加密後的密鑰進行比對就可以了,不至於對整個文件內容進行比較。
缺點:由於規定使用 md5
的哈希算法加密,別人可以使用同樣的算法對信息進行僞造,安全性不高。
Hmac 算法
1、Hmac 算法的使用
Hmac 算法又稱加鹽算法,是將哈希算法與一個密鑰結合在一起,用來阻止對簽名完整性的破壞,同樣具備 md5
加密的幾個特點。
// 使用加鹽算法加密
const crytpo = require("crytpo");
let hmac = crytpo.createHmac("sha1", "panda");
let result = hmac.update("hello").digest("Base64");
console.log(result); // 7spMLxN8WJdcEtQ8Hm/LR9pUE3YsIGag9Dcai7lwioo=
crytpo.createHmac
第一個參數同 crytpo.createHash
,爲加密的算法,常用 sha1
和 sha256
,第二個參數爲密鑰。
digest
方法生成的加密結果長度要大於 md5
,hex
生成的結果長度爲 64
,Base64
生成的結果長度爲 44
,以 =
結尾。
安全性高於 md5
,通過密鑰來加密,不知道密鑰無法破解,缺點是密鑰傳輸的過程容易被劫持,可以通過一些生成隨機密鑰的方式避免。
2、創建密鑰的方法
可以安裝 openSSH
客戶端,並通過命令行生成存儲密鑰的文件,命令如下。
openssl genrsa -out rsa_private.key 1024
openssl genrsa
代表生成密鑰,-out
代表輸出文件,rsa_private.key
代表文件名,1024
代表輸出密鑰的大小。
// 直接讀取密鑰文件配合加鹽算法加密
const fs = require("fs");
const crytpo = require("crytpo");
const path = require("path");
let key = fs.readFileSync(path.join(__dirname, "/rsa_private.key"));
let hmac = crytpo.createHmac("sha256", key);
let result = hmac.update("hello").digest("Base64");
console.log(result); // bmi2N+6kwgwt5b+U+zSgjL/NFs+GsUnZmcieqLKBy4M=
對稱性加密
對稱性加密是發送數據時使用密鑰和加密算法進行加密,接收數據時需要使用相同的密鑰和加密算法的逆算法(解密算法)進行解密,也就是說對稱性加密的過程是可逆的,crytpo
中使用的算法爲 blowfish
。
// 對稱性加密
const fs = require("fs");
const crypto = require("crypto");
const path = require("path");
let key = fs.readFileSync(path.join(__dirname, "/rsa_private.key"));
// 加密
let cipher = crypto.createCipher("blowfish", key);
cipher.update("hello");
// final 方法不能鏈式調用
let result = cipher.final("hex");
console.log(result); // 3eb9943113c7aa1e
// 解密
let decipher = crypto.createDecipher("blowfish", key);
decipher.update(result, "hex");
let data = decipher.final("utf8");
console.log(data); // hello
加密使用 crypto.createCipher
方法,解密使用 crypto.createDecipher
方法,但是使用的算法和密鑰必須相同,需要注意的是解密過程中 update
中需要在第二個參數中指定加密時的格式,如 hex
,在 final
還原數據時需要指定加密字符的編碼格式,如 utf8
。
注意:使用對稱性加密的字符串有長度限制,不得超過 7
個字符,否則雖然可以加密成功,但是無法解密。
缺點:密鑰在傳輸過程中容易被截獲,存在安全風險。
非對稱性加密
非對稱性加密相也是可逆的,較於對稱性加密要更安全,消息傳輸方和接收方都會在本地創建一對密鑰,公鑰和私鑰,互相將自己的公鑰發送給對方,每次消息傳遞時使用對方的公鑰加密,對方接收消息後使用他的的私鑰解密,這樣在公鑰傳遞的過程中被截獲也無法解密,因爲公鑰加密的消息只有配對的私鑰可以解密。
接下來我們使用 openSSH
對之前生成的私鑰 rsa_private.key
產生一個對應的公鑰,命令如下。
openssl rsa -in rsa_private.key -pubout -out rsa_public.key
上面的命令意思根據一個私鑰生成對應的公鑰,-pubout -out
代表公鑰輸出,rsa_public.key
爲公鑰的文件名。
// 非對稱性加密
const fs = require("fs");
const crypto = require("crypto");
const path = require("path");
// 獲取公鑰和私鑰
let publicKey = fs.readFileSync(path.join(__dirname, "/rsa_public.key"));
let privateKey = fs.readFileSync(path.join(__dirname, "/rsa_private.key"));
// 加密
let secret = crytpo.publicEncrypt(publicKey, Buffer.from("hello"));
// 解密
let result = crytpo.provateDecrypt(privateKey, secret);
console.log(result); // hello
使用公鑰加密的方法是 crytpo.publicEncrypt
,第一個參數爲公鑰,第二個參數爲加密信息(必須是 Buffer),使用私鑰解密的方法是 crytpo.provateDecrypt
,第一個參數爲私鑰,第二個參數爲解密的信息。
簽名
簽名與非對稱性加密非常類似,同樣有公鑰和私鑰,不同的是使用私鑰加密,對方使用公鑰進行解密驗證,以確保這段數據是私鑰的擁有者所發出的原始數據,且在網絡中的傳輸過程中未被修改。
我們還使用 rsa_public.key
和 rsa_private.key
作爲公鑰和私鑰,crypto
實現簽名代碼如下。
// 簽名
const fs = require("fs");
const crypto = require("crypto");
const path = require("path");
// 獲取公鑰和私鑰
let publicKey = fs.readFileSync(path.join(__dirname, "rsa_public.key"), "ascii");
let privateKey = fs.readFileSync(path.join(__dirname, "rsa_private.key"), "ascii");
// 生成簽名
let sign = crypto.createSign("RSA-SHA256");
sign.update("panda");
let signed = sign.sign(privateKey, "hex");
// 驗證簽名
let verify = crypto.createVerify("RSA-SHA256");
verify.update("panda");
let verifyResult = verify.verify(publicKey, signed, "hex");
console.log(verifyResult); // true
生成簽名的 sign
方法有兩個參數,第一個參數爲私鑰,第二個參數爲生成簽名的格式,最後返回的 signed
爲生成的簽名(字符串)。
驗證簽名的 verify
方法有三個參數,第一個參數爲公鑰,第二個參數爲被驗證的簽名,第三個參數爲生成簽名時的格式,返回爲布爾值,即是否通過驗證。
使用場景:經常用於對 cookie 簽名返回瀏覽器,當瀏覽器訪問同域服務器將 cookie 帶過來時再進行驗證,防止 cookie 被篡改和 CSRF 跨站請求僞造。
總結
各種項目在數據傳輸時根據信息的敏感度以及用途進行不同的加密算法和加密方式,在 NodeJS 中,crypto
的 API 完全可以實現我們的加密需求,也可以將上面的加密方案組合使用實現更復雜的加密方案。