From bf6b795edd726a4499da879244b455887aa430cc Mon Sep 17 00:00:00 2001 From: hindAlsh Date: Thu, 12 Nov 2026 11:25:36 -0700 Subject: [PATCH 1/5] Add ChaCha20 stream cipher This implementation is intended for educational purposes and should not be used as a replacement for a vetted cryptographic library in production. --- .../com/thealgorithms/ciphers/ChaCha20.java | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 src/main/java/com/thealgorithms/ciphers/ChaCha20.java 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..2db51710326f --- /dev/null +++ b/src/main/java/com/thealgorithms/ciphers/ChaCha20.java @@ -0,0 +1,141 @@ +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); + } +} From 63e3f0238674a66d8f3f099df3cac8ab1bcc98ad Mon Sep 17 00:00:00 2001 From: hindAlsh Date: Thu, 12 Nov 2026 11:27:15 -0700 Subject: [PATCH 2/5] Test ChaCha20 implementation --- .../thealgorithms/ciphers/ChaCha20Test.java | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 src/test/java/com/thealgorithms/ciphers/ChaCha20Test.java 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..6900b3bc5192 --- /dev/null +++ b/src/test/java/com/thealgorithms/ciphers/ChaCha20Test.java @@ -0,0 +1,104 @@ +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 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) + ); + } +} From 5ad6dc59a357e5b6b3d49ab29bf18413f05f47cb Mon Sep 17 00:00:00 2001 From: hindAlsh Date: Thu, 12 Nov 2026 11:30:03 -0700 Subject: [PATCH 3/5] Add test for ChaCha20 handling partial blocks --- .../com/thealgorithms/ciphers/ChaCha20Test.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/test/java/com/thealgorithms/ciphers/ChaCha20Test.java b/src/test/java/com/thealgorithms/ciphers/ChaCha20Test.java index 6900b3bc5192..ffb252955314 100644 --- a/src/test/java/com/thealgorithms/ciphers/ChaCha20Test.java +++ b/src/test/java/com/thealgorithms/ciphers/ChaCha20Test.java @@ -58,6 +58,22 @@ void decryptRestoresInputAcrossMultipleBlocks() { 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)); From e0fe4e7c95363a7479b36c48cedb96a18261da3b Mon Sep 17 00:00:00 2001 From: hindAlsh Date: Thu, 12 Nov 2026 11:45:30 -0700 Subject: [PATCH 4/5] Refactor CONSTANTS and littleEndianInt method --- src/main/java/com/thealgorithms/ciphers/ChaCha20.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/thealgorithms/ciphers/ChaCha20.java b/src/main/java/com/thealgorithms/ciphers/ChaCha20.java index 2db51710326f..5f9906d406d4 100644 --- a/src/main/java/com/thealgorithms/ciphers/ChaCha20.java +++ b/src/main/java/com/thealgorithms/ciphers/ChaCha20.java @@ -14,9 +14,7 @@ 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 static final int[] CONSTANTS = {0x61707865, 0x3320646e, 0x79622d32, 0x6b206574}; private ChaCha20() { // Utility class @@ -133,9 +131,6 @@ static void quarterRound(int[] state, int a, int b, int c, int d) { } 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); + return (bytes[offset] & 0xFF) | ((bytes[offset + 1] & 0xFF) << 8) | ((bytes[offset + 2] & 0xFF) << 16) | ((bytes[offset + 3] & 0xFF) << 24); } } From fcd6ce180d7e7f730e778fed4363e93127b08e3f Mon Sep 17 00:00:00 2001 From: hindAlsh Date: Thu, 12 Nov 2026 11:46:08 -0700 Subject: [PATCH 5/5] Refactor ChaCha20Test for clarity and readability Refactored nonce and expected byte array initialization for clarity. Updated test methods to improve readability by reducing line breaks. --- .../thealgorithms/ciphers/ChaCha20Test.java | 72 ++++++------------- 1 file changed, 22 insertions(+), 50 deletions(-) diff --git a/src/test/java/com/thealgorithms/ciphers/ChaCha20Test.java b/src/test/java/com/thealgorithms/ciphers/ChaCha20Test.java index ffb252955314..61d7c17ec663 100644 --- a/src/test/java/com/thealgorithms/ciphers/ChaCha20Test.java +++ b/src/test/java/com/thealgorithms/ciphers/ChaCha20Test.java @@ -23,19 +23,10 @@ void encryptingZeroesMatchesRfc8439BlockVector() { 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[] 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); @@ -59,20 +50,20 @@ void decryptRestoresInputAcrossMultipleBlocks() { } @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); + 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)); } - - 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() { @@ -83,38 +74,19 @@ void emptyInputReturnsEmptyOutput() { 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) - ); + 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) - ); + 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) - ); + 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)); } }