diff --git a/src/main/java/com/thealgorithms/ciphers/OneTimePadCipher.java b/src/main/java/com/thealgorithms/ciphers/OneTimePadCipher.java new file mode 100644 index 000000000000..66d512c5553f --- /dev/null +++ b/src/main/java/com/thealgorithms/ciphers/OneTimePadCipher.java @@ -0,0 +1,78 @@ +package com.thealgorithms.ciphers; + +import java.security.SecureRandom; + +/** + * The {@code OneTimePadCipher} class provides a simple implementation of the + * One-Time Pad (OTP) cipher — a theoretically unbreakable symmetric encryption + * technique when used correctly. + *

+ * This cipher works by generating a truly random key of the same length as the + * plaintext, and then performing a bitwise XOR (exclusive OR) operation between + * the plaintext bytes and the key bytes. The resulting ciphertext can be + * decrypted by applying the same XOR operation again using the same key. + *

+ * Important: For the OTP cipher to be perfectly secure, the + * following conditions must be met: + *

+ *

+ * Example usage: + *

{@code
+ * String message = "HELLO";
+ * byte[] plaintext = message.getBytes(StandardCharsets.UTF_8);
+ * byte[] key = OneTimePadCipher.generateKey(plaintext.length);
+ *
+ * // Encrypt
+ * byte[] ciphertext = OneTimePadCipher.applyOTP(plaintext, key);
+ *
+ * // Decrypt (same method)
+ * byte[] decrypted = OneTimePadCipher.applyOTP(ciphertext, key);
+ * System.out.println(new String(decrypted, StandardCharsets.UTF_8)); // "HELLO"
+ * }
+ * + * @see One-Time Pad Cipher - Wikipedia + * @author newtownboy + */ +public final class OneTimePadCipher { + private static final SecureRandom random = new SecureRandom(); + + private OneTimePadCipher() { + } + + /** + * Generate a truly random key of the same length as the plaintext. + * + * @param length length of the key + * @return random key as byte array + */ + public static byte[] generateKey(int length) { + byte[] key = new byte[length]; + random.nextBytes(key); + return key; + } + + /** + * Encrypt or decrypt using XOR operation. + * Since XOR is symmetric, the same method works for both. + * + * @param input plaintext or ciphertext + * @param key key of the same length as input + * @return result after XOR + */ + public static byte[] applyOTP(byte[] input, byte[] key) { + if (input.length != key.length) { + throw new IllegalArgumentException("Input and key must be the same length"); + } + + byte[] output = new byte[input.length]; + for (int i = 0; i < input.length; i++) { + output[i] = (byte) (input[i] ^ key[i]); + } + return output; + } +} diff --git a/src/test/java/com/thealgorithms/ciphers/OneTimePadCipherTest.java b/src/test/java/com/thealgorithms/ciphers/OneTimePadCipherTest.java new file mode 100644 index 000000000000..1d797b8ceb6d --- /dev/null +++ b/src/test/java/com/thealgorithms/ciphers/OneTimePadCipherTest.java @@ -0,0 +1,95 @@ +package com.thealgorithms.ciphers; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.*; + +class OneTimePadCipherTest { + @Test + void testGenerateKeyLength() { + int length = 16; + byte[] key = OneTimePadCipher.generateKey(length); + assertNotNull(key, "Key should not be null"); + assertEquals(length, key.length, "Key length should match requested length"); + } + + @Test + void testEncryptDecrypt() { + String plaintext = "Hello, OneTimePad!"; + byte[] input = plaintext.getBytes(); + + byte[] key = OneTimePadCipher.generateKey(input.length); + + byte[] ciphertext = OneTimePadCipher.applyOTP(input, key); + assertNotNull(ciphertext); + assertNotEquals(plaintext, new String(ciphertext), "Ciphertext should differ from plaintext"); + + byte[] decrypted = OneTimePadCipher.applyOTP(ciphertext, key); + assertEquals(plaintext, new String(decrypted), "Decrypted text should match original plaintext"); + } + + @Test + void testInvalidKeyLength() { + byte[] input = "Hello, OneTimePad!".getBytes(); + byte[] wrongKey = OneTimePadCipher.generateKey(input.length + 1); + + assertThrows(IllegalArgumentException.class, () -> { + OneTimePadCipher.applyOTP(input, wrongKey); + }); + } + + @Test + void testEmptyInputAndKey() { + byte[] input = new byte[0]; + byte[] key = new byte[0]; + byte[] result = OneTimePadCipher.applyOTP(input, key); + assertEquals(0, result.length, "Empty input should return empty output"); + } + + @Test + void testDeterministicXorProperty() { + byte[] input = "XORTest".getBytes(); + byte[] key = OneTimePadCipher.generateKey(input.length); + + byte[] once = OneTimePadCipher.applyOTP(input, key); + byte[] twice = OneTimePadCipher.applyOTP(once, key); + + assertArrayEquals(input, twice, "Applying OTP twice with same key should return original input"); + } + + @Test + void testRandomKeyUniqueness() { + byte[] key1 = OneTimePadCipher.generateKey(32); + byte[] key2 = OneTimePadCipher.generateKey(32); + + assertFalse(Arrays.equals(key1, key2), "Two generated keys should not be identical"); + } + + @Test + void testUnicodeCharacters() { + String plaintext = "Hello, OneTimePad!"; + byte[] input = plaintext.getBytes(); + + byte[] key = OneTimePadCipher.generateKey(input.length); + byte[] ciphertext = OneTimePadCipher.applyOTP(input, key); + byte[] decrypted = OneTimePadCipher.applyOTP(ciphertext, key); + + assertEquals(plaintext, new String(decrypted), "Decrypted Unicode text should match original"); + } + + @Test + void testNullInputs() { + byte[] validKey = OneTimePadCipher.generateKey(4); + byte[] validInput = "Test".getBytes(); + + assertThrows(NullPointerException.class, () -> OneTimePadCipher.applyOTP(null, validKey)); + assertThrows(NullPointerException.class, () -> OneTimePadCipher.applyOTP(validInput, null)); + } + + @Test + void testNegativeKeyLength() { + assertThrows(NegativeArraySizeException.class, () -> OneTimePadCipher.generateKey(-5)); + } +} \ No newline at end of file