Keyboard Shortcuts N Next post
P Previous post
S Save / unsave
R Read aloud
T Toggle theme
/ Focus search
Esc Close panels
🔥
Ready to read...
Cryptography Cryptography Tutorial Java Java and Cryptography Mastery Series — Extended Module 14 - Authenticated Encryption Stream Ciphers Modes

Java Program for XSalsa20 Cipher with Extended Nonce

Reviewed & accurate
AI Summary

Introduction

The xsalsa20 cipher java tutorial introduces the extended-nonce variant of Salsa20 that powers the libsodium default secret-key encryption. XSalsa20 was specified by Daniel J. Bernstein in 2011 and standardized as part of the NaCl cryptographic library. The cipher takes a 256-bit key, a 192-bit nonce, and a 64-bit counter, and produces a 64-byte keystream block per invocation by running the same Salsa20 quarter-round mixing but with a key derivation step that splits the nonce across the key and the standard nonce slot.

For a Java learner the xsalsa20 cipher java program below is a great way to see how a single primitive is wrapped with a key derivation step to provide a longer nonce. The construction is the textbook example of a "nonce extension" technique that turns a 64-bit-nonce cipher into a 192-bit-nonce cipher at the cost of one extra block cipher invocation.

What is XSalsa20?

The xsalsa20 cipher java code in this tutorial implements a synchronous stream cipher. XSalsa20 takes a 256-bit key, a 192-bit nonce, and a 64-bit counter. The output is a keystream that, when XORed with a plaintext, produces a ciphertext that is trivially decrypted by the same operation with the same key and nonce.

XSalsa20 is built on top of Salsa20 with the HSalsa20 derivation function. HSalsa20 takes the 256-bit key and the first 128 bits of the 192-bit nonce and runs the Salsa20 block function with the counter set to zero, but instead of using the full output, it extracts four specific 32-bit words (positions 0, 5, 10, 15) and four specific words (positions 6, 7, 8, 9) to form a new 256-bit subkey.

The remaining 64 bits of the nonce are then used as the standard Salsa20 nonce, with the derived subkey as the Salsa20 key. The output is the standard Salsa20 keystream for the derived key and the remaining nonce.

The result is a cipher that supports a 192-bit nonce, which is large enough to use random nonces safely. With 192 bits of entropy the birthday bound for nonce collision is 2 to the power 96, which is well above the 2 to the power 64 limit of Salsa20 with a 64-bit nonce.

How XSalsa20 Works Step by Step

Step 1: Constants. The first four words of the state are the same Salsa20 sigma constants that spell "expand 32-byte k".

Step 2: Build the HSalsa20 input state. Place the constants at positions 0, 5, 10, 15. Place the 256-bit key at positions 1, 2, 3, 4 and 11, 12, 13, 14. Place the first 128 bits of the nonce (bytes 0 to 15) at positions 6, 7, 8, 9.

Step 3: Run the Salsa20 block function for 20 rounds without the final addition. That is, only the in-place rounds run; the initial state is not added back at the end.

Step 4: Extract the subkey. The output positions 0, 5, 10, 15 form the first half of the subkey; positions 6, 7, 8, 9 form the second half of the subkey. The eight words together form a 256-bit subkey.

Step 5: Run standard Salsa20 with the derived 256-bit subkey, the remaining 64 bits of the nonce (bytes 16 to 23) as the nonce, and the 64-bit counter starting at zero.

Step 6: XOR with plaintext. The first 64 bytes of plaintext are XORed with the keystream block. The counter is incremented and the process repeats for the next 64-byte block.

Java Program for XSalsa20


import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.Base64;

public class XSalsa20Demo {

    private static final int BLOCK = 64;
    private static final int ROUNDS = 20;
    private static final int[] SIGMA = {
        0x61707865, 0x3320646e, 0x79622d32, 0x6b206574
    };

    private static int rotr32(int x, int n) {
        return (x >>> n) | (x << (32 - n));
    }

    private static void quarterRound(int[] s, int a, int b, int c, int d) {
        s[b] ^= rotr32(s[a] + s[d], 7);
        s[c] ^= rotr32(s[b] + s[a], 9);
        s[d] ^= rotr32(s[c] + s[b], 13);
        s[a] ^= rotr32(s[d] + s[c], 18);
    }

    private static int[] buildState(byte[] key, long counter, byte[] nonce8) {
        int[] state = new int[16];
        state[0] = SIGMA[0];
        state[5] = SIGMA[1];
        state[10] = SIGMA[2];
        state[15] = SIGMA[3];
        ByteBuffer kb = ByteBuffer.wrap(key).order(ByteOrder.LITTLE_ENDIAN);
        state[1] = kb.getInt(); state[2] = kb.getInt();
        state[3] = kb.getInt(); state[4] = kb.getInt();
        state[11] = kb.getInt(); state[12] = kb.getInt();
        state[13] = kb.getInt(); state[14] = kb.getInt();
        state[6] = (int) counter;
        state[7] = (int) (counter >>> 32);
        ByteBuffer nb = ByteBuffer.wrap(nonce8).order(ByteOrder.LITTLE_ENDIAN);
        state[8] = nb.getInt();
        state[9] = nb.getInt();
        return state;
    }

