前言

前陣子用 Node.js 開發時需要用一組加解密的 key 來將某個 url 的 query parameters 加密,就重新回頭研究一下對稱加密與非對稱加密的差異和手段,最後統一用 Node.js 的套件來簡單實作,留下此篇文章以供未來參考

密碼學在學什麼?

密碼學的目標可以簡單列出底下三個:

  • 資訊保密: 確保只有授權者才能取得訊息
  • 完整性驗證: 確保資訊沒有遭到算改
  • 身分驗證: 驗證傳送方與接受方的身分

密碼學主要分為三種,先講結論:

  • Encode(編碼)
    • Two way 的轉換,只是編碼格式的轉換,不需要密碼,所以不算加密,沒有任何資安防護能力
  • Encrypt(加密)
    • Two way 的轉換,需要密碼介入,可分成對稱加密與非對稱加密,前者是同一把金鑰,後者是有一組公私鑰,有防護資安的能力
  • Hash(雜湊)
    • One way 的轉換(不可逆),應用 Hashing 的演算法將待加密的字串轉換成一個獨一無二的 hash digest

Encode(編碼)

  • 只是換個方式來呈現資料
  • 沒有加密,所以沒有安全性
  • 只是各種平台使用數據的載體,ex: QRCode、UTF-8
  • 為了配合網路傳輸的標準規範(RFC,Requests For Comment),有時就必須透過編碼後才能傳送

Base64

是一種基於 64 個字符來表示的方法

Base64 中的 64 其實是有含意的,他會把資料轉成:

a~z (26)
A~Z (26)
0~9 (10)
+ (1)
/ (1)

base64 的 64 就是上面五種字元的所有可能(26+26+10+1+1=64)

打開瀏覽器的 console 介面,用 Web API 就可以開箱及用

Huffman Coding(霍夫曼編碼)

是一種無損數據壓縮的編碼方式

  • 根據整組資料中符號出現的頻率高低,決定如何給符號編碼。如果符號出現的頻率越高,則給符號的碼越短,相反符號的號碼越長
  • 可以透過這個網站 –> Huffman Tree Generator來了解 Huffman 的編碼 tree

Encrypt(加密)

將明文資訊轉變成難以讀取的密文

Caesar Cipher(凱薩加密法)

  • 凱撒密碼是一種替換加密技術
  • 明文中的所有字母都在字母表上向後(或向前)按照一個固定數目進行偏移後被替換成密文。
  • 已經不再使用,因為英文字母只有 26 個,全部破解然後列出一個彩虹表(Rainbow Table),看哪一個看得懂就破解了

AES(Advanced Encryption Standard)

  • 它是一個區塊加密的演算法
    • 所謂的「區塊加密」就是把明文拆成多個區段,然後分別加密再組合起來
    • 加密的方式是連續對每個 128 bit 的數據應用一系列的數學變換
  • 也是一種 對稱加密演算法(加密解密都是同個 key)
  • AES 有分成幾種模式,像是 ECB、CBC、CFB,其中CBC 是最安全的(記住這個就好)
  • 是業界常用的加密方法,速度快
  • 忘記密碼基本上就沒救了,因為排列組合太多,AES128 加密需要 10^18 年才能解開
  • 因為加密的數學計算量較低,所以小型裝置如筆電、手機都可以完成加密運算

RSA

  • 縮寫來自三位 MIT 科學家的名字(Rivest, Shamir, and Adleman)
  • 是一種 非對稱加密演算法(加密解密是不同 key,一個公鑰一個私鑰)
  • 概念很簡單,應用 兩個很大的質數 p 與 q 相乘出 N 很容易,但從 N 要找回原本 p 與 q 是極度困難的任務 的概念來做加密
  • 數學計算量較高,所以只適用於小量數據

Hash(雜湊)

  • 將資料內容打亂、混和
  • 無法從 hash 回推解密
  • 舉例來說,身分證號碼最後一碼是驗證碼,但因為只有 0-9,所以極有可能會雜湊碰撞(collection)

MD5 (Message-Digest Algorithm)

  • 可以產生出一個128 bit(16 byte)的雜湊值,用於確保資料傳輸是完整一致的
  • 兩個不同的明文不會得到相同的輸出值
  • 輸出值,不能得到原始的明文,即其過程不可逆
  • 可以判斷檔案內容是否一樣,看雜湊值是不是一樣
  • 也可以確認網站上下載下來的檔案是不是雜湊值一樣,例如下載任何套件的情境,這樣就能確保不會載到病毒
  • 即使僅改變一個字,也會導致結果差很多
  • 業界已經較禁止使用了, 因為已經證明容易破解! 因為 MD5 無法防止碰撞,駭客可以上傳一個內容不同但最終雜湊值相同的檔案

