基於NodeJS的HTTP server Plus 6:加密(crypto)

煎蛋面__cq發表於2019-03-01

加密簡介

加密是以某種演算法改變原有的資訊資料,使得未授權使用者即使獲得了已加密資訊,因不知解密的方法,無法得知資訊真正的含義,通過這種方式提高網路資料傳輸的安全性,加密演算法常見的有雜湊演算法、HMAC 演算法、簽名、對稱性加密演算法和非對稱性加密演算法,加密演算法也分為可逆和不可逆,比如 md5 就是不可逆加密,只能暴力破解(撞庫),我們在 NodeJS 開發中就是直接使用這些加密演算法,crypto 模組提供了加密功能,包含對 OpenSSL 的雜湊、HMAC、加密、解密、簽名以及驗證功能的一整套封裝,核心模組,使用時不需安裝。

雜湊演算法

雜湊演算法也叫雜湊演算法,用來把任意長度的輸入變換成固定長度的輸出,常見的有 md5sha1 等,這類演算法實現對原資料的轉化過程是否能被稱為加密備受爭議,為了後面敘述方便我們姑且先叫做加密。

檢視雜湊加密演算法的種類
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,常用的引數有 hexBase64hex 代表十六進位制,加密後長度為 32Base64 的結果長度為 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,為加密的演算法,常用 sha1sha256,第二個引數為金鑰。

digest 方法生成的加密結果長度要大於 md5hex 生成的結果長度為 64Base64 生成的結果長度為 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,第一個引數為私鑰,第二個引數為解密的資訊。

簽名

簽名與非對稱性加密非常類似,同樣有公鑰和私鑰,不同的是使用私鑰加密,對方使用公鑰進行解密驗證,以確保這段資料是私鑰的擁有者所發出的原始資料,且在網路中的傳輸過程中未被修改。

crypto 簽名

我們還使用 rsa_public.keyrsa_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 完全可以實現我們的加密需求,也可以將上面的加密方案組合使用實現更復雜的加密方案。

相關文章