javaencryptioncryptographybouncycastlelibsodium

How to do X25519 ECDH with a round of HSalsa20 to produce the shared secret key specified by NaCl and libsodium using BouncyCastle?


While trying to implement the "Authenticated encryption" scheme of libsodium using BouncyCastle I naively performed simple X25519 key agreement to obtain a javax.crypto.SecretKey object. I.e.:

public SecretKey generateSecretKey(PrivateKey privateKey, PublicKey publicKey) throws NoSuchAlgorithmException, InvalidKeyException {
    KeyAgreement keyAgreement = KeyAgreement.getInstance("X25519", new BouncyCastleProvider());
    keyAgreement.init(privateKey);
    keyAgreement.doPhase(publicKey, true);
    return keyAgreement.generateSecret("X25519");
}

I used this secretKey as an input to the org.bouncycastle.crypto.engines.XSalsa20Engine:

XSalsa20Engine xSalsa20Engine = new XSalsa20Engine();
xSalsa20Engine.init(true, new ParametersWithIV(new KeyParameter(secretKey), nonce));

However, reading the Cryptography in NaCl paper, and subsequent testing, I found that this is wrong.

Quote:

In the next step, described in Section 7, Alice will convert this 32-byte shared secret k into a 32-byte string HSalsa20(k, 0), which is then used to encrypt and authenticate packets. Bob similarly uses HSalsa20(k, 0) to verify and decrypt the packets. No other use is made of k. One can thus view HSalsa20(k, 0) as the shared secret rather than k.

The secret key produced by the KeyAgreement needs to be first run through a round of HSalsa20(k, 0). to produce the final key, called a "shared secret key" in the original paper by Bernstein, that is then used for encryption.

TLDR

How can I perform a round of HSalsa20 on the initial SecretKey object I got from X25519 exchange using BouncyCastle? There is no standalone HSalsa20 function implementation in Bouncycastle library. It seems to be implemented somehow implicitly as a part of the (X)Salsa20Engine class, and I'm not able to invoke it out of the message encryption context.


Solution

  • You can use BouncyCastle for an implementation of HSalsa20 based on an answer to one of your last questions:

    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 offers the following path for an implementation of HSalsa20: Implement a class HSalsa20 that derives from Salsa20Engine and take the logic from XSalsa20Engine#setKey():

    import java.io.ByteArrayOutputStream;
    import org.bouncycastle.crypto.engines.Salsa20Engine;
    import org.bouncycastle.util.Pack;
    
    class HSalsa20 extends Salsa20Engine
    {
        public byte[] getData(byte[] keyBytes, byte[] ivBytes) 
        {
            super.setKey(keyBytes, ivBytes);
            
            Pack.littleEndianToInt(ivBytes, 8, engineState, 8, 2);
    
            int[] hsalsa20Out = new int[engineState.length];
            salsaCore(20, engineState, hsalsa20Out);
    
            engineState[1] = hsalsa20Out[0] - engineState[0];
            engineState[2] = hsalsa20Out[5] - engineState[5];
            engineState[3] = hsalsa20Out[10] - engineState[10];
            engineState[4] = hsalsa20Out[15] - engineState[15];
    
            engineState[11] = hsalsa20Out[6] - engineState[6];
            engineState[12] = hsalsa20Out[7] - engineState[7];
            engineState[13] = hsalsa20Out[8] - engineState[8];
            engineState[14] = hsalsa20Out[9] - engineState[9];
    
            byte[] result = null;
            try {
                ByteArrayOutputStream outputStream = new ByteArrayOutputStream( );
                outputStream.write( Pack.intToLittleEndian(engineState[1] ));
                outputStream.write( Pack.intToLittleEndian(engineState[2] ));
                outputStream.write( Pack.intToLittleEndian(engineState[3] ));
                outputStream.write( Pack.intToLittleEndian(engineState[4] ));
                outputStream.write( Pack.intToLittleEndian(engineState[11] ));
                outputStream.write( Pack.intToLittleEndian(engineState[12] ));
                outputStream.write( Pack.intToLittleEndian(engineState[13] ));
                outputStream.write( Pack.intToLittleEndian(engineState[14] ));
                result = outputStream.toByteArray();
            } catch(Exception ex) {
                ex.printStackTrace(); // your exception handling
            }
            
            return result;
       }
    }
    

    Test: The document you linked has test vectors for HSalsa20 in chapter 8:

    import java.util.HexFormat;
    ...
    // test vector 1
    HSalsa20 hSalsa20 = new HSalsa20();
    byte[] test1 = hSalsa20.getData(
            new byte[] {0x4a,0x5d,(byte)0x9d,0x5b,(byte)0xa4,(byte)0xce,0x2d,(byte)0xe1,0x72,(byte)0x8e,0x3b,(byte)0xf4,(byte)0x80,0x35,0x0f,0x25,(byte)0xe0,0x7e,0x21,(byte)0xc9,0x47,(byte)0xd1,(byte)0x9e,0x33,0x76,(byte)0xf0,(byte)0x9b,0x3c,0x1e,0x16,0x17,0x42}, 
            new byte[16]);
    System.out.println(HexFormat.of().formatHex(test1)); // 0x1b27556473e985d462cd51197a9a46c76009549eac6474f206c4ee0844f68389
    
    // test vector 2
    hSalsa20 = new HSalsa20();
    byte[] test2 = hSalsa20.getData(
            new byte[] {0x1b,0x27,0x55,0x64,0x73,(byte)0xe9,(byte)0x85,(byte)0xd4,0x62,(byte)0xcd,0x51,0x19,0x7a,(byte)0x9a,0x46,(byte)0xc7,0x60,0x09,0x54,(byte)0x9e,(byte)0xac,0x64,0x74,(byte)0xf2,0x06,(byte)0xc4,(byte)0xee,0x08,0x44,(byte)0xf6,(byte)0x83,(byte)0x89}, 
            new byte[] {0x69,0x69,0x6e,(byte)0xe9,0x55,(byte)0xb6,0x2b,0x73,(byte)0xcd,0x62,(byte)0xbd,(byte)0xa8,0x75,(byte)0xfc,0x73,(byte)0xd6});
    System.out.println(HexFormat.of().formatHex(test2)); // 0xdc908dda0b9344a953629b733820778880f3ceb421bb61b91cbd4c3e66256ce4
    

    The outputs generated correspond to those of the test vectors.


    This allows Libsodium's crypto_box (public key cryptography) to be implemented with BouncyCastle as follows: