使用window.crypto.subtle加密,C#解密时认证标签不匹配问题求助
JS端
window.crypto.subtle加密,C#解密时认证标签不匹配问题 我想用window.crypto.subtle实现加密,在C#中完成解密。目前JS端的加解密功能正常,但C#端计算出的认证标签和输入不匹配。我不确定是否可以用任意12字节作为盐,也不清楚是否必须派生密码。
JS代码
export async function deriveKey(password, salt) { const buffer = utf8Encoder.encode(password); const key = await crypto.subtle.importKey( 'raw', buffer, { name: 'PBKDF2' }, false, ['deriveKey'], ); const privateKey = crypto.subtle.deriveKey( { name: 'PBKDF2', hash: { name: 'SHA-256' }, iterations, salt, }, key, { name: 'AES-GCM', length: 256, }, false, ['encrypt', 'decrypt'], ); return privateKey; } const buff_to_base64 = (buff) => btoa(String.fromCharCode.apply(null, buff)); const base64_to_buf = (b64) => Uint8Array.from(atob(b64), (c) => c.charCodeAt(null)); export async function encrypt(key, data) { const salt = crypto.getRandomValues(new Uint8Array(12)); const iv = crypto.getRandomValues(new Uint8Array(12)); console.log('encrypt'); console.log('iv', iv); console.log('salt', salt); const buffer = new TextEncoder().encode(data); const privatekey = await deriveKey(key, salt); const encrypted = await crypto.subtle.encrypt( { name: 'AES-GCM', iv, tagLength: 128, }, privatekey, buffer, ); const bytes = new Uint8Array(encrypted); console.log('concat'); const buff = new Uint8Array(iv.byteLength + encrypted.byteLength + salt.byteLength); buff.set(iv, 0); buff.set(salt, iv.byteLength); buff.set(bytes, iv.byteLength + salt.byteLength); console.log('iv', iv); console.log('salt', salt); console.log('buff', buff); const base64Buff = buff_to_base64(buff); console.log(base64Buff); return base64Buff; } export async function decrypt(key, data) { console.log('decryption'); console.log('buff', base64_to_buf(data)); const d = base64_to_buf(data); const iv = d.slice(0, 12); const salt = d.slice(12, 24); const ec = d.slice(24); console.log('iv', iv); console.log('salt', salt); console.log(ec); const decrypted = await window.crypto.subtle.decrypt( { name: 'AES-GCM', iv, tagLength: 128, }, await deriveKey(key, salt), ec, ); return new TextDecoder().decode(new Uint8Array(decrypted)); }
C#原代码
Span<byte> encryptedData = Convert.FromBase64String(enc).AsSpan(); Span<byte> nonce = encryptedData[..12]; Span<byte> salt = encryptedData.Slice(12, 12); Span<byte> data = encryptedData.Slice(12 + 12, encryptedData.Length - 16 - 12 - 12); Span<byte> tag = encryptedData[^16..]; Span<byte> result = new byte[data.Length]; using Rfc2898DeriveBytes pbkdf2 = new(Encoding.UTF8.GetBytes(password), salt.ToArray(), 1000, HashAlgorithmName.SHA256); using AesGcm aes = new(pbkdf2.GetBytes(16)); aes.Decrypt(nonce, data, tag, result);
问题分析与修复
1. 密钥长度不匹配
JS中通过PBKDF2派生的是256位AES密钥(length: 256),但C#中pbkdf2.GetBytes(16)仅生成128位密钥,两者长度不一致导致解密失败。需要改为生成32字节(256位)密钥。
2. 密文与标签拆分错误
JS中encrypted是AES-GCM加密后的完整结果,包含密文+16字节认证标签。原C#代码的切片逻辑错误,正确的拆分方式是:
- 从索引24开始的全部内容是「密文+标签」
- 密文:取前
总长度-16字节 - 标签:取最后16字节
3. 迭代次数一致性
确保JS中iterations变量的值与C#中的1000完全一致,否则派生的密钥会不同。
关于盐的疑问
12字节的盐是完全合规的,PBKDF2要求盐至少8字节,随机生成的12字节盐符合安全标准,无需担心。
修复后的C#代码
Span<byte> encryptedData = Convert.FromBase64String(enc).AsSpan(); Span<byte> nonce = encryptedData[..12]; // AES-GCM的IV对应nonce Span<byte> salt = encryptedData.Slice(12, 12); Span<byte> ciphertextWithTag = encryptedData.Slice(24); Span<byte> ciphertext = ciphertextWithTag[..^16]; Span<byte> tag = ciphertextWithTag[^16..]; Span<byte> result = new byte[ciphertext.Length]; // 派生256位AES密钥 using Rfc2898DeriveBytes pbkdf2 = new(Encoding.UTF8.GetBytes(password), salt.ToArray(), 1000, HashAlgorithmName.SHA256); using AesGcm aes = new(pbkdf2.GetBytes(32)); // 32字节=256位 aes.Decrypt(nonce, ciphertext, tag, result);
额外注意事项
- 确认JS中的
utf8Encoder是new TextEncoder(),与C#的Encoding.UTF8编码一致。 - 如果JS中
iterations不是1000,需要同步修改C#中的迭代次数参数。
内容的提问来源于stack exchange,提问作者Vincent
相关产品推荐
相关产品推荐