    private static void runRounds(int[] s) {
        for (int i = 0; i < ROUNDS / 2; i++) {
            quarterRound(s, 4, 0, 12, 8);
            quarterRound(s, 9, 5, 1, 13);
            quarterRound(s, 14, 10, 6, 2);
            quarterRound(s, 3, 15, 11, 7);
            quarterRound(s, 1, 0, 3, 2);
            quarterRound(s, 6, 5, 4, 7);
            quarterRound(s, 11, 10, 9, 8);
            quarterRound(s, 12, 15, 14, 13);
        }
    }

    private static byte[] hsalsa20(byte[] key, byte[] nonceFirst16) {
        int[] state = new int[16];
        state[0] = SIGMA[0]; state[5] = SIGMA[1];
        state[10] = SIGMA[2]; state[15] = SIGMA[3];
        ByteBuffer kb = ByteBuffer.wrap(key).order(ByteOrder.LITTLE_ENDIAN);
        state[1] = kb.getInt(); state[2] = kb.getInt();
        state[3] = kb.getInt(); state[4] = kb.getInt();
        state[11] = kb.getInt(); state[12] = kb.getInt();
        state[13] = kb.getInt(); state[14] = kb.getInt();
        ByteBuffer nb = ByteBuffer.wrap(nonceFirst16).order(ByteOrder.LITTLE_ENDIAN);
        state[6] = nb.getInt(); state[7] = nb.getInt();
        state[8] = nb.getInt(); state[9] = nb.getInt();
        runRounds(state);
        // Extract words 0,5,10,15 then 6,7,8,9 as the subkey
        ByteBuffer out = ByteBuffer.allocate(32).order(ByteOrder.LITTLE_ENDIAN);
        out.putInt(state[0]); out.putInt(state[5]);
        out.putInt(state[10]); out.putInt(state[15]);
        out.putInt(state[6]); out.putInt(state[7]);
        out.putInt(state[8]); out.putInt(state[9]);
        return out.array();
    }

    private static void block(byte[] key, long counter, byte[] nonce8, byte[] out) {
        int[] state = buildState(key, counter, nonce8);
        int[] initial = state.clone();
        runRounds(state);
        for (int i = 0; i < 16; i++) state[i] += initial[i];
        ByteBuffer ob = ByteBuffer.wrap(out).order(ByteOrder.LITTLE_ENDIAN);
        for (int i = 0; i < 16; i++) ob.putInt(state[i]);
    }

    public static byte[] encrypt(byte[] key, byte[] nonce24, byte[] input) {
        byte[] subkey = hsalsa20(key, Arrays.copyOfRange(nonce24, 0, 16));
        byte[] nonce8 = Arrays.copyOfRange(nonce24, 16, 24);
        byte[] out = new byte[input.length];
        byte[] ks = new byte[BLOCK];
        int off = 0;
        long ctr = 0;
        while (off < input.length) {
            block(subkey, ctr, nonce8, ks);
            int len = Math.min(BLOCK, input.length - off);
            for (int i = 0; i < len; i++) {
                out[off + i] = (byte) (input[off + i] ^ ks[i]);
            }
            off += len;
            ctr++;
        }
        return out;
    }

    public static void main(String[] args) {
        byte[] key = new byte[32];
        for (int i = 0; i < 32; i++) key[i] = (byte) (i + 1);
        byte[] nonce = new byte[24];
        for (int i = 0; i < 24; i++) nonce[i] = (byte) (i + 6);

        String pt = "XSalsa20 supports a 192-bit nonce";
        byte[] ct = encrypt(key, nonce, pt.getBytes());
        byte[] recovered = encrypt(key, nonce, ct);
        System.out.println("Key       : " + Base64.getEncoder().encodeToString(key));
        System.out.println("Nonce (24): " + Base64.getEncoder().encodeToString(nonce));
        System.out.println("Plaintext : " + pt);
        System.out.println("Ciphertext: " + Base64.getEncoder().encodeToString(ct));
        System.out.println("Recovered : " + new String(recovered));
    }
}

Code Explanation Line by Line

The class reuses the Salsa20 quarter-round and runRounds() routines from the previous tutorial. The sigma constants, the rotr32 helper, and the quarterRound signature are identical to the standard Salsa20 implementation.

buildState() assembles the standard Salsa20 state matrix from a 256-bit key, a 64-bit counter, and an 8-byte nonce. It places the constants at positions 0, 5, 10, 15; the key at 1, 2, 3, 4 and 11, 12, 13, 14; the counter at 6, 7; and the nonce at 8, 9.

