鐵人賽 Day 17 Node.js 如何生成 RSA 加解密

文章目錄
本系列文章所討論的 JavaScript 資安與逆向工程技術,旨在分享知識、探討防禦之道,並促進技術交流。
所有內容僅供學術研究與學習,請勿用於任何非法或不道德的行為。
讀者應對自己的行為負完全責任。尊重法律與道德規範是所有技術人員應共同遵守的準則。
在前面文章我們已經介紹過 RSA 今天直接實戰使用 jsencrypt、node-forge 來進行加解密。
安裝jsencrypt
在開始之前需要先安裝crypto-js
npm install encrypt
生成公鑰及私鑰
const JSEncrypt = require('jsencrypt');
const encryptor = new JSEncrypt({ default_key_size: 2048 });
encryptor.getKey(() => {
// 獲取生成的公鑰和私鑰
const publicKey = encryptor.getPublicKey();
const privateKey = encryptor.getPrivateKey();
console.log('--- 公鑰 (Public Key) ---');
console.log(publicKey);
console.log('--- 私鑰 (Private Key) ---');
console.log(privateKey);
});
使用公鑰進行加密
const JSEncrypt = require('jsencrypt');
const publicKey = `-----BEGIN PUBLIC KEY-----MIIBITANBgkqhkiG9w0BAQEFAAOCAQ4AMIIBCQKCAQBoEVq72+LuWi38uTUDesQxYIuaSq41/pLx1Xh2ax4f7S/f2klABJ+yrz3RoS5MI0AiKQs5J77OxMUITL6lBHbcU9L3fb2MnYq0X/SWlxarMBizxboIvj1aoHZhTHFXsKP8+0q34gyP2iyDEIkdv6N3HLK0wIxRhUoF5/TnJfCllf8weLHkml6TlYOG86EKjTqBpxjhPIx7JfNbIVvGPyZ0s+O6U1DYspSvRduyFkPzanvlB9qV5ZOCtXqF4Y7lxcg5GmaiHFP/p79EKMoJyoO591+dP3cvVU82TVXDp/S5+fFskdl9vTNhUGIGeXxqFOXL0i46nG8LJ/klk7y8UV9bAgMBAAE=-----END PUBLIC KEY-----`
const plaintext = '這是一段需要加密的訊息';
const encryptor = new JSEncrypt();
encryptor.setPublicKey(publicKey);
const encrypted = encryptor.encrypt(plaintext);
console.log("加密結果:", encrypted);
// YzJ/y+2AUqoZzQQNgzNWTQQ4UXwk5KaSrpCN/NL8YWzG8AazB/nabCwYjsZ8oIqPGbM+dK/mifcmOcs5kHyTV0aDmBlhcF/bbnqhIeaZn0+GOT+2Ueykdqyt2U0ACz+l5YWz02sdzSNI/UZHhhf0nCjSPDHAv2qHeStAdWhZenuM2ZhyZRXrVJe6QZEs9eaOiKVHtTk0QczWoiKt/KU03VTabOV5Ky45axYFa4kf4KpWeC9Gsqg2nlt/W4WJ6Ag6AIx+uOI9AnWrKezPNKjCRQ+GH+SHuqF//xniBb4v4m0b07MCSfMyMLS5bbmBVlWvcwEPjeiLHP2Hdn/CPU2EZQ==
使用私鑰進行解密
const JSEncrypt = require('jsencrypt');
const privateKey = `-----BEGIN RSA PRIVATE KEY-----MIIEoQIBAAKCAQBoEVq72+LuWi38uTUDesQxYIuaSq41/pLx1Xh2ax4f7S/f2klABJ+yrz3RoS5MI0AiKQs5J77OxMUITL6lBHbcU9L3fb2MnYq0X/SWlxarMBizxboIvj1aoHZhTHFXsKP8+0q34gyP2iyDEIkdv6N3HLK0wIxRhUoF5/TnJfCllf8weLHkml6TlYOG86EKjTqBpxjhPIx7JfNbIVvGPyZ0s+O6U1DYspSvRduyFkPzanvlB9qV5ZOCtXqF4Y7lxcg5GmaiHFP/p79EKMoJyoO591+dP3cvVU82TVXDp/S5+fFskdl9vTNhUGIGeXxqFOXL0i46nG8LJ/klk7y8UV9bAgMBAAECggEAKm2A33wrTd/IRfPAUFXZ7QOeht4RnoPWpu/QN/89/eg2j34wRQBdl3zoqDGdbX8lo4e2QqwYl7YTWmnng+GJEBTAuxQxlkWYiidg0ZBxtoNaXtirGutsmik3ej2vLAAhK3/MG6H2WyOo6BpyvIUoAOTbWuPxkT7VSgkiiKaoMOmqoPC9+wRv3smVnbDKZC0EmngWxFGhYAh2NyH8ibtxqXwyNBVNRYij2gB7cgzqJ+xHiinXA6w3xvD7kIhOnBJwdSLwwnb6KPnlkdKhJk+MDSYiWvvTNH3PlS6AfgrsAWPyRrWL7RP0Hdth3HWcCvefFVru7zPEH5EDQ4MDV14P0QKBgQC/fSQQv14I7d6oHQ5S+uf6051o6iNyWrElmyo3mvgBb2SDB8myulJrNN4rjWLh3i3RgyEfZG1J0GOu7Ipj/YWfNVUEt98c0IKePvjm/fWFiGm13y/b0MCSx/AcyTWUDjHes2FsTkwcA7+L6esKuELkPI840eTneY5rbrjrWQKxWQKBgQCLIKEbewCnhDlA2N7uwdyGWJh+KveDaH+0aVcBGsUJqsdYzMGnmJWrWFmqnpbZLcUvAEjP7n69xW+xgSO+dDs/45za4iBZ2+S6gQ95VmLeKqJLzqdj8uNguE+e0jsEWmshD2cIgnuvq4XviC/kXc5YeItcZ9abnANmD/AlpqFr0wKBgA1WlS2Jdu5eS1UgeP/0tDX3iY5mSMPNZ2t8LGulIsNO1AyAfV8ytUz8aMFV3t5m0IA4hxUdtLMgjeEAXv9qCGW3nE1w1Vy3dXG6ZzIH3JNJljtx6W6BUvimbqZCqbW/a1/c1NtrdMe6xxvi1llvzlEBmuRVUoGBKRd4pe7Wy2Y5AoGACQoLrZ9mQXwDxETS5yxNSaVD8x6TikQl1/DoKDg3CRPBc/GJu3vcbY+F8+Ht5xpkL1OTZ38VWPsU8LF1QxCGMPZ24HnEpFH3IG72NGn6bnjSpp48ne/P+h6/fZAnKXc+cp1vkkv4AUfhodh1VB8MIw9h7pUIin+ucNkkPy3+WuUCgYBDU4ZbwnEVHNwwfYuf8Ec8PVhQ+Ba7wUtvMBFusj6fEDocnlARM8tVItoz6N1TkaT6jRjEG5+tMiQaFT6Juy0kWUfw3JNflvYD8ZYpS4iYxQmk3nCzTiOJm0S/mOnv0pmLVEafwXRnJ2TQpjbKeEJDd29leBaKaJjPIxgZu9wJwA==-----END RSA PRIVATE KEY-----`
const encrypted = `YzJ/y+2AUqoZzQQNgzNWTQQ4UXwk5KaSrpCN/NL8YWzG8AazB/nabCwYjsZ8oIqPGbM+dK/mifcmOcs5kHyTV0aDmBlhcF/bbnqhIeaZn0+GOT+2Ueykdqyt2U0ACz+l5YWz02sdzSNI/UZHhhf0nCjSPDHAv2qHeStAdWhZenuM2ZhyZRXrVJe6QZEs9eaOiKVHtTk0QczWoiKt/KU03VTabOV5Ky45axYFa4kf4KpWeC9Gsqg2nlt/W4WJ6Ag6AIx+uOI9AnWrKezPNKjCRQ+GH+SHuqF//xniBb4v4m0b07MCSfMyMLS5bbmBVlWvcwEPjeiLHP2Hdn/CPU2EZQ==`;
const encryptor = new JSEncrypt();
encryptor.setPrivateKey(privateKey);
const decrypted = encryptor.decrypt(encrypted);
console.log("解密結果:", decrypted);
// 這是一段需要加密的訊息
安裝node-forge
在開始之前需要先安裝crypto-js
npm install node-forge
生成公鑰及私鑰
const forge = require('node-forge');
forge.pki.rsa.generateKeyPair({ bits: 2048, e: 0x10001 }, (err, keypair) => {
if (err) {
console.error('金鑰生成失敗:', err);
return;
}
const publicKey = forge.pki.publicKeyToPem(keypair.publicKey);
const privateKey = forge.pki.privateKeyToPem(keypair.privateKey);
console.log('--- 公鑰 (Public Key) ---');
console.log(publicKey);
console.log('--- 私鑰 (Private Key) ---');
console.log(privateKey);
});
使用公鑰進行加密
const forge = require('node-forge');
const publicPem = `-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoje4RMt/F4Liza0fPWWH40dwzbRo25SQ6ai159IZVsmafBKgWpZvt3sLP1RtUuqOkZbFmCQk7ImndBu6jLyNBgn9BKq3PytIzuPrFbm3MhtvJpsD+r3OAgd9eLKkwiKLNCOwvDDsjVKhdLDEFGboNwMLatut2u2wS2q4b47mh7HRVuA20NfcR7Xe0O+1hUXd2C2xp16ATqIH7+gBBBK2hA4vtYT63VTle+kLOf42202KPETLGy4N9pwr2rGrI2UBTEKXsG0BGc8h3GQMyaxiFZgdKeC9RLYsahG1irq44WmMaYGAsIrRFBRAaVPgB31/zzXKEq2BWM8VGPKsk/7zhwIDAQAB-----END PUBLIC KEY-----`
const publicKey = forge.pki.publicKeyFromPem(publicPem);
const plaintext = '這是一段需要加密的訊息';
const encrypted = publicKey.encrypt(plaintext, 'RSAES-PKCS1-V1_5');
console.log(forge.util.encode64(encrypted));
// BPIo4dzeKfRYhGQBVtZ3/Fp8Jn+7J1/yMkU2EvvMIf0JzFDaQs0tX27hpD138CLOqmtuVGUDUGlX5vHM6N0lL9cbaYb6VlYvVV+sANsrRPx2nuSlXCGPqR07XnFK+kIx+0Ek8132G9iLCm4kRBIe6yOaImOoMgJOAzTVXGRBDL1HxKCzFqV1G5sRUPG5RoFMHs2u+KaC3eWK13kMTRz0iF2Q4Jr9vIbD38U+ws17zECB3GcukKuLGcMJwIr8yDAX84nO6rlJQveRZz+c8VH34wc1yorumJgPTR652pIx/jcZQW+rDTEpRjHVXTcyucSmf727uqn54HrVD7p6xzMz9A==
使用私鑰進行解密
const forge = require('node-forge');
const privatePem = `-----BEGIN RSA PRIVATE KEY-----MIIEowIBAAKCAQEAoje4RMt/F4Liza0fPWWH40dwzbRo25SQ6ai159IZVsmafBKgWpZvt3sLP1RtUuqOkZbFmCQk7ImndBu6jLyNBgn9BKq3PytIzuPrFbm3MhtvJpsD+r3OAgd9eLKkwiKLNCOwvDDsjVKhdLDEFGboNwMLatut2u2wS2q4b47mh7HRVuA20NfcR7Xe0O+1hUXd2C2xp16ATqIH7+gBBBK2hA4vtYT63VTle+kLOf42202KPETLGy4N9pwr2rGrI2UBTEKXsG0BGc8h3GQMyaxiFZgdKeC9RLYsahG1irq44WmMaYGAsIrRFBRAaVPgB31/zzXKEq2BWM8VGPKsk/7zhwIDAQABAoIBAAHTos2ThRDctnUed/WXCRq0GT8Xwv6GA8O91J6RrRtfFdBD3W5nt0ZXql7CMqD2ZziYfA0kiw3m1u3lygn/rY7d2e2Nhpa9hApgYqtR9hAoYu4KXNccAA/hybXsPRK6lNq3dCwKWP+WaJ8Jxot8RS0paZ2Ab8rBVnTi9MS81Te98kXWUAtn6iFLj80b1m6M82H3rLi23PLtU1Pt+miitTUhb5PUGz557yws88/xuMSU7P7Z2RendGRIvwu9lyqz/nDEKQXjcOaTzWvWAnkI02L6ajFRzbze4KH2/10dkP85875/yPeBDRH3YZjVOH52fdYRZOxaQpAS12eqHmAij1ECgYEA2j9F6iLjYm9As/NUXy45lVBT7F0aQi0zJmwkKXBTxnNZ/w+6gvesXtyRB85M5KGdmwUl77CN2eOBAkM6ndJRt6dEEVWVX1iDn//rqXbyCGcunvX0Mf8bpG3ccq23NsLhGs3yeRQ+GBka7/D19GTR3pRx0IXsiQ0RViaozhWQqyUCgYEAvkdFAdmtLc4oeQdbq4noOwfUrN+SD7iUQYNP3ZuvCRjOTrwZ4SXFF5Tk2i1qT1hRupEEg3sWIL1gxYU01FNyoX09uxBywwyr8P1GVGRQwG1EUTM7REYVgA/DBcXL98je0/dMnQRbyUchtExgPDjgQJoUG6HZoMP4NAR940Mi2jsCgYA5fqu7d79xYh+oiHT0sifHVycUwJ43StMLzzdjl0zqnd7am9klwXuuberjJI0LOixb92o8D8X+3blR0EYz54FRoYYEDwjP2nCqiPAbb9NpNU2J4P+Y4Dz2/14w8+StT3+f4Rt8nEGTdaGsDk2tOZY0TeUB6xYaMNAdYwfbasD25QKBgQCmPcoypaXCQT3nD+CtbF1zRZIwP1xzU8ZjvoAGk8BHaF7HN1w88yzRP4r6e3dIP1eYcp7I6L8ad5qWt5zCbO3X3X4Tc3lwpB+M+xt5G/PZSx3G+8WqDrk2G489+eQMZnkYJBXRkT9ICVePGLNzqhVzG6q5wZxL8ytk9w+tgLhwhQKBgGHvX9w2DTbi3tNmM3x8xRFkJw3g/UQ0xiNPN3W2nn3gAdXby7ahLjst59AJC8PDv/Hr/Ja+syY7XhoWq+C2f0EmutbrNcR5GNF4Q8E5n3HBcOAGr8avyYjNjJk0/pNVx274sU66vvom5STry3Up8zHK6p5zPb+zm3dVQxM8i79D-----END RSA PRIVATE KEY-----`
const privateKey = forge.pki.privateKeyFromPem(privatePem);
const encrypted = `XLiycApBps9SbhuVkKyZUNz/n2hnyjRWmSCeZ/2URT41RckzFoQ9QkrAAdSKGmx4zVLSPknBvVmwDDSTDTIT35JxzU3T7ImoFFWX6cvJqlSaCvb9HFLiB3uPBvhbjadBVwAGDM+uHSBc1yMWeO7OLroJkTYQ0JDUk0dMHa+UR1JFLQHzRI0tsqISi1wMOc9l4g3DhkLN7bmmVGA8otRn+747AnmFQt0mH2hLX1LkTR53c7AqkKhE3ioERZVIpUM3p6/JbL2rMJkWmMJNToN51+eHAeykoP55dCqPrydy7M/1HPkv0uis5PTBmbHyOQjDUh7wjAvj4YB/L6lmAouSjw==`
const decryptedBytes = privateKey.decrypt(forge.util.decode64(encrypted), 'RSAES-PKCS1-V1_5');
const decrypted = forge.util.decodeUtf8(decryptedBytes);
console.log("解密結果:", decrypted);
// 這是一段需要加密的訊息
Padding填充模式
眼尖的你一定發現了,在使用node-forge進行加解密的時候會加上第二個參數RSAES-PKCS1-V1_5,實際使用RSA時會搭配一個「填充演算法」來確保隨機性與安全性。
RSAES-PKCS1-v1_5
在加密前會在明文前面填入固定格式與隨機 bytes,保證輸入給 RSA 的數字大小隨機化,很多套件(如 jsencrypt)只支援這個,所以node-forge使用此模式進行RSA加密的內容可以透過jsencrypt進行解密。
RSA-OAEP
安全性比 v1.5 高很多,能抵抗大部分已知攻擊,但和jsencrypt卻不能互通。