【開發筆記】關於那些加密(Cryptography)的二三事
作為全棧工程師,總是會遇上需要將數據內容加密的情況。簡單來說,加密就是把本來可以閱讀的明文,變成替代的記號,使經手的中間人無法知悉數據的確切內容
對稱式與非對稱式加密
作為全棧工程師,總是會遇上需要將數據內容加密的情況。簡單來說,加密就是把本來可以閱讀的明文,變成替代的記號,使經手的中間人無法知悉數據的確切內容。
至於加密的方式,根據應用上的需要,能夠有多種不同的操作方式。如果加密與解密的關鍵訊息(如:passphrase 或是 pem 鑰匙)都是相同的,這一類的操作屬於對稱式加密。反之如果加密與解密的關鍵訊息刻意區分開來,則稱之為非對稱式加密。
對稱式加密比較容易理解。就像門鎖的鑰匙,上鎖與開鎖都是同一把,既能夠開門也能夠鎖門。所以鑰匙的保管是非常要緊的。一但外流,會變成任何鑰匙的持有者都能夠輕易將該鑰匙編碼過的訊息解密。
而非對稱式的加密則是把上鎖與開鎖的鑰匙分開,開鎖的鑰匙只有一把,自己保存解密時使用。而上鎖的鑰匙可以多把分享,因為任何明文訊息的持有者使用上鎖的鑰匙加密後,唯獨只有持解鎖鑰匙的人能夠進行解密,閱覽數據內容。
OpenSSL 創建公私鑰
至於何為分開上鎖與開鎖的鑰匙?從 openssl 創建公私鑰的流程可以略見端倪。
# 第一步:以 rsa 算法,創建 1024 bits 大小的私鑰
openssl genrsa -out privateKey.pem 1024
這時會得到一個 pkcs1 格式的 pem 私鑰如下。
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCVynWk12Uadv+oeLtYANjFaC8LmqqeJI3MwaT71fQ3dEU
....
..
.
PEhOJvRBe6SLlkkaldXxfOvLm/grCWC/DM+wLXqbrq0=
-----END RSA PRIVATE KEY-----
接著使用上述步驟取得的私鑰,生成加密用的公鑰。
# 第二步:生成公鑰。留意需要 DER 格式的公鑰,僅需將 PEM 改成 DER 即可。
openssl rsa -in privateKey.pem -out publicKey.pem -pubout -outform PEM
透過 pico 開啟私鑰 pem,可以看到這是一個 pkcs8 格式的 pem 文字檔案。
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCVynWk12Uadv+oeLtYANjFaC8L
...
..
.
Bv7to4vC5WoQ2VJ4bQIDAQAB
-----END PUBLIC KEY-----
留意 pkcs1 在標題頭有 RSA 提示,pkcs8 則無。因此當需要 pkcs8 格式的私鑰時,也可以透過 openssl 來轉換。
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in privateKey.pem -out pkcs8.privateKey.pem
從上述的步驟可以發現,公鑰是以私鑰為基礎衍生的簡化副產品。因此公鑰外流之後,遭到推敲重現私鑰的困難度會大上很多。這也是為什麼雖然實作上也能夠私鑰加密公鑰解密,但在安全性的考量上。公鑰加密私鑰解密才是操作非對稱加密的正確姿勢。
node.js 程式碼實作公私鑰加密解密
下方範例引用 node-forge 來實現。
const forge = require('node-forge');
const pki = forge.pki;
const rsa = pki.rsa;
// node-forge API 同步生成 keypair(*也可以透過上述 openssl 生成 pem 公私鑰,然後 fs 載入)
const keypair = rsa.generateKeyPair({ bits: 1024, e: 0x10001 });
// 轉換成 PEM 格式
const pem_pri = pki.privateKeyToPem(keypair.privateKey); // pkcs1
console.log(pem_pri);
const pem_pub = pki.publicKeyToPem(keypair.publicKey); // pkcs8
console.log(pem_pub);
// 將 PEM 格式轉換成 FORGE 原生的形式
const privateKey = pki.privateKeyFromPem(pem_pri);
const publicKey = pki.publicKeyFromPem(pem_pub);
const text = 'MESSAGE_TO_BE_ENCRYPTED';
const encrypted = publicKey.encrypt(text);
const cipher = forge.util.encode64(encrypted);
console.log(cipher);
//解密需要帶入 Bytes 資料,因此當拿到的資料是 base64 或 hex 時,需要先進行轉換
const bytes = forge.util.decode64(cipher);
//const bytes = forge.util.hexToBytes(hex);
const decrypted = privateKey.decrypt(bytes);
console.log(decrypted);
對稱式加密算法採用 AES 居多
至於對稱式加密算法從最早 DES/3DES 到如今主流採用 AES,在程式碼的實現上簡單許多。加密解密的關鍵訊息則是以約定好的 passphrase 為主。以下範例引用 crypto-js 來實現。
const CryptoJS = require("crypto-js");
const passphrase = 'KEY_TO_ENCRYPTED_DECRYPTED';
const message = 'MESSAGE_TO_BE_ENCRYPTED';
const cipher = CryptoJS.AES.encrypt(message, passphrase).toString();
console.log(cipher);
const bytes = CryptoJS.AES.decrypt(cipher, passphrase);
const _message = bytes.toString(CryptoJS.enc.Utf8);
console.log(_message);
PKCS1/PKCS8/OPENSSL 格式上的區別
其實只要熟悉流程,無論對稱或非對稱的加密解密,都是很單純的固定流程。比較需要留意的就是根據應用場合的需要。有時候會有 PEM 公私鑰格式的特定要求。小心鑒別格式上的匹配,實作起來應該就不會有什麼問題了。