如何在JavaScript中解密无MAC的ChaCha20-Poly1305加密字符串?
问题与解决方案
问题描述
在Python中,可通过PyCryptodome库不使用MAC(标签)实现ChaCha20-Poly1305的加解密,代码示例如下:
from Crypto.Cipher import ChaCha20_Poly1305 key = '20d821e770a6d3e4fc171fd3a437c7841d58463cb1bc7f7cce6b4225ae1dd900' # 随机生成的密钥 nonce = '18c02beda4f8b22aa782444a' # 随机生成的12字节nonce def decode(ciphertext): cipher = ChaCha20_Poly1305.new(key=bytes.fromhex(key), nonce=bytes.fromhex(nonce)) plaintext = cipher.decrypt(bytes.fromhex(ciphertext)) return plaintext.decode('utf-8') def encode(plaintext): cipher = ChaCha20_Poly1305.new(key=bytes.fromhex(key), nonce=bytes.fromhex(nonce)) ciphertext = cipher.encrypt(plaintext.encode('utf-8')) return ciphertext.hex() encrypted = encode('abcdefg123') print(encrypted) # 输出: ab6cf9f9e0cf73833194 print(decode(encrypted)) # 输出: abcdefg123
但在React的JavaScript环境中,尝试libsodium-wrappers、js-chacha20等库时,均要求提供MAC才能解密。已知:
- 待解密字符串已通过上述方式加密,无法重新生成
- 直接用纯ChaCha20算法解密失败,因为同密钥和nonce下,纯ChaCha20的加密输出与上述方式不同
注:此操作仅用于教育用途,已知无MAC的方式安全性更低。
问题根源
PyCryptodome中ChaCha20_Poly1305的encrypt/decrypt方法在不处理MAC标签时,实际遵循RFC7539中ChaCha20-Poly1305的流加密规则:
- 使用12字节长度的nonce
- ChaCha20流的计数器从
1开始(而非纯ChaCha20默认的0)
普通纯ChaCha20库通常默认使用8字节nonce + 计数器从0开始,这导致加密输出不一致,无法直接解密。
解决方案
在JS中手动模拟该逻辑,以下是两种可行实现:
方式1:使用libsodium-wrappers实现
libsodium支持自定义ChaCha20的计数器起始值,可直接匹配PyCryptodome的行为:
import sodium from 'libsodium-wrappers'; // 初始化libsodium await sodium.ready(); const KEY = sodium.from_hex('20d821e770a6d3e4fc171fd3a437c7841d58463cb1bc7f7cce6b4225ae1dd900'); const NONCE = sodium.from_hex('18c02beda4f8b22aa782444a'); // 12字节nonce // 加密:模拟PyCryptodome的无MAC加密逻辑 function encode(plaintext) { const plaintextBytes = sodium.from_string(plaintext); // 使用crypto_stream_chacha20_xor_ic指定计数器起始值为1 const ciphertextBytes = sodium.crypto_stream_chacha20_xor_ic(plaintextBytes, null, NONCE, 1, KEY); return sodium.to_hex(ciphertextBytes); } // 解密:与加密逻辑完全一致 function decode(ciphertextHex) { const ciphertextBytes = sodium.from_hex(ciphertextHex); const plaintextBytes = sodium.crypto_stream_chacha20_xor_ic(ciphertextBytes, null, NONCE, 1, KEY); return sodium.to_string(plaintextBytes); } // 测试验证 const encrypted = encode('abcdefg123'); console.log(encrypted); // 输出: ab6cf9f9e0cf73833194 console.log(decode(encrypted)); // 输出: abcdefg123
方式2:使用@stablelib/chacha20实现
若不想依赖libsodium,可使用@stablelib/chacha20库,它支持自定义nonce长度和计数器起始值:
import { ChaCha20 } from '@stablelib/chacha20'; import { fromHex, toHex, fromString, toString } from '@stablelib/encoding'; const KEY = fromHex('20d821e770a6d3e4fc171fd3a437c7841d58463cb1bc7f7cce6b4225ae1dd900'); const NONCE = fromHex('18c02beda4f8b22aa782444a'); // 12字节nonce function encode(plaintext) { const plaintextBytes = fromString(plaintext); // 初始化ChaCha20时指定计数器从1开始 const cipher = new ChaCha20(KEY, NONCE, 1); const ciphertextBytes = cipher.secretXOR(plaintextBytes); return toHex(ciphertextBytes); } function decode(ciphertextHex) { const ciphertextBytes = fromHex(ciphertextHex); const cipher = new ChaCha20(KEY, NONCE, 1); const plaintextBytes = cipher.secretXOR(ciphertextBytes); return toString(plaintextBytes); } // 测试验证 const encrypted = encode('abcdefg123'); console.log(encrypted); // 输出: ab6cf9f9e0cf73833194 console.log(decode(encrypted)); // 输出: abcdefg123
内容的提问来源于stack exchange,提问作者ylu
相关产品推荐
相关产品推荐

