Angular 15生产环境中SubtleCrypto.decrypt()方法报错求助
Angular 15生产环境Web Crypto API AES-CBC解密失败问题排查与解决
问题背景
在Angular 15应用中使用Web Crypto API解密AES/CBC/PKCS7Padding加密的十六进制字符串时,本地开发环境(ng serve)运行正常,但生产构建部署后,crypto.subtle.decrypt()抛出错误:
- Firefox:
DOMException: The operation failed for an operation-specific reason - Chromium:仅提示
Error无详细信息
原代码示例
public decryptAES(encryptedText: string, secretKey: string, ivString: string): Promise<string> { return new Promise((resolve, reject) => { console.log('xxx - encryptedText ', encryptedText); console.log('xxx - secretKey ', secretKey); console.log('xxx - ivString ', ivString); crypto.subtle.importKey( 'raw', new TextEncoder().encode(secretKey), { name: 'AES-CBC', length: 256 }, false, ['decrypt'], ) .then(keyBuffer => { console.log('xxx - keyBuffer ', keyBuffer); const ivBuffer = new TextEncoder().encode(ivString); console.log('xxx - ivBuffer ', ivBuffer); const encryptedBuffer = new Uint8Array(Uint8Array.from(encryptedText.match(/.{1,2}/g).map(byte => parseInt(byte, 16))),); console.log('xxx - encryptedBuffer ', encryptedBuffer); crypto.subtle.decrypt( { name: 'AES-CBC', iv: ivBuffer }, keyBuffer, encryptedBuffer, ) .then(decryptedBuffer => { console.log('xxx - decryptedBuffer ', decryptedBuffer); const decryptedText = new TextDecoder().decode(decryptedBuffer); console.log('xxx - decryptedText ', decryptedText); resolve(decryptedText); }) .catch(err => { console.log('xxx - error in subtle.decrypt - ', err); reject(err); }); }) .catch(err => { console.log('xxx - error in subtle.importKey - ', err); reject(err); }); }); }
排查与解决方案
1. IV编码逻辑错误(核心问题)
原代码用TextEncoder直接编码IV字符串,但AES-CBC的IV应为十六进制字符串转换的二进制数据,而非字符串的UTF-8编码。本地环境可能因IV字符串恰好匹配二进制值而侥幸成功,生产环境则会因格式不符导致解密失败。
修复代码:
// 将十六进制IV字符串转换为Uint8Array const ivPairs = ivString.match(/.{1,2}/g); if (!ivPairs) { reject(new Error('Invalid IV format')); return; } const ivBuffer = new Uint8Array(ivPairs.map(byte => parseInt(byte, 16))); // 验证IV长度必须为16字节(AES-CBC要求) if (ivBuffer.length !== 16) { reject(new Error('AES-CBC requires 16-byte IV')); return; }
2. 加密文本解析鲁棒性不足
生产环境中加密字符串可能被意外截断、转义或混入非十六进制字符,导致match返回无效结果。
修复代码:
// 清理加密文本,去除空格和非法字符 const cleanedEncryptedText = encryptedText.replace(/\s+/g, '').replace(/[^a-fA-F0-9]/g, ''); const encryptedPairs = cleanedEncryptedText.match(/.{1,2}/g); if (!encryptedPairs) { reject(new Error('Invalid encrypted text format')); return; } const encryptedBuffer = new Uint8Array(encryptedPairs.map(byte => parseInt(byte, 16)));
3. 密钥长度与格式验证
AES-256要求密钥为32字节,原代码未验证密钥长度;若密钥是十六进制字符串,直接用TextEncoder编码会导致长度不符。
修复代码:
let keyBytes: Uint8Array; // 判断密钥是否为十六进制格式(AES-256的十六进制密钥为64字符) if (/^[a-fA-F0-9]+$/.test(secretKey) && secretKey.length === 64) { const keyPairs = secretKey.match(/.{1,2}/g); if (!keyPairs) { reject(new Error('Invalid secret key format')); return; } keyBytes = new Uint8Array(keyPairs.map(byte => parseInt(byte, 16))); } else { keyBytes = new TextEncoder().encode(secretKey); if (keyBytes.length !== 32) { reject(new Error('AES-256 requires 32-byte key')); return; } }
4. 生产环境代码压缩影响
Angular生产构建的Terser压缩可能混淆Web Crypto相关逻辑,可通过配置避免:
在angular.json的build配置中添加:
"optimization": { "scripts": { "terserOptions": { "keep_classnames": true, "keep_fnames": true } } }
5. 安全上下文验证
Web Crypto API仅支持HTTPS或localhost的安全上下文,确保生产环境部署使用HTTPS协议。
修正后完整代码
public decryptAES(encryptedText: string, secretKey: string, ivString: string): Promise<string> { return new Promise((resolve, reject) => { console.log('xxx - encryptedText ', encryptedText); console.log('xxx - secretKey ', secretKey); console.log('xxx - ivString ', ivString); // 处理密钥:验证格式与长度 let keyBytes: Uint8Array; if (/^[a-fA-F0-9]+$/.test(secretKey) && secretKey.length === 64) { const keyPairs = secretKey.match(/.{1,2}/g); if (!keyPairs) { reject(new Error('Invalid secret key format')); return; } keyBytes = new Uint8Array(keyPairs.map(byte => parseInt(byte, 16))); } else { keyBytes = new TextEncoder().encode(secretKey); if (keyBytes.length !== 32) { reject(new Error('AES-256 requires 32-byte key')); return; } } crypto.subtle.importKey( 'raw', keyBytes, { name: 'AES-CBC', length: 256 }, false, ['decrypt'], ) .then(keyBuffer => { console.log('xxx - keyBuffer ', keyBuffer); // 处理IV:转换为Uint8Array并验证长度 const ivPairs = ivString.match(/.{1,2}/g); if (!ivPairs) { reject(new Error('Invalid IV format')); return; } const ivBuffer = new Uint8Array(ivPairs.map(byte => parseInt(byte, 16))); if (ivBuffer.length !== 16) { reject(new Error('AES-CBC requires 16-byte IV')); return; } console.log('xxx - ivBuffer ', ivBuffer); // 处理加密文本:清理并转换为Uint8Array const cleanedEncryptedText = encryptedText.replace(/\s+/g, '').replace(/[^a-fA-F0-9]/g, ''); const encryptedPairs = cleanedEncryptedText.match(/.{1,2}/g); if (!encryptedPairs) { reject(new Error('Invalid encrypted text format')); return; } const encryptedBuffer = new Uint8Array(encryptedPairs.map(byte => parseInt(byte, 16))); console.log('xxx - encryptedBuffer ', encryptedBuffer); crypto.subtle.decrypt( { name: 'AES-CBC', iv: ivBuffer }, keyBuffer, encryptedBuffer, ) .then(decryptedBuffer => { console.log('xxx - decryptedBuffer ', decryptedBuffer); const decryptedText = new TextDecoder().decode(decryptedBuffer); console.log('xxx - decryptedText ', decryptedText); resolve(decryptedText); }) .catch(err => { console.log('xxx - error in subtle.decrypt - ', err); reject(err); }); }) .catch(err => { console.log('xxx - error in subtle.importKey - ', err); reject(err); }); }); }
内容的提问来源于stack exchange,提问作者sriyanjith
相关产品推荐
相关产品推荐

