Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions src/main/java/com/thealgorithms/ciphers/ChaCha20.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package com.thealgorithms.ciphers;

import java.util.Objects;

/**
* Implements the ChaCha20 stream cipher described in RFC 8439.
*
* <p>This implementation is intended for educational purposes and should not
* be used as a replacement for a vetted cryptographic library in production.
*
* @see <a href="https://www.rfc-editor.org/rfc/rfc8439.html">RFC 8439</a>
*/
public final class ChaCha20 {

private static final int BLOCK_SIZE = 64;
private static final long MAX_COUNTER = 0xFFFF_FFFFL;
private static final int[] CONSTANTS = {0x61707865, 0x3320646e, 0x79622d32, 0x6b206574};

private ChaCha20() {
// Utility class
}

/**
* Encrypts bytes with ChaCha20. Decryption uses the same operation.
*
* @param input plaintext bytes; must not be {@code null}
* @param key 32-byte key
* @param nonce 12-byte nonce
* @param initialCounter unsigned 32-bit initial block counter
* @return encrypted bytes
* @throws NullPointerException if an argument is {@code null}
* @throws IllegalArgumentException if a key, nonce, or counter is invalid,
* or the counter would overflow while processing the input
*/
public static byte[] encrypt(byte[] input, byte[] key, byte[] nonce, long initialCounter) {
validateInputs(input, key, nonce, initialCounter);

long blockCount = (input.length + (long) BLOCK_SIZE - 1) / BLOCK_SIZE;
if (blockCount > 0 && initialCounter > MAX_COUNTER - (blockCount - 1)) {
throw new IllegalArgumentException("Counter would overflow");
}

byte[] output = new byte[input.length];
for (int offset = 0; offset < input.length; offset += BLOCK_SIZE) {
long counter = initialCounter + offset / BLOCK_SIZE;
byte[] keyStream = block(key, nonce, counter);
int bytesToProcess = Math.min(BLOCK_SIZE, input.length - offset);

for (int i = 0; i < bytesToProcess; i++) {
output[offset + i] = (byte) (input[offset + i] ^ keyStream[i]);
}
}
return output;
}

/**
* Decrypts bytes with ChaCha20. This is identical to encryption.
*
* @param input ciphertext bytes; must not be {@code null}
* @param key 32-byte key
* @param nonce 12-byte nonce
* @param initialCounter unsigned 32-bit initial block counter
* @return decrypted bytes
*/
public static byte[] decrypt(byte[] input, byte[] key, byte[] nonce, long initialCounter) {
return encrypt(input, key, nonce, initialCounter);
}

private static void validateInputs(byte[] input, byte[] key, byte[] nonce, long counter) {
Objects.requireNonNull(input, "input must not be null");
Objects.requireNonNull(key, "key must not be null");
Objects.requireNonNull(nonce, "nonce must not be null");

if (key.length != 32) {
throw new IllegalArgumentException("Key must be 32 bytes");
}
if (nonce.length != 12) {
throw new IllegalArgumentException("Nonce must be 12 bytes");
}
if (counter < 0 || counter > MAX_COUNTER) {
throw new IllegalArgumentException("Counter must be an unsigned 32-bit value");
}
}

private static byte[] block(byte[] key, byte[] nonce, long counter) {
int[] state = new int[16];
System.arraycopy(CONSTANTS, 0, state, 0, CONSTANTS.length);

for (int i = 0; i < 8; i++) {
state[4 + i] = littleEndianInt(key, i * 4);
}

state[12] = (int) counter;
state[13] = littleEndianInt(nonce, 0);
state[14] = littleEndianInt(nonce, 4);
state[15] = littleEndianInt(nonce, 8);

int[] workingState = state.clone();
for (int i = 0; i < 10; i++) {
quarterRound(workingState, 0, 4, 8, 12);
quarterRound(workingState, 1, 5, 9, 13);
quarterRound(workingState, 2, 6, 10, 14);
quarterRound(workingState, 3, 7, 11, 15);

quarterRound(workingState, 0, 5, 10, 15);
quarterRound(workingState, 1, 6, 11, 12);
quarterRound(workingState, 2, 7, 8, 13);
quarterRound(workingState, 3, 4, 9, 14);
}

byte[] output = new byte[BLOCK_SIZE];
for (int i = 0; i < workingState.length; i++) {
int word = workingState[i] + state[i];
output[i * 4] = (byte) word;
output[i * 4 + 1] = (byte) (word >>> 8);
output[i * 4 + 2] = (byte) (word >>> 16);
output[i * 4 + 3] = (byte) (word >>> 24);
}
return output;
}

static void quarterRound(int[] state, int a, int b, int c, int d) {
state[a] += state[b];
state[d] = Integer.rotateLeft(state[d] ^ state[a], 16);
state[c] += state[d];
state[b] = Integer.rotateLeft(state[b] ^ state[c], 12);
state[a] += state[b];
state[d] = Integer.rotateLeft(state[d] ^ state[a], 8);
state[c] += state[d];
state[b] = Integer.rotateLeft(state[b] ^ state[c], 7);
}

private static int littleEndianInt(byte[] bytes, int offset) {
return (bytes[offset] & 0xFF) | ((bytes[offset + 1] & 0xFF) << 8) | ((bytes[offset + 2] & 0xFF) << 16) | ((bytes[offset + 3] & 0xFF) << 24);
}
}
92 changes: 92 additions & 0 deletions src/test/java/com/thealgorithms/ciphers/ChaCha20Test.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.thealgorithms.ciphers;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.Test;

class ChaCha20Test {

@Test
void quarterRoundMatchesRfc8439Vector() {
int[] state = {0x11111111, 0x01020304, 0x9b8d6f43, 0x01234567};

ChaCha20.quarterRound(state, 0, 1, 2, 3);

assertArrayEquals(new int[] {0xea2a92f4, 0xcb1cf8ce, 0x4581472e, 0x5881c4bb}, state);
}

@Test
void encryptingZeroesMatchesRfc8439BlockVector() {
byte[] key = new byte[32];
for (int i = 0; i < key.length; i++) {
key[i] = (byte) i;
}

byte[] nonce = {0, 0, 0, 9, 0, 0, 0, 0x4a, 0, 0, 0, 0};
byte[] expected = {0x10, (byte) 0xf1, (byte) 0xe7, (byte) 0xe4, (byte) 0xd1, 0x3b, 0x59, 0x15, 0x50, 0x0f, (byte) 0xdd, 0x1f, (byte) 0xa3, 0x20, 0x71, (byte) 0xc4, (byte) 0xc7, (byte) 0xd1, (byte) 0xf4, (byte) 0xc7, 0x33, (byte) 0xc0, 0x68, 0x03, 0x04, 0x22, (byte) 0xaa, (byte) 0x9a,
(byte) 0xc3, (byte) 0xd4, 0x6c, 0x4e, (byte) 0xd2, (byte) 0x82, 0x64, 0x46, 0x07, (byte) 0x9f, (byte) 0xaa, 0x09, 0x14, (byte) 0xc2, (byte) 0xd7, 0x05, (byte) 0xd9, (byte) 0x8b, 0x02, (byte) 0xa2, (byte) 0xb5, 0x12, (byte) 0x9c, (byte) 0xd1, (byte) 0xde, 0x16, 0x4e, (byte) 0xb9,
(byte) 0xcb, (byte) 0xd0, (byte) 0x83, (byte) 0xe8, (byte) 0xa2, 0x50, 0x3c, 0x4e};

byte[] actual = ChaCha20.encrypt(new byte[64], key, nonce, 1);

assertArrayEquals(expected, actual);
}

@Test
void decryptRestoresInputAcrossMultipleBlocks() {
byte[] input = new byte[130];
for (int i = 0; i < input.length; i++) {
input[i] = (byte) (i * 3);
}

byte[] key = new byte[32];
byte[] nonce = new byte[12];

byte[] encrypted = ChaCha20.encrypt(input, key, nonce, 7);
byte[] decrypted = ChaCha20.decrypt(encrypted, key, nonce, 7);

assertArrayEquals(input, decrypted);
}

@Test
void handlesPartialBlocksAroundBlockBoundary() {
for (int length : new int[] {1, 63, 65}) {
byte[] input = new byte[length];
for (int i = 0; i < input.length; i++) {
input[i] = (byte) (i + 1);
}

byte[] key = new byte[32];
byte[] nonce = new byte[12];
byte[] encrypted = ChaCha20.encrypt(input, key, nonce, 0);

assertArrayEquals(input, ChaCha20.decrypt(encrypted, key, nonce, 0));
}
}

@Test
void emptyInputReturnsEmptyOutput() {
assertArrayEquals(new byte[0], ChaCha20.encrypt(new byte[0], new byte[32], new byte[12], 1));
}

@Test
void maximumCounterCanProcessOneBlock() {
byte[] input = new byte[64];

assertArrayEquals(input, ChaCha20.decrypt(ChaCha20.encrypt(input, new byte[32], new byte[12], 0xFFFF_FFFFL), new byte[32], new byte[12], 0xFFFF_FFFFL));
}

@Test
void counterOverflowIsRejected() {
assertThrows(IllegalArgumentException.class, () -> ChaCha20.encrypt(new byte[65], new byte[32], new byte[12], 0xFFFF_FFFFL));
}

@Test
void invalidKeyNonceAndCounterAreRejected() {
assertThrows(IllegalArgumentException.class, () -> ChaCha20.encrypt(new byte[1], new byte[31], new byte[12], 0));
assertThrows(IllegalArgumentException.class, () -> ChaCha20.encrypt(new byte[1], new byte[32], new byte[11], 0));
assertThrows(IllegalArgumentException.class, () -> ChaCha20.encrypt(new byte[1], new byte[32], new byte[12], -1));
assertThrows(IllegalArgumentException.class, () -> ChaCha20.encrypt(new byte[1], new byte[32], new byte[12], 0x1_0000_0000L));
}
}
Loading