Node.js加密数据存IndexedDB,WebCrypto离线解密及RSA密钥复用方法
Hey there! Let's walk through each of your questions step by step with working code examples and key notes to make sure everything works seamlessly between Node.js and WebCrypto.
The trick here is to use an algorithm supported by both environments—AES-GCM is a perfect pick since it’s widely supported and provides authenticated encryption. You’ll need to ensure the key, IV (initialization vector), and encryption parameters match exactly on both sides.
Node.js Encryption Code
We’ll use Node’s built-in crypto module to generate a random AES key and IV, then encrypt the data. We’ll export values as base64 strings for easy transfer to the browser:
const crypto = require('crypto'); async function encryptDataNode(plaintext) { // Generate 256-bit AES key const key = crypto.randomBytes(32); // Generate 12-byte IV (recommended standard for AES-GCM) const iv = crypto.randomBytes(12); // Create cipher and encrypt const cipher = crypto.createCipheriv('aes-256-gcm', key, iv); let encrypted = cipher.update(plaintext, 'utf8', 'base64'); encrypted += cipher.final('base64'); // Grab authentication tag (required for valid decryption) const authTag = cipher.getAuthTag().toString('base64'); return { key: key.toString('base64'), iv: iv.toString('base64'), ciphertext: encrypted, authTag: authTag }; } // Usage example: encryptDataNode('Hello from Node.js!').then(result => { console.log('Encryption output:', result); // Pass these values to your browser frontend });
WebCrypto Decryption Code
In the browser, convert the base64 strings back to ArrayBuffers, import the key, then decrypt using WebCrypto:
async function decryptDataWeb(ciphertextB64, keyB64, ivB64, authTagB64) { // Convert base64 to Uint8Array (compatible with WebCrypto) const ciphertext = Uint8Array.from(atob(ciphertextB64), c => c.charCodeAt(0)); const key = Uint8Array.from(atob(keyB64), c => c.charCodeAt(0)); const iv = Uint8Array.from(atob(ivB64), c => c.charCodeAt(0)); const authTag = Uint8Array.from(atob(authTagB64), c => c.charCodeAt(0)); // Import AES key into WebCrypto const cryptoKey = await window.crypto.subtle.importKey( 'raw', key, { name: 'AES-GCM', length: 256 }, false, ['decrypt'] ); // Perform decryption const decryptedBuffer = await window.crypto.subtle.decrypt( { name: 'AES-GCM', iv: iv, tag: authTag }, cryptoKey, ciphertext ); // Convert buffer back to readable string return new TextDecoder().decode(decryptedBuffer); } // Usage example (using the result from Node.js): decryptDataWeb(result.ciphertext, result.key, result.iv, result.authTag) .then(plaintext => console.log('Decrypted text:', plaintext));
Critical Notes:
- Never reuse an IV with the same AES key—always generate a new one for each encryption
- The authentication tag is non-negotiable for AES-GCM; omitting it will cause decryption to fail
This builds on the first question, adding IndexedDB storage for offline access. The workflow is: encrypt in Node.js, send the encrypted payload + metadata to the browser, store it in IndexedDB, then retrieve and decrypt even when offline.
Step 1: Node.js Encryption (Same as Question 1)
Use the encryptDataNode function above to generate your encrypted data, key, IV, and auth tag.
Step 2: Browser - Store in IndexedDB
We’ll use IndexedDB’s native API (you can also use a promise-based wrapper like idb for cleaner code):
async function storeInIndexedDB(data) { return new Promise((resolve, reject) => { const request = indexedDB.open('CryptoStorage', 1); request.onupgradeneeded = (event) => { const db = event.target.result; // Create an object store with auto-incrementing ID db.createObjectStore('encryptedData', { keyPath: 'id', autoIncrement: true }); }; request.onsuccess = (event) => { const db = event.target.result; const transaction = db.transaction('encryptedData', 'readwrite'); const store = transaction.objectStore('encryptedData'); store.add({ ...data }); transaction.oncomplete = () => { db.close(); resolve('Data saved to IndexedDB successfully'); }; transaction.onerror = (err) => reject(err); }; request.onerror = (err) => reject(err); }); } // Usage example: encryptDataNode('Sensitive offline content').then(encryptedData => { storeInIndexedDB(encryptedData).then(msg => console.log(msg)); });
Step 3: Browser - Offline Decryption from IndexedDB
Retrieve the stored data and decrypt it using the decryptDataWeb function from Question 1:
async function getAndDecryptOffline(entryId) { return new Promise((resolve, reject) => { const request = indexedDB.open('CryptoStorage', 1); request.onsuccess = async (event) => { const db = event.target.result; const transaction = db.transaction('encryptedData', 'readonly'); const store = transaction.objectStore('encryptedData'); const getRequest = store.get(entryId); getRequest.onsuccess = async () => { const storedData = getRequest.result; if (!storedData) return reject('No data found with that ID'); const plaintext = await decryptDataWeb( storedData.ciphertext, storedData.key, storedData.iv, storedData.authTag ); db.close(); resolve(plaintext); }; getRequest.onerror = (err) => reject(err); }; request.onerror = (err) => reject(err); }); } // Usage example (works offline!): getAndDecryptOffline(1) // Replace with your entry ID .then(plaintext => console.log('Offline decrypted text:', plaintext));
Key Notes:
- For symmetric AES keys, consider storing them in
sessionStorageinstead of IndexedDB if they don’t need to persist across browser restarts (better security) - If you need to persist keys, use WebCrypto’s
extractable: falseoption when importing keys to prevent accidental exposure
RSA is great for asymmetric encryption (encrypt with public key, decrypt with private key). To reuse keys across environments, use a format supported by both—JWK (JSON Web Key) is the simplest, or PKCS#8/PEM for private keys and SPKI/PEM for public keys.
Option 1: JWK Format (Most Straightforward)
Node.js: Generate and Export RSA Key Pair
const crypto = require('crypto'); async function generateRSAKeyPair() { const { publicKey, privateKey } = await crypto.generateKeyPair('rsa', { modulusLength: 2048, publicKeyEncoding: { type: 'spki', format: 'pem' }, privateKeyEncoding: { type: 'pkcs8', format: 'pem' } }); // Convert PEM keys to JWK for cross-environment compatibility const publicJwk = await crypto.webcrypto.subtle.importKey( 'spki', Buffer.from(publicKey, 'utf8'), { name: 'RSA-OAEP', hash: 'SHA-256' }, true, ['encrypt'] ).then(key => crypto.webcrypto.subtle.exportKey('jwk', key)); const privateJwk = await crypto.webcrypto.subtle.importKey( 'pkcs8', Buffer.from(privateKey, 'utf8'), { name: 'RSA-OAEP', hash: 'SHA-256' }, true, ['decrypt'] ).then(key => crypto.webcrypto.subtle.exportKey('jwk', key)); return { publicJwk, privateJwk }; } // Usage example: generateRSAKeyPair().then(keys => { console.log('Public JWK:', keys.publicJwk); console.log('Private JWK:', keys.privateJwk); // Send publicJWK to browser; keep privateJWK secure on your server });
Browser: Import JWK and Decrypt
async function importRsaKey(jwk, isPrivate) { return window.crypto.subtle.importKey( 'jwk', jwk, { name: 'RSA-OAEP', hash: 'SHA-256' }, false, isPrivate ? ['decrypt'] : ['encrypt'] ); } async function decryptWithRsaWeb(ciphertextB64, privateJwk) { const privateKey = await importRsaKey(privateJwk, true); const ciphertext = Uint8Array.from(atob(ciphertextB64), c => c.charCodeAt(0)); const decryptedBuffer = await window.crypto.subtle.decrypt( { name: 'RSA-OAEP' }, privateKey, ciphertext ); return new TextDecoder().decode(decryptedBuffer); } // Usage example: decryptWithRsaWeb(encryptedTextB64, privateJwk) .then(plaintext => console.log('RSA Decrypted text:', plaintext));
Option 2: PEM Format
If you prefer PEM keys, convert them to ArrayBuffer in the browser first:
// Browser: Convert PEM public key to ArrayBuffer function pemToArrayBuffer(pem) { const cleanedPem = pem.replace(/-----BEGIN PUBLIC KEY-----|-----END PUBLIC KEY-----|\n/g, ''); return Uint8Array.from(atob(cleanedPem), c => c.charCodeAt(0)); } // Import PEM public key to WebCrypto async function importRsaPublicKey(pem) { const buffer = pemToArrayBuffer(pem); return window.crypto.subtle.importKey( 'spki', buffer, { name: 'RSA-OAEP', hash: 'SHA-256' }, true, ['encrypt'] ); }
Critical Notes:
- Use RSA-OAEP instead of RSA-PKCS1-v1_5 for stronger security
- Never expose private keys to untrusted clients—only send them to authenticated users if absolutely necessary
- A 2048-bit modulus is a good balance between security and performance; 4096 is more secure but slower
内容的提问来源于stack exchange,提问作者n kavinit

