diff --git a/src/main/java/com/thealgorithms/ciphers/ChaCha20.java b/src/main/java/com/thealgorithms/ciphers/ChaCha20.java new file mode 100644 index 000000000000..5f9906d406d4 --- /dev/null +++ b/src/main/java/com/thealgorithms/ciphers/ChaCha20.java @@ -0,0 +1,136 @@ +package com.thealgorithms.ciphers; + +import java.util.Objects; + +/** + * Implements the ChaCha20 stream cipher described in RFC 8439. + * + *

This implementation is intended for educational purposes and should not + * be used as a replacement for a vetted cryptographic library in production. + * + * @see RFC 8439 + */ +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); + } +} diff --git a/src/test/java/com/thealgorithms/ciphers/ChaCha20Test.java b/src/test/java/com/thealgorithms/ciphers/ChaCha20Test.java new file mode 100644 index 000000000000..61d7c17ec663 --- /dev/null +++ b/src/test/java/com/thealgorithms/ciphers/ChaCha20Test.java @@ -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)); + } +}