前言
非 Crypto-JS 庫
串接五花八門,文章百百種,完全看不懂,那肯定要會用 crypto 這 NodeJS 原生函式庫。
好用線上工具
sha256 ? MD5 ? 到底什麼時候使用
簡單敘述:加密演算法
- sha256 - SHA256是SHA-2下細分出的一種演算法,詳情查閱維基百科。
- MD5 - MD5訊息摘要演算法,詳情查閱維基百科。
Base64 ? Hex ? UTF-8 ? 到底什麼時候使用
簡單敘述:加密後輸出格式
- Base64 - 更優於hex,有區分字母大小寫
- Hex (又稱 Base16) - 無區分大小寫
- UTF-8 - URLEncode 會轉成的格式
Hash - 雜湊函式
單純將字串加密,無後續處理。
主要是 algorithm 常用的有: sha256 / MD5
可以使用 crypto.getHashes() 有多少方式可生成。
const crypto = require("crypto");
const algorithm = 'sha256' // algorithm 算法
const inputEncoding = 'utf8' // 加密格式 常見: "utf8" | "hex" | "base64"
const outputEncoding = 'hex' // 輸出格式 常見: "utf8" | "hex" | "base64"
const message = 'this is message'
const hash = crypto.createHash(algorithm)
.update(message, inputEncoding) // 預設如果是 String 會強制使用utf8編碼。
.digest(outputEncoding) // 加密輸出 hex(base16)
Hmac - 金鑰雜湊訊息鑑別碼
生成一組 String or Buffer 去當作 KEY
雙方都有這把金鑰,自行去驗證後,比照對等相同即可成功。
const crypto = require("crypto");
const algorithm = 'sha256' // algorithm 算法: 可以使用 crypto.getHashes() 有這些加密方式
const inputEncoding = 'utf8' // 加密格式 常見: "utf8" | "hex" | "base64"
const outputEncoding = 'hex' // 輸出格式 常見: "utf8" | "hex" | "base64"
const key = 'thesecret' // **共識金鑰**
const message = 'this is message'
const hash = crypto.createHmac(algorithm, key)
.update(message, inputEncoding) // 預設如果是 String 會強制使用utf8編碼。
.digest(outputEncoding) // 加密輸出 hex(base16)
Sign & Verify - 簽名 與 驗證
openssl生成公私鑰:
$ openssl genrsa -out rsa.pem 2048
$ openssl rsa -in rsa.pem -pubout -out rsa.pub
const fs = require("fs")
const crypto = require("crypto");
const privatePath = `${__dirname}/rsa.pem`
const publicPath = `${__dirname}/rsa.pub`
const outputEncoding = 'hex' // 輸出格式 常見: "utf8" | "hex" | "base64"
const algorithm = 'SHA256' // algorithm 算法: 可以使用 crypto.getHashes() 有這些加密方式
const message = 'this is message'
const privateKey = fs.readFileSync(privatePath, 'utf8'); // 取得密鑰的位置
const publicKey = fs.readFileSync(publicPath, 'utf8'); // 取得密鑰的位置
// 簽名
const signature = crypto.createSign(algorithm)
.update(message)
.sign(privateKey)
.toString(outputEncoding) // 簽名輸出要與驗證器輸出相等
// 驗證器
const verifier = crypto.createVerify(algorithm)
.update(message)
let result = verifier.verify(publicKey, signature, outputEncoding)
console.log(result) // true
上方算是常見使用方式,若介接文件有特殊加解密製作,要進行 Key 及 IV 相關算法要自行製作
重點:不適用每次的串接方式,每間公司串接加解密都不同!
常見的 aes-256-cbc
加、解密器製作
createCipheriv()、createDecipheriv() 這兩組分別為加密器與解密器於 crypto
模組
常見名詞: \
- algorithm - 算法
- key - 密鑰
- iv - 加/解密安全性而生成的隨機數。
本範例 algorithm 使用 aes-256-cbc
:
- AES - 進階加密標準
- 256 - 算法使用 256 bits
- CBC - 加密模式,網路上會看到超多看不懂的密碼學
const crypto = require("crypto");
const algorithm = "aes-256-cbc";
const key = "12345678123456781234567812345678";
const inputEncoding = "utf8" // 加密格式 常見: "utf8" | "hex" | "base64"
const outputEncoding = "hex" // 輸出格式 常見: "utf8" | "hex" | "base64"
const iv = crypto.randomBytes(16)
// 需加密之字串
const message = "Hello There, I should be a secret";
// 加密器
const encrypter = crypto.createCipheriv(algorithm, key, iv);
let encryptedMsg = encrypter.update(message, inputEncoding, outputEncoding);
encryptedMsg += encrypter.final(outputEncoding);
console.log("Encrypter message: " + encryptedMsg);
// 解密器
const decrypter = crypto.createDecipheriv(algorithm , key, iv);
let decryptedMsg = decrypter.update(encryptedMsg, outputEncoding, inputEncoding);
decryptedMsg += decrypter.final(inputEncoding);
console.log("Decrypted message: " + decryptedMsg);
超進階
看專案不同導入不同的 AES 加解密方式
- Key Length: 密鑰長度
- Key: 密鑰本身
- IV: 初始向量
- Mode: 加密模式
- Padding: 填充方式
對稱金鑰加密 - 常用演算法
- 進階加密演算法 AES(Advanced Encryption Standard)
- 三重資料加密演算法3DES(Triple Data Encryption Standard)
演算法 | key 長度(bit) |
---|---|
DES | 64 bit |
3DES | 128、192 bit |
AES | 128、192、256 bit |
解釋上方範例 aes-256-cbc
分別為 前-中-後
- 前 -
AES
固定名詞 這是這個加密法的名稱 - 中 - key 需 8 byte 倍數 32 * 8 = 256, key需32個字元 (UTF-8之後字元總數為32)
- 後 - 這個是最為頭疼的重頭戲 下方會詳情解說
Crypto-JS Modes
- CBC (
Crypto-JS
- default) - CFB
- CTR
- OFB
- ECB
Crypto-JS Padding
- Pkcs7 (Nodjejs
Crypto
setAutoPadding() - default /Crypto-JS
- default) - Iso97971
- AnsiX923
- Iso10126
- ZeroPadding
- NoPadding
setAutoPadding() - 設置自動填充?這是什麼?
重點: setAutoPadding() default: true
深入研究實作 AES 時,Crypto-JS - Block Modes and Padding 參考發現 有分 Modes
和 Padding
若真的有 padding 可深入 padding 切分:GitHub Gist - silicakes/nodeJSCustomPadding.js 範例
IV
IV 加/解密安全性而生成的隨機數 字母數 為 16
// 注意每次輸出都會不同
const iv = crypto.randomBytes(16)
// <Buffer e3 11 6c 96 23 58 bc 95 55 e5 58 01 0d 58 e8 c4>
const strIV = iv.toString("hex")
console.log(strIV);
// e3116c962358bc9555e558010d58e8c4
// 可以發現是 32 個字串 看情境使用 有些廠商 iv 是會給純字串 直接使用 iv 即可 不要用 Nodejs Buffer
console.log(Buffer.from(strIV, 'hex'))
// <Buffer e3 11 6c 96 23 58 bc 95 55 e5 58 01 0d 58 e8 c4>
Modes
ECB - Electronic codebook
const crypto = require("crypto");
const algorithm = "des-ede3"; // 3DES
const key = "0123456789abcd0123456789"; // 3DES 金鑰最長 24字元 * 8 = 192位
const message = "Hello There, I should be a secret"; // 需加密之字串
const inputEncoding = "utf8" // 加密格式 常見: "utf8" | "hex" | "base64"
const outputEncoding = "hex" // 輸出格式 常見: "utf8" | "hex" | "base64"
const encrypter = crypto.createCipheriv(algorithm, key, ''); // ECB 無 IV介入
let encryptedMsg = encrypter.update(message, inputEncoding, outputEncoding);
encryptedMsg += encrypter.final(outputEncoding);
console.log("Encrypter message: " + encryptedMsg);
// 解密器
const decrypter = crypto.createDecipheriv(algorithm , key, '');
let decryptedMsg = decrypter.update(encryptedMsg, outputEncoding, inputEncoding);
decryptedMsg += decrypter.final(inputEncoding);
console.log("Decrypted message: " + decryptedMsg);
CBC - Cipher block chaining
const crypto = require("crypto");
const algorithm = "aes-256-cbc";
const key = "12345678123456781234567812345678"; // 256 bit 最長 32 字母
const message = "Hello There, I should be a secret"; // 需加密之字串
const inputEncoding = "utf8" // 加密格式 常見: "utf8" | "hex" | "base64"
const outputEncoding = "hex" // 輸出格式 常見: "utf8" | "hex" | "base64"
const iv = crypto.randomBytes(16)
// 加密器
const encrypter = crypto.createCipheriv(algorithm, key, iv);
let encryptedMsg = encrypter.update(message, inputEncoding, outputEncoding);
encryptedMsg += encrypter.final(outputEncoding);
console.log("Encrypter message: " + encryptedMsg);
// 解密器
const decrypter = crypto.createDecipheriv(algorithm , key, iv);
let decryptedMsg = decrypter.update(encryptedMsg, outputEncoding, inputEncoding);
decryptedMsg += decrypter.final(inputEncoding);
console.log("Decrypted message: " + decryptedMsg);
CFB - Cipher feedback
const crypto = require("crypto");
const algorithm = 'aes-256-cfb';
const key = "12345678123456781234567812345678"; // 256 bit 最長 32 字母
const message = "Hello There, I should be a secret"; // 需加密之字串
const inputEncoding = "utf8" // 加密格式 常見: "utf8" | "hex" | "base64"
const outputEncoding = "hex" // 輸出格式 常見: "utf8" | "hex" | "base64"
const iv = crypto.randomBytes(16)
// 加密器
const encrypter = crypto.createCipheriv(algorithm, key, iv);
let enc = [iv, encrypter.update(message, inputEncoding)];
enc.push(encrypter.final());
let encryptedMsg = Buffer.concat(enc).toString(outputEncoding); //轉入格式
console.log("Encrypter message: " + encryptedMsg);
// 解密器
const contents = Buffer.from(encryptedMsg, outputEncoding); //轉入格式
const decipherIV = contents.slice(0, 16); // 切 前16 Byte 為 iv
const textBytes = contents.slice(16); // 後面才是加密過後的字串
const decipher = crypto.createDecipheriv(algorithm, key, decipherIV);
let decryptedMsg = decipher.update(textBytes, undefined, inputEncoding);
decryptedMsg += decipher.final(inputEncoding);
console.log("Decrypted message: " + decryptedMsg);
CTR - Counter
const crypto = require("crypto");
const algorithm = 'aes-256-ctr';
const key = "12345678123456781234567812345678"; // 256 bit 最長 32 字母
const message = "Hello There, I should be a secret"; // 需加密之字串
const inputEncoding = "utf8" // 加密格式 常見: "utf8" | "hex" | "base64"
const outputEncoding = "hex" // 輸出格式 常見: "utf8" | "hex" | "base64"
const iv = crypto.randomBytes(16)
// 加密器
const encrypter = crypto.createCipheriv(algorithm, key, '');
let enc = [encrypter.update(message, inputEncoding), encrypter.final()];
let encryptedMsg = Buffer.concat(enc).toString(outputEncoding); //轉入格式
console.log("Encrypter message: " + encryptedMsg);
// 解密器
const contents = Buffer.from(encryptedMsg, outputEncoding); //轉入格式
const textBytes = contents; // 加密過後的字串
const decipher = crypto.createDecipheriv(algorithm, key, '');
let decryptedMsg = decipher.update(textBytes, undefined, inputEncoding);
decryptedMsg += decipher.final(inputEncoding);
console.log("Decrypted message: " + decryptedMsg);
OFB - Output feedback
略
Padding
setAutoPadding(true)預設是 Pkcs7,以下是Zero-Padding 範例
const customPadding = (inputData, padder, blockSize = 256, length = 8) => {
inputData = Buffer.from(inputData, "utf8").toString('hex');
console.log(`before padding \t${inputData}`)
var bitLength = inputData.length * length;
if (bitLength < blockSize) {
for (let i = bitLength; i < blockSize; i += length) { inputData += padder }
}
else if (bitLength > blockSize) {
while ((inputData.length * length) % blockSize != 0) { inputData += padder }
}
console.log(`after padding \t${inputData}`)
return Buffer.from(inputData, 'hex').toString("utf8");
}
const algorithm = "aes-256-cbc";
const key = "12345678123456781234567812345678"
const iv = randomBytes(16)
const inputEncoding = "utf8"
const outputEncoding = "hex"
const encrypt = (plain, key, iv) => {
const cipher = createCipheriv(algorithm, key, iv).setAutoPadding(false);
return cipher.update(Buffer.from(plain, inputEncoding), undefined, outputEncoding) + cipher.final(outputEncoding);
}
const decrypt = (encrypted, key, iv) => {
const decipher = createDecipheriv(algorithm, key, iv).setAutoPadding(false);
return decipher.update(Buffer.from(encrypted, outputEncoding), undefined, inputEncoding) + decipher.final(inputEncoding);
}
console.log(`aes key \t${key}`)
console.log(`init vector \t${iv}`)
let plain = "Hello world"
plain = customPadding(plain, 0) // Zero-Padding 範例
const encrypted = encrypt(plain, key, iv)
console.log(`Encrypted \t${encrypted}`)
const decrypted = decrypt(encrypted, key, iv);
console.log(`Decrypted \t${decrypted}`)
常見的Padding
- Pkcs7
- Iso97971
- AnsiX923
- Iso10126
- ZeroPadding
- NoPadding
番外小工具
Object 好好排順序~
// 常常會有 Object 的 Key 要排順序
const sortByKey = (data) => Object.keys(data).sort().reduce((obj, key) => { obj[key] = data[key]; return obj; }, {})
let data = { rrr:123123, qqq:456456, aaa:789789 }
console.log(sortByKey(data)) // 輸出:{ aaa: 123123, qqq: 456456, rrr: 789789 }
// 以下這兩種通常在串接文件中就會講說不能使用特殊符號 , 或 | 這樣
const valuesToString = (data) => Object.values(a).toString()
console.log(valuesToString(data)) // 輸出:123123,456456,789789
const commaToVerticalBar = (data) => Object.values(a).toString().replace(/,/g, "|")
console.log(commaToVerticalBar(data)) // 輸出:123123|456456|789789
URLEncode - 轉成 UTF-8,雖然不是 Crypto 庫但也十分常使用於加密前
let data = { name: '測試', URL: 'http://example.com'}
console.log(data)
// {name: '測試', URL: 'http://example.com'}
const noUrlencode = (data) => {
var str = [];
for (var k in data) { str.push(k + "=" + data[k]) }
return str.join("&")
}
console.log(noUrlencode(data));
// 'name=測試&URL=http://example.com'
let encodeData = encodeURI(noUrlencode(data))
console.log(encodeData)
// 'name=%E6%B8%AC%E8%A9%A6&URL=http://example.com'
let decodeData = decodeURI(encodeData)
console.log(decodeData)
// 'name=測試&URL=http://example.com'
文獻參考
總結
組資料算是串接廠商非常常見的事情,深入探討 Cipher 模組,目的就是更暸解這 base on Node 的功能,Cipher-JS固方便,但Cipher更彈性,了解它讓串接更順利。