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

如何在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;
}

解密流程

解密时步骤反向即可:

  1. 用相同的共享密钥、nonce生成Poly1305密钥;
  2. 重新计算MAC并与接收的MAC对比(验证完整性);
  3. 初始化XSalsa20引擎(计数器从1开始)解密密文。

关键注意事项

  • nonce必须全局唯一:XSalsa20要求同一密钥下不能重复使用nonce,否则会导致密钥泄露,必须用密码学安全随机数生成24字节nonce。
  • 密钥派生对齐:如果需要完全匹配libsodium的crypto_box逻辑,需额外添加Blake2b哈希步骤,用共享密钥+双方公钥派生最终的XSalsa20密钥。

内容的提问来源于stack exchange,提问作者Leprechaun

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 07:54:55