如何在BouncyCastle中实现类似libsodium的XSalsa20+Poly1305 AEAD方案
用BouncyCastle实现XSalsa20-Poly1305(兼容libsodium默认AEAD)
为什么ChaCha20-Poly1305有专属AEAD实现,而XSalsa20-Poly1305没有?
- ChaCha20-Poly1305是IETF标准化方案,被广泛应用于TLS等通用场景,因此BouncyCastle优先提供了开箱即用的AEAD封装实现。
- XSalsa20-Poly1305是libsodium特有的组合,属于Salsa20家族的扩展变体,未成为通用国际标准,所以BC没有直接提供封装好的AEAD类,需要手动组合底层原语实现。
实现步骤(基于BouncyCastle原语)
假设你已经通过X25519 KeyAgreement得到了32字节的SecretKey(X25519共享密钥长度固定为32字节),接下来按以下步骤实现:
1. 引入核心依赖类
import org.bouncycastle.crypto.engines.Salsa20Engine; import org.bouncycastle.crypto.macs.Poly1305; import org.bouncycastle.crypto.params.KeyParameter; import org.bouncycastle.crypto.params.ParametersWithIV; import org.bouncycastle.util.Arrays; import java.security.SecureRandom; import java.nio.charset.StandardCharsets;
2. 生成Poly1305密钥
libsodium的XSalsa20-Poly1305规则中,Poly1305的密钥由XSalsa20加密全零块生成(计数器起始为0):
// 从ECDH得到的32字节共享密钥 byte[] sharedKey = secretKey.getEncoded(); // 生成24字节的XSalsa20 nonce(必须全局唯一,用密码学安全随机数生成) byte[] nonce = new byte[24]; new SecureRandom().nextBytes(nonce); // 初始化XSalsa20引擎(true表示启用XSalsa20变体) Salsa20Engine xsalsa20 = new Salsa20Engine(true); xsalsa20.init(true, new ParametersWithIV(new KeyParameter(sharedKey), nonce)); // 加密全零的32字节块,得到Poly1305密钥 byte[] poly1305Key = new byte[32]; xsalsa20.processBytes(new byte[32], 0, 32, poly1305Key, 0);
3. 用XSalsa20加密明文
重新初始化XSalsa20引擎,将计数器起始值设为1(避免和生成Poly1305密钥的块冲突):
// 重置XSalsa20引擎,强制计数器从1开始 xsalsa20.init(true, new ParametersWithIV(new KeyParameter(sharedKey), nonce) { @Override public int getCounter() { return 1; } }); // 加密明文 byte[] plaintext = "测试兼容libsodium的加密内容".getBytes(StandardCharsets.UTF_8); byte[] ciphertext = new byte[plaintext.length]; xsalsa20.processBytes(plaintext, 0, plaintext.length, ciphertext, 0);
4. 计算Poly1305 MAC
按照libsodium规则,MAC计算需包含附加数据(可选)、密文、附加数据长度(小端64位)、密文长度(小端64位),且所有数据需填充到16字节对齐:
// 附加数据(无则传空数组) byte[] additionalData = new byte[0]; // 构建MAC输入:填充后的附加数据 + 填充后的密文 + 长度小端字节 byte[] paddedAd = padTo16Bytes(additionalData); byte[] paddedCiphertext = padTo16Bytes(ciphertext); byte[] adLengthLE64 = longToLittleEndian64(additionalData.length); byte[] cipherLengthLE64 = longToLittleEndian64(ciphertext.length); byte[] macInput = Arrays.concatenate(paddedAd, paddedCiphertext, adLengthLE64, cipherLengthLE64); // 初始化Poly1305并计算MAC Poly1305 poly1305 = new Poly1305(); poly1305.init(new KeyParameter(poly1305Key)); poly1305.update(macInput, 0, macInput.length); byte[] mac = new byte[poly1305.getMacSize()]; poly1305.doFinal(mac, 0);
5. 辅助工具方法
// 16字节对齐填充 private static byte[] padTo16Bytes(byte[] data) { int padLength = (16 - (data.length % 16)) % 16; return Arrays.concatenate(data, new byte[padLength]); } // 长整数转小端64位字节数组 private static byte[] longToLittleEndian64(long value) { byte[] result = new byte[8]; for (int i = 0; i < 8; i++) { result[i] = (byte) (value & 0xFF); value >>= 8; } return result; }
解密流程
解密时步骤反向即可:
- 用相同的共享密钥、nonce生成Poly1305密钥;
- 重新计算MAC并与接收的MAC对比(验证完整性);
- 初始化XSalsa20引擎(计数器从1开始)解密密文。
关键注意事项
- nonce必须全局唯一:XSalsa20要求同一密钥下不能重复使用nonce,否则会导致密钥泄露,必须用密码学安全随机数生成24字节nonce。
- 密钥派生对齐:如果需要完全匹配libsodium的
crypto_box逻辑,需额外添加Blake2b哈希步骤,用共享密钥+双方公钥派生最终的XSalsa20密钥。
内容的提问来源于stack exchange,提问作者Leprechaun
相关产品推荐
相关产品推荐