SHA384 (Secure Hash Algorithm)

  • 可以理解成更複雜的 MD5
  • 會 output 出 384 個字元,複雜度較高
  • 可以驗證檔案的完整性
  • 保存使用者密碼
    • 後端資料庫應該要存的是 Hash 過後的密碼
    • 這樣即使網站被攻擊後也無法被竊取解密
  • 如果某線上服務忘記密碼回傳的是明文,那就代表該網站後端存的是明文密碼(怕),因為 Hash 理論上無法被回推,所以代表資料庫一定也是存明碼,否則無法將當初設定的密碼明文寄回
    • 理論上應該要導去重新設定密碼的頁面,重新讓使用者輸入密碼

bcrypt

  • bcrypt 能夠將一個字串做雜湊加密
  • 專門為密碼而設計的雜湊函式,為避免每次雜湊出來的值都是一樣的,有加鹽機制。
    • 其中有個參數叫saltRounds 是在密碼學中的加鹽(salt)
    • 加鹽的意思是在要加密的字串中加特定的字符,打亂原始的字符串,使其生成的 Hash value 產生變化
    • 參數越高,代表加鹽次數愈多愈安全,相對的加密時間就越長
  • 比 SHA 更安全,但耗時也較久,所以要用 bcrypt 還是 SHA 是看情況而定

Node.js 的各種加密方式

Crypto

Encrypt & Decrypt

Output Encoding 設定成 hex,所以只會有 16 種符號(a-f, 0-9)

一旦執行cipher.final(),cipher 物件就不能再加密文字

const crypto = require("crypto");

// settings
const initKey = crypto.randomBytes(16);
const securityKey = crypto.randomBytes(32);
const algorithm = "aes-256-cbc";

const message = "Hello I am Madi";

// Cipher for encrypt data
const cipher = crypto.createCipheriv(algorithm, securityKey, initKey);
let encryptedMsg = cipher.update(message, "utf-8", "hex"); // input-encoding: utf-8, output encoding: hex
encryptedMsg += cipher.final("hex");

console.log("Encrypted: ", encryptedMsg); // Encrypted:  c64dc767eedbf6f18696b548537d01d2

// Decipher for decrypted data
const decipher = crypto.createDecipheriv(algorithm, securityKey, initKey);
let decryptedMsg = decipher.update(encryptedMsg, "hex", "utf-8"); // input-encoding: hex, output encoding: utf-8
decryptedMsg += decipher.final("utf-8");

console.log("Decrypted: ", decryptedMsg); // Decrypted:  Hello I am Madi

Encrypt & Decrypt with salt

const crypto = require("crypto");

const salt = crypto.randomBytes(3); // Create salt

const message = "Hello, I am Madi";

// USe PBKDF2 function to derive secret
crypto.pbkdf2(message, salt, 10000, 32, "sha256", (err, derivedKey) => {
  if (err) throw err;

  const algorithm = "aes-256-cbc";
  console.log(`DerivedKey: ${derivedKey.toString("hex")}`); // DerivedKey: 2f41bbe3a59652f5c5c8a727366db3cc7162a0846fae720687a9ab880928f70e
  console.log(`Salt: ${salt.toString("hex")}`); // Salt: cdfbf3

  // Encrypt
  const initKey = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv(algorithm, derivedKey, initKey);
  let encrypted = cipher.update(message, "utf-8", "hex");
  encrypted += cipher.final("hex");

  console.log(`Message: ${message}`); // Message: Hello, I am Madi
  console.log(`Encrypted: ${encrypted}`); // Encrypted: db846c67a25645b51d7fe6d2b00b61c9e7084d6256a20556cbef60d0590ca6fa

  // Decrypt
  const decipher = crypto.createDecipheriv(algorithm, derivedKey, initKey);
  let decrypted = decipher.update(encrypted, "hex", "utf-8");
  decrypted += decipher.final("utf-8");

  console.log(`Decrypted: ${decrypted}`); // Decrypted: Hello, I am Madi
});

Hash

Output Encoding 設定成 hex,所以只會有 16 種符號(a-f, 0-9)

const crypto = require("crypto");

const hash = crypto.createHash("sha256"); // 256 bits
const message = "Hello, I am Madi";
const hashedMsg = hash.update(message).digest("hex");

console.log(hashedMsg); // 689e9ce3a07a10a090b9c387c4b08cfac7b9a5edd55b416c81f44248983172ab

Hash 轉 hex 長度會增為 2 倍,因為每個 byte(8 bits)會被表示成兩個十六進制的字元(4 bits each)

Hash 轉 Base64 長度會增為 4/3 倍,因為每 3 個 byte 會被表示成 4 個 base64 字元,如果未滿 3 的倍數,則會用=做 padding,這部分在使用 JWT 時常會看到

結論

  • encode、encrypt、hash 三者可以綜合使用:
    • 壓縮檔加密: encode + encrypt
    • Https: RSA + AES
    • 數位簽章: hash + RSA
    • JWT: hash + encode
  • git 在做 commit 的時候也是使用 SHA1 的方式,產生一組唯一的 id
  • 自己的密碼盡量不要重複,太多密碼不好記,可以考慮用 keePass 來管理