返回
Featured image of post NodeJS - Crypto

NodeJS - Crypto

NodeJS Crypto 筆記,非 `Crypto-JS`,串接廠商加密必學

前言

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)
DES64 bit
3DES128、192 bit
AES128、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 參考發現 有分 ModesPadding

若真的有 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

維基百科 - Electronic codebook (ECB) 加密
維基百科 - Electronic codebook (ECB) 加密
維基百科 - Electronic codebook (ECB) 解密
維基百科 - Electronic codebook (ECB) 解密

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

維基百科 - Cipher block chaining (CBC) 加密
維基百科 - Cipher block chaining (CBC) 加密
維基百科 - Cipher block chaining (CBC) 解密
維基百科 - Cipher block chaining (CBC) 解密

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

維基百科 - Cipher feedback (CFB) 加密
維基百科 - Cipher feedback (CFB) 加密
維基百科 - Cipher feedback (CFB) 解密
維基百科 - Cipher feedback (CFB) 解密

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

維基百科 - Counter (CTR) 加密
維基百科 - Counter (CTR) 加密
維基百科 - Counter (CTR) 解密
維基百科 - Counter (CTR) 解密

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

維基百科 - Output feedback (OFB) 加密
維基百科 - Output feedback (OFB) 加密
維基百科 - Output feedback (OFB) 解密
維基百科 - Output feedback (OFB) 解密

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更彈性,了解它讓串接更順利。

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus