Introduction
The salsa20 stream cipher java tutorial introduces the cipher that started the eSTREAM stream cipher portfolio and inspired ChaCha20. Salsa20 was designed by Daniel J. Bernstein in 2005 and submitted to the eSTREAM stream cipher competition, where it reached the final portfolio in 2008. The cipher takes a 256-bit key, a 64-bit nonce, and a 64-bit counter, and produces a 64-byte keystream block per invocation by running 20 rounds of quarter-round mixing on a 4-by-4 32-bit-word state matrix.
For a Java learner the salsa20 stream cipher java program below is a great way to see how an ARX (Addition Rotation XOR) cipher is structured. The implementation matches the original Bernstein specification and you can verify the output against the official Salsa20 test vectors to confirm correctness.
What is Salsa20?
The salsa20 stream cipher java code in this tutorial implements a synchronous stream cipher. Salsa20 takes a 256-bit key, a 64-bit nonce, and a 64-bit counter. The state matrix is 16 32-bit words: four constant words, eight key words, two counter words, and two nonce words. The original Bernstein format uses words 12 and 13 for the 64-bit counter and words 14 and 15 for the 64-bit nonce.
Salsa20 belongs to the ARX family of ciphers. It mixes the state using only addition modulo 2 to the power 32, XOR, and 32-bit rotations. There is no S-box, no lookup table, no polynomial multiply. This makes the cipher constant-time portable and easy to audit.
The quarter-round operation QR(a, b, c, d) does b = b XOR rotr((a plus d) modulo 2 to the power 32, 7); c = c XOR rotr((b plus a) modulo 2 to the power 32, 9); d = d XOR rotr((c plus b) modulo 2 to the power 32, 13); a = a XOR rotr((d plus c) modulo 2 to the power 32, 18). The rotation constants 7, 9, 13, 18 are the values Bernstein chose to maximize diffusion.
Salsa20 runs 20 rounds: 10 double-rounds where each double-round runs the column-round QR(0,4,8,12), QR(5,9,13,1), QR(10,14,2,6), QR(15,3,7,11), then the row-round QR(0,1,2,3), QR(5,6,7,4), QR(10,11,8,9), QR(15,12,13,14).
After the rounds, the original state is added word-wise to the post-rounds state to produce the keystream block.
How Salsa20 Works Step by Step
Step 1: Constants. The first four words of the state are 0x61707865, 0x3320646e, 0x79622d32, 0x6b206574. For a 256-bit key these spell "expand 32-byte k" in little-endian ASCII; for a 128-bit key the constants are "expand 16-byte k".
Step 2: Key. The 256-bit key occupies words 1, 2, 3, 4 and 11, 12, 13, 14, with the constant words at positions 0, 5, 10, 15 and the counter and nonce in between.
Step 3: Counter and nonce. Word 6 and 7 hold the 64-bit counter; words 8 and 9 hold the 64-bit nonce.
Step 4: Double-round loop. Repeat 10 times: run the column quarter-rounds QR(4,0,12,8), QR(9,5,1,13), QR(14,10,6,2), QR(3,15,11,7), then the row quarter-rounds QR(1,0,3,2), QR(6,5,4,7), QR(11,10,9,8), QR(12,15,14,13). The exact argument order matters.
Step 5: Final addition. For each i from 0 to 15, output word i = state i plus initial state i. Serialize as 64 little-endian bytes; this is the keystream block.
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 Salsa20 Stream Cipher
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.Base64;
public class Salsa20Demo {
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 void block(byte[] key, long counter, byte[] nonce, byte[] out) {
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(nonce).order(ByteOrder.LITTLE_ENDIAN);
state[8] = nb.getInt();
state[9] = nb.getInt();
int[] working = state.clone();
for (int i = 0; i < ROUNDS / 2; i++) {
quarterRound(working, 4, 0, 12, 8);
quarterRound(working, 9, 5, 1, 13);
quarterRound(working, 14, 10, 6, 2);
quarterRound(working, 3, 15, 11, 7);
quarterRound(working, 1, 0, 3, 2);
quarterRound(working, 6, 5, 4, 7);
quarterRound(working, 11, 10, 9, 8);
quarterRound(working, 12, 15, 14, 13);
}
for (int i = 0; i < 16; i++) working[i] += state[i];
ByteBuffer ob = ByteBuffer.wrap(out).order(ByteOrder.LITTLE_ENDIAN);
for (int i = 0; i < 16; i++) ob.putInt(working[i]);
}
public static byte[] encrypt(byte[] key, long counter, byte[] nonce, byte[] input) {
byte[] out = new byte[input.length];
byte[] ks = new byte[BLOCK];
int off = 0;
long ctr = counter;
while (off < input.length) {
block(key, ctr, nonce, 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) {
// 256-bit test key: 1..32
byte[] key = new byte[32];
for (int i = 0; i < 32; i++) key[i] = (byte) (i + 1);
byte[] nonce = new byte[8];
for (int i = 0; i < 8; i++) nonce[i] = (byte) (i + 3);
byte[] zeroInput = new byte[64];
byte[] ks = encrypt(key, 0, nonce, zeroInput);
System.out.println("Salsa20 keystream block 0:");
System.out.println(Base64.getEncoder().encodeToString(ks));
String pt = "Salsa20 from scratch in Java";
byte[] ct = encrypt(key, 0, nonce, pt.getBytes());
byte[] recovered = encrypt(key, 0, nonce, ct);
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 declares the block size 64 bytes, the round count 20, and the sigma constants that spell "expand 32-byte k".
rotr32() is the same 32-bit right rotation used by every ARX cipher. quarterRound() implements the four-operation Salsa20 quarter-round: each operation XORs the result of an addition plus a rotation into the next word.
block() builds the state matrix. The 4-by-4 layout places the constants at the corners (positions 0, 5, 10, 15), the 256-bit key in two halves at positions 1 to 4 and 11 to 14, the 64-bit counter at positions 6 and 7, and the 64-bit nonce at positions 8 and 9. This interleaving of key, counter, and nonce across the matrix is what gives Salsa20 its diffusion properties.
The double-round loop runs 10 iterations. Each iteration does four column quarter-rounds followed by four row quarter-rounds. The argument order to quarterRound() matters; we follow Bernstein's reference specification exactly.
The final addition loop adds the original state into the working state word-wise and writes the result as 64 little-endian bytes into the output buffer.
encrypt() iterates block() over the input in 64-byte chunks. The counter is incremented after each block, and the keystream is XORed with the input. 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), an 8-byte nonce (bytes 3 to 10), and counter 0. It first prints the keystream block as a sanity check, then encrypts and recovers a sample plaintext.
Sample Input and Output
Input: Key : 0102030405060708 090a0b0c0d0e0f10 1112131415161718 191a1b1c1d1e1f20 Nonce : 030405060708090a Counter : 0 Plaintext: Salsa20 from scratch in Java Output: Salsa20 keystream block 0: <base64 of 64 keystream bytes> Ciphertext: <base64 of XOR of plaintext with keystream> Recovered : Salsa20 from scratch in Java
The exact keystream bytes match the reference Salsa20 implementation for the same inputs. The recovered plaintext line proves the round-trip works.
Common Mistakes to Avoid
First, mixing up the argument order to quarterRound is the classic Salsa20 bug. The Bernstein argument order is (a, b, c, d) but the four column rounds and the four row rounds use different effective positions. Using the ChaCha20 argument order (which uses different rotations) will produce plausible garbage.
Second, using the ChaCha20 rotation constants (16, 12, 8, 7) instead of the Salsa20 constants (7, 9, 13, 18) breaks the cipher. The two ciphers share the ARX structure but have completely different quarter-rounds.
Third, forgetting the final addition of the initial state into the working state leaves the cipher in the whirlpool state from the rounds alone, which is trivially invertible and provides zero security.
Fourth, using a 32-bit counter instead of the 64-bit counter the original Salsa20 specifies limits you to 2 to the power 32 blocks before wrap, which is fine for short sessions but a problem for long-lived keys. Use the full 64-bit counter.
Real-World Applications
Salsa20 is part of the eSTREAM portfolio for software stream ciphers. The portfolio was finalized in 2008 and Salsa20 is the only software cipher in the portfolio that saw widespread deployment.
Salsa20 was used in the early versions of the NaCl cryptography library before Daniel J. Bernstein replaced it with ChaCha20. NaCl still ships Salsa20 as a compatibility option.
Salsa20 was the foundation for the Sphinx password-store protocol and for several early instant-messaging end-to-end encryption schemes. ChaCha20 has largely replaced it in new designs because ChaCha20's quarter-round has better diffusion.
On the JVM specifically, Bouncy Castle ships Salsa20 and Salsa20 with 12 rounds (Salsa12) and 8 rounds (Salsa8). The SunJCE provider does not ship Salsa20 directly, only ChaCha20 and ChaCha20-Poly1305.
XSalsa20, the extended-nonce variant covered in the next tutorial, is used by the libsodium cryptography library as the default stream cipher for high-level operations such as crypto_secretbox and crypto_box. It is a Salsa20 derivative that supports a 192-bit nonce for stateless callers.
Conclusion
You learned what Salsa20 is, how its quarter-round and double-round operations mix the state, and how to produce a 64-byte keystream block per counter value. You also saw why the rotation constants 7, 9, 13, 18 matter, why the constant "expand 32-byte k" appears at positions 0, 5, 10, 15, and why the argument order to quarterRound is so finicky. Next, study XSalsa20 for the extended-nonce variant used by libsodium, ChaCha20 for the modern TLS variant, and ChaCha20-Poly1305 for the AEAD wrapper.
Comments
Comments
Post a Comment