You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 01:18:17