【开发笔记】关于那些加密(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 公私钥格式的特定要求。小心鉴别格式上的匹配,实作起来应该就不会有什么问题了。