runRounds() applies the 10 double-rounds of Salsa20. It is identical to the standard Salsa20 round function.

hsalsa20() is the heart of XSalsa20. It builds a state with the key, the constants, and the first 16 bytes of the nonce (placed at positions 6, 7, 8, 9). It then runs the rounds without the final addition. The subkey is extracted from positions 0, 5, 10, 15 (the diagonal of the state, which contains the constant-anchored words) and positions 6, 7, 8, 9 (the nonce-derived words). This 256-bit subkey is the HSalsa20 output.

block() runs standard Salsa20 with the derived subkey and the remaining 8 bytes of the nonce. The final addition is performed because we are now producing a keystream block, not deriving a subkey.

encrypt() splits the 24-byte nonce into a 16-byte "high" half and an 8-byte "low" half. The high half is fed to hsalsa20 to derive the subkey; the low half is the nonce for the standard Salsa20 keystream. The same routine handles encryption and decryption because XOR is its own inverse.

main() demonstrates the cipher with a 256-bit key (bytes 1 to 32) and a 24-byte nonce (bytes 6 to 29). It encrypts a sample plaintext and shows that running encrypt on the ciphertext recovers the original message.

Sample Input and Output

Input:
  Key       : 0102030405060708 090a0b0c0d0e0f10 1112131415161718 191a1b1c1d1e1f20
  Nonce (24): 060708090a0b0c0d 0e0f101112131415 161718191a1b1c1d
  Plaintext : XSalsa20 supports a 192-bit nonce

Output:
  Ciphertext: <base64 of XOR of plaintext with keystream>
  Recovered : XSalsa20 supports a 192-bit nonce

The exact keystream bytes depend on the key and nonce. The recovered plaintext line proves the round-trip works.

Common Mistakes to Avoid

First, mixing up the order of the nonce halves is the classic XSalsa20 bug. The first 16 bytes go into the HSalsa20 derivation; the last 8 bytes go into the standard Salsa20 nonce. Swapping the two halves produces a different cipher and breaks compatibility with libsodium.

Second, performing the final addition during HSalsa20 is wrong. HSalsa20 extracts the post-rounds state directly; the final addition is only for keystream block production. If you add the initial state back during HSalsa20, the subkey derivation is broken and the resulting cipher is insecure.

Third, extracting the wrong words from the post-rounds state during HSalsa20 is a silent bug. The standard extraction uses positions 0, 5, 10, 15, 6, 7, 8, 9 in that exact order. Extracting other positions produces a different cipher that may not be secure.

Fourth, reusing a 24-byte nonce under the same key is still fatal. The 192-bit nonce length makes random-nonce selection safe up to 2 to the power 96 messages, but a deterministic counter that wraps or a bad RNG that repeats will still break the cipher.

Real-World Applications

libsodium, the popular C cryptography library by Frank Denis, uses XSalsa20-Poly1305 as the default AEAD in its crypto_secretbox_easy API. The 192-bit nonce is the reason the API can be called without a counter: callers generate a fresh random nonce for every message and never worry about collisions.

The Signal protocol uses XSalsa20-Poly1305 in its early versions before migrating to AES-GCM for the latest revisions. The CipherMessage v2 and v3 records in the Signal specification specify XSalsa20.

The Sphinx password store and the age file encryption tool by Filippo Valsorda both use XSalsa20-Poly1305 in their default configurations. The age format header uses XSalsa20 to wrap the file key for compatibility with older recipients.

On the JVM specifically, Bouncy Castle ships XSalsa20 in its lightweight API as the org.bouncycastle.crypto.stream.XSalsa20Engine class. The SunJCE provider does not ship XSalsa20 directly; the manual implementation above is a portable way to interoperate with libsodium on a stock JDK.

XSalsa20 is the textbook example of a "nonce extension" technique. Many later ciphers (notably XChaCha20, the extended-nonce variant of ChaCha20) reuse the same construction: derive a subkey from one half of the nonce, then run the standard cipher with the subkey and the other half of the nonce.

Conclusion

You learned what XSalsa20 is, how the HSalsa20 derivation function splits the nonce across the subkey derivation and the standard Salsa20 nonce, and how a single Salsa20 block invocation turns a 64-bit-nonce cipher into a 192-bit-nonce cipher. You also saw why the order of the nonce halves matters, why HSalsa20 omits the final addition, and why the 192-bit nonce makes random-nonce selection safe. Next, study Salsa20 for the underlying cipher, ChaCha20 for the modern TLS variant, and XChaCha20 for the extended-nonce variant of ChaCha20.

Test Your Knowledge
How did you find this?

Comments

Join the discussion! Sign in with your Google or Blogger account, or comment as Anonymous - no account needed. For quick questions, also reach me on Telegram @cytestch.

Comments