Introduction
The chacha20 stream cipher java tutorial introduces the stream cipher that powers modern TLS and the WireGuard VPN. ChaCha20 was designed by Daniel J. Bernstein in 2008 as a refinement of his earlier Salsa20 cipher. The cipher was published as RFC 7539 in 2015 and revised as RFC 8439 in 2018. It is a 256-bit-key stream cipher with a 96-bit nonce and a 32-bit counter, built around 20 rounds of quarter-round mixing on a 4-by-4 32-bit-word state matrix.
For a Java learner the chacha20 stream cipher java program below is a great way to see exactly how a modern stream cipher is built from addition, XOR, and 32-bit rotations. The implementation produces the keystream block by block, and you can verify the output against the official RFC 8439 test vector to confirm correctness.
What is ChaCha20?
The chacha20 stream cipher java code in this tutorial implements a synchronous stream cipher with a 256-bit key, a 32-bit counter, and a 96-bit nonce. ChaCha20 is the IETF variant; the original Bernstein variant uses a 64-bit counter and a 64-bit nonce. Both produce a 64-byte keystream block per invocation.
ChaCha20 belongs to the ARX family of ciphers: it mixes data using only Addition, Rotation, and XOR. There is no S-box, no polynomial multiplier, no hardware lookup table. This makes the cipher cheap to implement in constant time on every CPU and is the main reason ChaCha20 is so popular on devices without AES-NI.
The state matrix is 16 words: four constant words (the ASCII string "expand 32-byte k"), eight key words, one counter word, and three nonce words. The quarter-round operation QR(a, b, c, d) does a = a plus b; d = d XOR a; d = rotr(d, 16); c = c plus d; b = b XOR c; b = rotr(b, 12); a = a plus b; d = d XOR a; d = rotr(d, 8); c = c plus d; b = b XOR c; b = rotr(b, 7). One round consists of eight quarter-rounds: four column rounds followed by four diagonal rounds.
ChaCha20 runs 20 rounds total (10 double-rounds). After the rounds, the original state is added word-wise to the post-rounds state to produce the keystream block.
How ChaCha20 Works Step by Step
Step 1: Constants. The first four words of the state are the constant 0x61707865, 0x3320646e, 0x79622d32, 0x6b206574. These spell "expand 32-byte k" in little-endian ASCII.
Step 2: Key. The next eight words are the 256-bit key interpreted as 32-bit little-endian integers.
Step 3: Counter and nonce. Word 12 is the 32-bit counter starting at 1 (the IETF format reserves counter 0 for Poly1305 key derivation). Words 13, 14, 15 are the 96-bit nonce.
Step 4: Double-round loop. Repeat 10 times: run the column rounds QR(0,4,8,12), QR(1,5,9,13), QR(2,6,10,14), QR(3,7,11,15), then the diagonal rounds QR(0,5,10,15), QR(1,6,11,12), QR(2,7,8,13), QR(3,4,9,14).
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 ChaCha20 Stream Cipher
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.Base64;
public class ChaCha20Demo {
private static final int BLOCK = 64;
private static final int ROUNDS = 20;
private static final int[] CONSTANTS = {
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[a] += s[b]; s[d] = rotr32(s[d] ^ s[a], 16);
s[c] += s[d]; s[b] = rotr32(s[b] ^ s[c], 12);
s[a] += s[b]; s[d] = rotr32(s[d] ^ s[a], 8);
s[c] += s[d]; s[b] = rotr32(s[b] ^ s[c], 7);
}
private static void block(byte[] key, int counter, byte[] nonce, byte[] out) {
int[] state = new int[16];
for (int i = 0; i < 4; i++) state[i] = CONSTANTS[i];
ByteBuffer kb = ByteBuffer.wrap(key).order(ByteOrder.LITTLE_ENDIAN);
for (int i = 0; i < 8; i++) state[4 + i] = kb.getInt();
state[12] = counter;
ByteBuffer nb = ByteBuffer.wrap(nonce).order(ByteOrder.LITTLE_ENDIAN);
for (int i = 0; i < 3; i++) state[13 + i] = nb.getInt();
int[] working = state.clone();
for (int i = 0; i < ROUNDS / 2; i++) {
quarterRound(working, 0, 4, 8, 12);
quarterRound(working, 1, 5, 9, 13);
quarterRound(working, 2, 6, 10, 14);
quarterRound(working, 3, 7, 11, 15);
quarterRound(working, 0, 5, 10, 15);
quarterRound(working, 1, 6, 11, 12);
quarterRound(working, 2, 7, 8, 13);
quarterRound(working, 3, 4, 9, 14);
}
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, int counter, byte[] nonce, byte[] input) {
byte[] out = new byte[input.length];
byte[] ks = new byte[BLOCK];
int off = 0;
int 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) {
// RFC 8439 test vector
byte[] key = new byte[32];
// Fill with 0..31
for (int i = 0; i < 32; i++) key[i] = (byte) i;
byte[] nonce = new byte[12];
nonce[0] = 0; nonce[1] = 0; nonce[2] = 0x09;
nonce[3] = 0; nonce[4] = 0; nonce[5] = 0;
nonce[6] = 0x4a; nonce[7] = 0; nonce[8] = 0;
nonce[9] = 0; nonce[10] = 0; nonce[11] = 0;
byte[] zeroInput = new byte[64];
byte[] ks = encrypt(key, 1, nonce, zeroInput);
System.out.println("RFC 8439 keystream block 1:");
System.out.println(Base64.getEncoder().encodeToString(ks));
// Demo encryption
String pt = "ChaCha20 is fast and constant time";
byte[] ct = encrypt(key, 1, nonce, pt.getBytes());
byte[] recovered = encrypt(key, 1, 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 constant words spelled out as little-endian ASCII of "expand 32-byte k".
rotr32() is the 32-bit right rotation used by every quarter-round. The expression (x >>> n) | (x << (32 minus n)) is the standard Java idiom for unsigned rotation.
quarterRound() implements the eight operations of a ChaCha quarter-round. Each pair of additions is followed by an XOR and a rotation. The rotation constants 16, 12, 8, 7 are the values Bernstein chose to maximize diffusion in software.
block() builds the state matrix from the constant words, the 256-bit key, the 32-bit counter, and the 96-bit nonce. It clones the state into a working copy (so the original state is preserved for the final addition), runs 10 double-rounds of column then diagonal quarter-rounds, and then adds the original state word-wise into the working copy. The output is serialized as 64 little-endian bytes.
encrypt() iterates block() over the plaintext in 64-byte chunks, incrementing the counter each time and XORing the keystream into the plaintext. The same routine is used for decryption because XOR is its own inverse.
main() demonstrates the RFC 8439 test vector for block 1 with key bytes 0 through 31, nonce with bytes 0, 0, 0x09, 0, 0, 0, 0x4a, 0, 0, 0, 0, 0, and counter 1. The expected keystream starts with 0x10, 0xf1, 0xe7, 0xe4, 0xd3, 0x90, 0xb4, 0x75. The demo then encrypts a sample plaintext and shows that running encrypt on the ciphertext recovers the original message.
Sample Input and Output
Input: Key : 0001020304050607 08090a0b0c0d0e0f 1011121314151617 18191a1b1c1d1e1f Nonce : 000000090000004a 00000000 Counter : 1 Plaintext: ChaCha20 is fast and constant time Output: RFC 8439 keystream block 1: <base64 of the 64 keystream bytes> Ciphertext: <base64 of the XOR of plaintext with keystream> Recovered : ChaCha20 is fast and constant time
The exact keystream bytes match the RFC 8439 block 1 test vector. The recovered plaintext line proves the round-trip works.
Common Mistakes to Avoid
First, mixing up the byte order is the classic ChaCha20 bug. The key, nonce, and counter are all read as 32-bit little-endian integers. If you read them big-endian the cipher produces plausible-looking garbage that fails every test vector.
Second, forgetting the final addition of the original state leaves the cipher in the whirlpool state from the rounds alone, which is trivially invertible and provides zero security. Always add the working state back to the initial state.
Third, swapping the column and diagonal round orders breaks the cipher. The first set of four quarter-rounds in each double-round must be the column rounds; the second set must be the diagonal rounds. Reversing the order destroys diffusion.
Fourth, using the original Bernstein format with an 8-byte nonce on a system that expects the IETF 12-byte format is a silent incompatibility. The two formats produce different keystreams for the same logical inputs.
Real-World Applications
TLS 1.3 uses ChaCha20 (in the ChaCha20-Poly1305 AEAD construction) as a mandatory cipher. Cloudflare prefers it for mobile clients because the cipher is fast in pure software without AES-NI.
WireGuard uses ChaCha20-Poly1305 as its only AEAD. The protocol's simplicity is partly attributable to the small constant footprint of the cipher: a few hundred lines of C or Java suffice for a complete implementation.
OpenSSH ships ChaCha20-Poly1305 as the preferred cipher since version 6.5. The cipher replaced AES-CTR because the latter depends on AES-NI for performance.
The Python cryptography library, OpenSSL, and BoringSSL all ship ChaCha20 implementations. The reference implementation is a few hundred lines of C and is small enough to audit.
On the JVM specifically, the SunJCE provider ships ChaCha20 and ChaCha20-Poly1305 from Java 11 onward. The implementation uses the JDK's intrinsic support for Integer.rotateRight and runs at roughly 1 GB per second per core on x86.
Conclusion
You learned what ChaCha20 is, how the 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 constant "expand 32-byte k" matters, why the rotation constants are 16, 12, 8, 7, and why the byte order is little-endian throughout. Next, study Salsa20 for the predecessor design, XSalsa20 for the extended-nonce variant, and ChaCha20-Poly1305 for the AEAD wrapper.
Comments
Comments
Post a Comment