javaencryptioncryptographybouncycastlelibsodium

How do I use XSalsa20 and Poly1305 primitives in Bouncycastle for AEAD


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?


Solution

  • 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:

    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:

    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:

    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.