Skip to content

Commit 6f8b350

Browse files
authored
Create ChaCha20.java
1 parent bb6385e commit 6f8b350

File tree

1 file changed

+201
-0
lines changed

1 file changed

+201
-0
lines changed
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package com.thealgorithms.ciphers;
2+
3+
import java.nio.ByteBuffer;
4+
import java.nio.ByteOrder;
5+
import java.util.Arrays;
6+
7+
/**
8+
* Implements the ChaCha20 stream cipher algorithm as specified in RFC 8439.
9+
* ChaCha20 is a refinement of the Salsa20 algorithm and is known for its
10+
* speed and security on modern CPUs.
11+
*
12+
* <p>Wikipedia: https://en.wikipedia.org/wiki/ChaCha20
13+
* <p>RFC 8439: https://tools.ietf.org/html/rfc8439
14+
*
15+
* @author Mitrajit Ghorui(KeyKyrios)
16+
*/
17+
public final class ChaCha20 {
18+
19+
private ChaCha20() {
20+
} // Static class
21+
22+
private static final int KEY_SIZE_BYTES = 32; // 256 bits
23+
private static final int NONCE_SIZE_BYTES = 12; // 96 bits
24+
private static final int BLOCK_SIZE_BYTES = 64; // 512 bits
25+
26+
// ChaCha20 constants "expand 32-byte k"
27+
private static final int[] CONSTANTS = {0x61707865, 0x3320646e, 0x79622d32, 0x6b206574};
28+
29+
/**
30+
* Encrypts the given plaintext using ChaCha20.
31+
* Since ChaCha20 is a stream cipher, encryption and decryption are the same
32+
* operation (XOR with keystream).
33+
*
34+
* @param key The 256-bit (32-byte) secret key.
35+
* @param nonce The 96-bit (12-byte) nonce. Must be unique for each
36+
* encryption with the same key.
37+
* @param plaintext The data to encrypt.
38+
* @return The resulting ciphertext.
39+
* @throws IllegalArgumentException if the key or nonce has an invalid length,
40+
* or if any input is null.
41+
*/
42+
public static byte[] encrypt(final byte[] key, final byte[] nonce, final byte[] plaintext) {
43+
validateInputs(key, nonce, plaintext);
44+
return process(key, nonce, plaintext, 1); // Start with block counter 1 as per RFC 8439
45+
}
46+
47+
/**
48+
* Decrypts the given ciphertext using ChaCha20.
49+
* Since ChaCha20 is a stream cipher, encryption and decryption are the same
50+
* operation (XOR with keystream).
51+
*
52+
* @param key The 256-bit (32-byte) secret key.
53+
* @param nonce The 96-bit (12-byte) nonce used during encryption.
54+
* @param ciphertext The data to decrypt.
55+
* @return The resulting plaintext.
56+
* @throws IllegalArgumentException if the key or nonce has an invalid length,
57+
* or if any input is null.
58+
*/
59+
public static byte[] decrypt(final byte[] key, final byte[] nonce, final byte[] ciphertext) {
60+
validateInputs(key, nonce, ciphertext);
61+
return process(key, nonce, ciphertext, 1); // Start with block counter 1
62+
}
63+
64+
/**
65+
* Performs the core ChaCha20 processing (XOR with keystream).
66+
*
67+
* @param key The 32-byte key.
68+
* @param nonce The 12-byte nonce.
69+
* @param data Plaintext or Ciphertext.
70+
* @param counter The initial block counter.
71+
* @return The result of XORing data with the generated keystream.
72+
*/
73+
private static byte[] process(final byte[] key, final byte[] nonce, final byte[] data, final int counter) {
74+
byte[] output = new byte[data.length];
75+
ByteBuffer keyStreamBlock = ByteBuffer.allocate(BLOCK_SIZE_BYTES).order(ByteOrder.LITTLE_ENDIAN);
76+
int offset = 0;
77+
int blockCounter = counter;
78+
79+
while (offset < data.length) {
80+
keyStreamBlock.clear();
81+
generateChaCha20Block(key, nonce, blockCounter++, keyStreamBlock.array());
82+
83+
int length = Math.min(BLOCK_SIZE_BYTES, data.length - offset);
84+
for (int i = 0; i < length; i++) {
85+
output[offset + i] = (byte) (data[offset + i] ^ keyStreamBlock.get(i));
86+
}
87+
offset += length;
88+
}
89+
return output;
90+
}
91+
92+
/**
93+
* Generates a 64-byte ChaCha20 keystream block.
94+
*
95+
* @param key The 32-byte key.
96+
* @param nonce The 12-byte nonce.
97+
* @param counter The block counter.
98+
* @param output The 64-byte array to store the generated block.
99+
*/
100+
private static void generateChaCha20Block(final byte[] key, final byte[] nonce, final int counter, final byte[] output) {
101+
int[] state = initializeState(key, nonce, counter);
102+
int[] workingState = Arrays.copyOf(state, state.length);
103+
104+
// 20 rounds (10 double rounds)
105+
for (int i = 0; i < 10; i++) {
106+
// Column rounds
107+
quarterRound(workingState, 0, 4, 8, 12);
108+
quarterRound(workingState, 1, 5, 9, 13);
109+
quarterRound(workingState, 2, 6, 10, 14);
110+
quarterRound(workingState, 3, 7, 11, 15);
111+
// Diagonal rounds
112+
quarterRound(workingState, 0, 5, 10, 15);
113+
quarterRound(workingState, 1, 6, 11, 12);
114+
quarterRound(workingState, 2, 7, 8, 13);
115+
quarterRound(workingState, 3, 4, 9, 14);
116+
}
117+
118+
// Add initial state to the final state
119+
for (int i = 0; i < state.length; i++) {
120+
workingState[i] += state[i];
121+
}
122+
123+
// Serialize state to output bytes (Little Endian)
124+
ByteBuffer buffer = ByteBuffer.wrap(output).order(ByteOrder.LITTLE_ENDIAN);
125+
for (int val : workingState) {
126+
buffer.putInt(val);
127+
}
128+
}
129+
130+
/**
131+
* Initializes the 16-word (512-bit) ChaCha20 state.
132+
*/
133+
private static int[] initializeState(final byte[] key, final byte[] nonce, final int counter) {
134+
int[] state = new int[16];
135+
ByteBuffer keyBuffer = ByteBuffer.wrap(key).order(ByteOrder.LITTLE_ENDIAN);
136+
ByteBuffer nonceBuffer = ByteBuffer.wrap(nonce).order(ByteOrder.LITTLE_ENDIAN);
137+
138+
// Constants
139+
state[0] = CONSTANTS[0];
140+
state[1] = CONSTANTS[1];
141+
state[2] = CONSTANTS[2];
142+
state[3] = CONSTANTS[3];
143+
144+
// Key (8 words)
145+
for (int i = 0; i < 8; i++) {
146+
state[4 + i] = keyBuffer.getInt(i * 4);
147+
}
148+
149+
// Counter (1 word)
150+
state[12] = counter;
151+
152+
// Nonce (3 words)
153+
for (int i = 0; i < 3; i++) {
154+
state[13 + i] = nonceBuffer.getInt(i * 4);
155+
}
156+
157+
return state;
158+
}
159+
160+
/**
161+
* The ChaCha20 quarter round function. Modifies the state array in place.
162+
*/
163+
private static void quarterRound(final int[] state, final int a, final int b, final int c, final int d) {
164+
state[a] += state[b];
165+
state[d] = rotl(state[d] ^ state[a], 16);
166+
state[c] += state[d];
167+
state[b] = rotl(state[b] ^ state[c], 12);
168+
state[a] += state[b];
169+
state[d] = rotl(state[d] ^ state[a], 8);
170+
state[c] += state[d];
171+
state[b] = rotl(state[b] ^ state[c], 7);
172+
}
173+
174+
/**
175+
* Rotates the bits of an integer to the left.
176+
*/
177+
private static int rotl(final int value, final int shift) {
178+
return (value << shift) | (value >>> (32 - shift));
179+
}
180+
181+
/**
182+
* Validates key, nonce, and data inputs.
183+
*/
184+
private static void validateInputs(final byte[] key, final byte[] nonce, final byte[] data) {
185+
if (key == null) {
186+
throw new IllegalArgumentException("Key cannot be null.");
187+
}
188+
if (key.length != KEY_SIZE_BYTES) {
189+
throw new IllegalArgumentException("Invalid key size. Key must be " + KEY_SIZE_BYTES + " bytes (256 bits).");
190+
}
191+
if (nonce == null) {
192+
throw new IllegalArgumentException("Nonce cannot be null.");
193+
}
194+
if (nonce.length != NONCE_SIZE_BYTES) {
195+
throw new IllegalArgumentException("Invalid nonce size. Nonce must be " + NONCE_SIZE_BYTES + " bytes (96 bits).");
196+
}
197+
if (data == null) {
198+
throw new IllegalArgumentException("Plaintext/Ciphertext cannot be null.");
199+
}
200+
}
201+
}

0 commit comments

Comments
 (0)