I want to use BouncyCastle to immitate the default AEAD scheme of libsodium (X25519 ECDH followed by XSalsa20 symmetric cipher with Poly1305 KDF).
I was able to do the DH with X25519 KeyAgreement
to generate a javax.crypto.SecretKey
.
But for the next step I'm left clueless. BouncyCastle offers a ChaCha20Poly1305
class implementing AEADCipher
, which seems to be doing both steps at once (generating MAC and encrypting). However, I did not find any such equivalent for "XSalsa20Poly1305".
There is a Poly1305KeyGenerator
available in BC, but I don't see any way to pass a nonce to it, since it accepts only a simple KeyGenerationParameters
objects on init.
Why is the combination of ChaCha20 and Poly1305 so special (compared to [X]Salsa20+Poly1305) that it has a dedicated implementation? How can I do the same thing for XSalsa20 with BouncyCastle primitives?
Libsodium applies XSalsa20-Poly1305 for authenticated encryption, which is implemented in the function crypto_secretbox_easy()
. The underlying algorithm of XSalsa20-Poly1305 can be read from the reference implementation:
k
and the nonce n
as input parameters, a key subkey is derived.n
as nonce (called subnonce in the following), the concatenation of an empty 32 bytes array and the plaintext m
of length mlen
is encrypted.c
.mac
is derived for c
applying the Poly1305 key.For further details on the algorithms, in particular HSalsa20, see e.g. Extending the Salsa20 nonce, and on authentication, see e.g. Poly1305.
BouncyCastle implements XSalsa20Engine
as derivation of Salsa20Engine
. XSalsa20Engine
overwrites setKey()
, which implements HSalsa20 and internally determines subkey and subnonce. When the XSalsa20Engine
instance is initialized, Salsa20Engine#init()
is executed and thus setKey()
.
This allows XSalsa20-Poly1305 to be implemented with Bouncycastle as follows:
XSalsa20Engine
using key and nonce. This internally calculates subkey and subnonce.In addition, a random nonce is generated (this is done in the reference implementation before the crypto_secretbox_easy()
call), and nonce, Poly1305 MAC and ciphertext are concatenated in this order.
Regarding the Poly1305 key, please note the following:
macKey
in the above procedure is not (yet) clamped, this only happens implicitly during the initialization of Poly1305 (in init()
via setKey()
).macKey
with checkKey()
, clamp()
must be executed first (the subsequent implicit clamping during Poly1305 initialization then has no effect).Poly1305KeyGenerator
or generateKey()
must not be used to generate the Poly1305 key in the context of XSalsa20-Poly1305. generateKey()
is intended for the generation of random Poly1305 keys, while the Poly1305 key generated in the context of XSalsa20-Poly1305 is generated deterministically (in the way described above).Example implementation for encryption with XSalsa20-Poly1305:
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.HexFormat;
import org.bouncycastle.crypto.engines.XSalsa20Engine;
import org.bouncycastle.crypto.macs.Poly1305;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;
import org.bouncycastle.util.Arrays;
...
static final int NONCEBYTES = 24;
static final int KEYBYTES = 32;
...
public static byte[] encrypt(byte[] key, byte[] plaintext) {
// generate random nonce
byte[] nonce = new byte[NONCEBYTES];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(nonce);
XSalsa20Engine xSalsa20Engine = new XSalsa20Engine();
xSalsa20Engine.init(true, new ParametersWithIV(new KeyParameter(key), nonce));
// generate mac key
byte[] macKey = new byte[KEYBYTES];
xSalsa20Engine.processBytes(macKey, 0, macKey.length, macKey, 0);
// encrypt plaintext
byte[] ciphertext = new byte[plaintext.length];
xSalsa20Engine.processBytes(plaintext, 0, plaintext.length, ciphertext, 0);
// generate mac
Poly1305 poly1305 = new Poly1305();
poly1305.init(new KeyParameter(macKey));
byte[] mac = new byte[poly1305.getMacSize()];
poly1305.update(ciphertext, 0, plaintext.length); // ciphertext size = plaintext size
poly1305.doFinal(mac, 0);
// concatenate, e.g. nonce|mac|ciphertext
return Arrays.concatenate(nonce, mac, ciphertext);
}
Example for decryption:
public static byte[] decrypt(byte[] key, byte[] nonceMacCiphertext) {
// separate nonce, mac and ciphertext
Poly1305 poly1305 = new Poly1305();
byte[] nonce = Arrays.copyOfRange(nonceMacCiphertext, 0, NONCEBYTES);
byte[] mac = Arrays.copyOfRange(nonceMacCiphertext, NONCEBYTES, NONCEBYTES + poly1305.getMacSize());
byte[] ciphertext = Arrays.copyOfRange(nonceMacCiphertext, NONCEBYTES + poly1305.getMacSize(), nonceMacCiphertext.length);
XSalsa20Engine xSalsa20Engine = new XSalsa20Engine();
xSalsa20Engine.init(false, new ParametersWithIV(new KeyParameter(key), nonce));
// generate mac key
byte[] macKey = new byte[KEYBYTES];
xSalsa20Engine.processBytes(macKey, 0, macKey.length, macKey, 0);
// calculate Mac
byte[] macCalculated = new byte[poly1305.getMacSize()];
poly1305.init(new KeyParameter(macKey));
poly1305.update(ciphertext, 0, ciphertext.length);
poly1305.doFinal(macCalculated, 0);
// decrypt on successful authentication
byte[] decrypted = null;
if (MessageDigest.isEqual(macCalculated, mac)) {
decrypted = new byte[ciphertext.length];
xSalsa20Engine.processBytes(ciphertext, 0, ciphertext.length, decrypted, 0);
}
return decrypted;
}
Test:
The most convenient way to check compatibility with XSalsa20-Poly1305 is to compare a ciphertext generated with a Libsodium library, e.g. Lazysodium, and identical input parameters:
import com.goterl.lazysodium.LazySodiumJava;
import com.goterl.lazysodium.SodiumJava;
import com.goterl.lazysodium.utils.Key;
...
public static void main(String[] args) throws Exception {
// Check XSalsa20-Poly1305 implementation
byte[] key = HexFormat.of().parseHex("a7e845b0854294da9aa743b807cb67b19647c1195ea8120369f3d12c70468f29");
byte[] plaintext = "The quick brown fox jumps over the lazy dog".getBytes(StandardCharsets.UTF_8);
byte[] nonceMacCiphertext = encrypt(key, plaintext);
System.out.println("CT, BouncyCastle: " + HexFormat.of().formatHex(nonceMacCiphertext));
byte[] decrypted = decrypt(key, nonceMacCiphertext);
System.out.println("Decrypted data: " + new String(decrypted, StandardCharsets.UTF_8));
// Compare nonce|mac|ciphertext with Lazysodium
byte[] nonce = Arrays.copyOfRange(nonceMacCiphertext, 0, NONCEBYTES);
SodiumJava sodium = new SodiumJava();
LazySodiumJava lazySodium = new LazySodiumJava(sodium, StandardCharsets.UTF_8);
String macCiphertext = lazySodium.cryptoSecretBoxEasy("The quick brown fox jumps over the lazy dog", nonce , Key.fromBytes(key));
System.out.println("CT, Lazysodium: " + HexFormat.of().formatHex(nonce) + macCiphertext.toLowerCase());
}
Both ciphertexts match for identical input data.