Node.js Crypto与CryptoJS的Hmac SHA256 Base64结果不一致问题及统一方案
Great question! Let's break this down clearly:
Which Result Is "Correct"?
Neither result is inherently "wrong"—both are valid outputs from their respective encryption configurations. The difference comes down to mismatched encryption parameters between CryptoJS (Angular) and Node.js's crypto module. Common mismatches include:
- How the raw private key is processed (CryptoJS defaults to deriving keys via PBKDF2 for string passwords, while Node.js
cryptouses the string directly as a buffer unless told otherwise) - Initialization Vector (IV) generation/usage
- Encryption mode or padding scheme differences
How to Get Matching Results in Angular (CryptoJS)
To align Angular's output with Node.js, you need to mirror every parameter used in the Node.js implementation. Below are two common scenarios with code examples:
Scenario 1: Node.js Uses Hashed Key + Random IV (Secure, Common Implementation)
Assume your Node.js code uses AES-256-CBC, hashes the raw key to get a valid 32-byte AES-256 key, generates a random 16-byte IV, and concatenates the IV with the ciphertext before encoding to base64:
// Node.js Implementation const crypto = require('crypto'); function encryptNode(message, privateKey) { // Hash raw key to 32 bytes for AES-256 const key = crypto.createHash('sha256').update(privateKey).digest(); // Generate random 16-byte IV (AES block size) const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); let encrypted = cipher.update(message, 'utf8'); encrypted = Buffer.concat([encrypted, cipher.final()]); // Concatenate IV + ciphertext, then encode to base64 return Buffer.concat([iv, encrypted]).toString('base64'); } // Example output (varies per run due to random IV): nYu2PGqfRDWnHbT649q0gc+7DcIq8iwcwHAQQa5T2HY= console.log(encryptNode('simple', '123456789'));
对应的Angular (CryptoJS) code that matches this behavior:
// Angular/CryptoJS Implementation import * as CryptoJS from 'crypto-js'; function encryptAngular(message: string, privateKey: string): string { // Match Node.js's key hashing const key = CryptoJS.SHA256(privateKey); // Match random 16-byte IV generation const iv = CryptoJS.lib.WordArray.random(16); // Configure AES to match Node.js: CBC mode, PKCS7 padding (CryptoJS default) const encrypted = CryptoJS.AES.encrypt(message, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); // Concatenate IV + ciphertext, then encode to base64 const combinedBytes = iv.concat(encrypted.ciphertext); return combinedBytes.toString(CryptoJS.enc.Base64); } // Output will match Node.js's result (varies per run due to random IV) console.log(encryptAngular('simple', '123456789'));
Scenario 2: Align Node.js to CryptoJS's Default Behavior
If you want Node.js to match CryptoJS's default behavior (CryptoJS uses PBKDF2 to derive keys/IVs from string passwords), here's how to adjust Node.js:
// Node.js Implementation (aligned to CryptoJS defaults) const crypto = require('crypto'); function encryptNodeAligned(message, password) { // CryptoJS defaults: PBKDF2 with 1000 iterations, SHA1, 256-bit key + 128-bit IV const salt = crypto.randomBytes(8); const keyIv = crypto.pbkdf2Sync(password, salt, 1000, 32 + 16, 'sha1'); const key = keyIv.slice(0, 32); const iv = keyIv.slice(32, 48); const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); let encrypted = cipher.update(message, 'utf8'); encrypted = Buffer.concat([encrypted, cipher.final()]); // CryptoJS default output format: "Salted__" + salt + ciphertext const result = Buffer.concat([Buffer.from('Salted__'), salt, encrypted]).toString('base64'); return result; } // Output will match CryptoJS's default AES.encrypt result console.log(encryptNodeAligned('simple', '123456789'));
Critical Security Notes
- Avoid ECB mode: It's insecure and doesn't use an IV. Always use CBC or GCM mode.
- Random IVs are mandatory: If you use a random IV, you must transmit it with the ciphertext (e.g., prepend it) to decrypt successfully.
- Use valid key lengths: AES-128 requires 16-byte keys, AES-256 requires 32-byte keys. Never use raw short strings as keys—derive them via hashing or PBKDF2.
内容的提问来源于stack exchange,提问作者Adam Adamski

