mirror of
https://github.com/TheAlgorithms/Java.git
synced 2025-12-19 07:00:35 +08:00
* feat(ciphers): Add PermutationCipher implementation with comprehensive tests - Implement PermutationCipher class for transposition encryption/decryption - Add encrypt() and decrypt() methods with permutation key support - Include robust key validation (1-based positions, no duplicates) - Implement automatic padding for incomplete blocks using 'X' character - Add comprehensive error handling with descriptive exceptions - Create 20+ JUnit test cases covering encryption, decryption, edge cases - Support various key sizes and text processing (spaces removal, case handling) - Include detailed JavaDoc documentation with algorithm explanation Algorithm Details: - Divides plaintext into blocks based on key length - Rearranges characters within each block according to permutation positions - Supports round-trip encryption/decryption with inverse permutation - Handles edge cases: empty strings, single character keys, padding Tests include: basic functionality, different key sizes, error validation, real-world examples, and edge case handling. * Run PermutationCipherTest using Maven * refactor(PermutationCipher): clean up code by removing unnecessary whitespace and comments * fix(tests): remove unnecessary whitespace in test assertion for encryption * fix(tests): correct indentation in assertion for encryption verification --------- Co-authored-by: a <alexanderklmn@gmail.com>
This commit is contained in:
194
src/main/java/com/thealgorithms/ciphers/PermutationCipher.java
Normal file
194
src/main/java/com/thealgorithms/ciphers/PermutationCipher.java
Normal file
@@ -0,0 +1,194 @@
|
||||
package com.thealgorithms.ciphers;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A Java implementation of Permutation Cipher.
|
||||
* It is a type of transposition cipher in which the plaintext is divided into blocks
|
||||
* and the characters within each block are rearranged according to a fixed permutation key.
|
||||
*
|
||||
* For example, with key {3, 1, 2} and plaintext "HELLO", the text is divided into blocks
|
||||
* of 3 characters: "HEL" and "LO" (with padding). The characters are then rearranged
|
||||
* according to the key positions.
|
||||
*
|
||||
* @author GitHub Copilot
|
||||
*/
|
||||
public class PermutationCipher {
|
||||
|
||||
private static final char PADDING_CHAR = 'X';
|
||||
|
||||
/**
|
||||
* Encrypts the given plaintext using the permutation cipher with the specified key.
|
||||
*
|
||||
* @param plaintext the text to encrypt
|
||||
* @param key the permutation key (array of integers representing positions)
|
||||
* @return the encrypted text
|
||||
* @throws IllegalArgumentException if the key is invalid
|
||||
*/
|
||||
public String encrypt(String plaintext, int[] key) {
|
||||
validateKey(key);
|
||||
|
||||
if (plaintext == null || plaintext.isEmpty()) {
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
// Remove spaces and convert to uppercase for consistent processing
|
||||
String cleanText = plaintext.replaceAll("\\s+", "").toUpperCase();
|
||||
|
||||
// Pad the text to make it divisible by key length
|
||||
String paddedText = padText(cleanText, key.length);
|
||||
|
||||
StringBuilder encrypted = new StringBuilder();
|
||||
|
||||
// Process text in blocks of key length
|
||||
for (int i = 0; i < paddedText.length(); i += key.length) {
|
||||
String block = paddedText.substring(i, Math.min(i + key.length, paddedText.length()));
|
||||
encrypted.append(permuteBlock(block, key));
|
||||
}
|
||||
|
||||
return encrypted.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts the given ciphertext using the permutation cipher with the specified key.
|
||||
*
|
||||
* @param ciphertext the text to decrypt
|
||||
* @param key the permutation key (array of integers representing positions)
|
||||
* @return the decrypted text
|
||||
* @throws IllegalArgumentException if the key is invalid
|
||||
*/
|
||||
public String decrypt(String ciphertext, int[] key) {
|
||||
validateKey(key);
|
||||
|
||||
if (ciphertext == null || ciphertext.isEmpty()) {
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
// Create the inverse permutation
|
||||
int[] inverseKey = createInverseKey(key);
|
||||
|
||||
StringBuilder decrypted = new StringBuilder();
|
||||
|
||||
// Process text in blocks of key length
|
||||
for (int i = 0; i < ciphertext.length(); i += key.length) {
|
||||
String block = ciphertext.substring(i, Math.min(i + key.length, ciphertext.length()));
|
||||
decrypted.append(permuteBlock(block, inverseKey));
|
||||
}
|
||||
|
||||
// Remove padding characters from the end
|
||||
return removePadding(decrypted.toString());
|
||||
}
|
||||
/**
|
||||
* Validates that the permutation key is valid.
|
||||
* A valid key must contain all integers from 1 to n exactly once, where n is the key length.
|
||||
*
|
||||
* @param key the permutation key to validate
|
||||
* @throws IllegalArgumentException if the key is invalid
|
||||
*/
|
||||
private void validateKey(int[] key) {
|
||||
if (key == null || key.length == 0) {
|
||||
throw new IllegalArgumentException("Key cannot be null or empty");
|
||||
}
|
||||
|
||||
Set<Integer> keySet = new HashSet<>();
|
||||
for (int position : key) {
|
||||
if (position < 1 || position > key.length) {
|
||||
throw new IllegalArgumentException("Key must contain integers from 1 to " + key.length);
|
||||
}
|
||||
if (!keySet.add(position)) {
|
||||
throw new IllegalArgumentException("Key must contain each position exactly once");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pads the text with padding characters to make its length divisible by the block size.
|
||||
*
|
||||
* @param text the text to pad
|
||||
* @param blockSize the size of each block
|
||||
* @return the padded text
|
||||
*/
|
||||
private String padText(String text, int blockSize) {
|
||||
int remainder = text.length() % blockSize;
|
||||
if (remainder == 0) {
|
||||
return text;
|
||||
}
|
||||
|
||||
int paddingNeeded = blockSize - remainder;
|
||||
StringBuilder padded = new StringBuilder(text);
|
||||
for (int i = 0; i < paddingNeeded; i++) {
|
||||
padded.append(PADDING_CHAR);
|
||||
}
|
||||
|
||||
return padded.toString();
|
||||
}
|
||||
/**
|
||||
* Applies the permutation to a single block of text.
|
||||
*
|
||||
* @param block the block to permute
|
||||
* @param key the permutation key
|
||||
* @return the permuted block
|
||||
*/
|
||||
private String permuteBlock(String block, int[] key) {
|
||||
if (block.length() != key.length) {
|
||||
// Handle case where block is shorter than key (shouldn't happen with proper padding)
|
||||
block = padText(block, key.length);
|
||||
}
|
||||
|
||||
char[] result = new char[key.length];
|
||||
char[] blockChars = block.toCharArray();
|
||||
|
||||
for (int i = 0; i < key.length; i++) {
|
||||
// Key positions are 1-based, so subtract 1 for 0-based array indexing
|
||||
result[i] = blockChars[key[i] - 1];
|
||||
}
|
||||
|
||||
return new String(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the inverse permutation key for decryption.
|
||||
*
|
||||
* @param key the original permutation key
|
||||
* @return the inverse key
|
||||
*/
|
||||
private int[] createInverseKey(int[] key) {
|
||||
int[] inverse = new int[key.length];
|
||||
|
||||
for (int i = 0; i < key.length; i++) {
|
||||
// The inverse key maps each position to where it should go
|
||||
inverse[key[i] - 1] = i + 1;
|
||||
}
|
||||
|
||||
return inverse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes padding characters from the end of the decrypted text.
|
||||
*
|
||||
* @param text the text to remove padding from
|
||||
* @return the text without padding
|
||||
*/
|
||||
private String removePadding(String text) {
|
||||
if (text.isEmpty()) {
|
||||
return text;
|
||||
}
|
||||
|
||||
int i = text.length() - 1;
|
||||
while (i >= 0 && text.charAt(i) == PADDING_CHAR) {
|
||||
i--;
|
||||
}
|
||||
|
||||
return text.substring(0, i + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the padding character used by this cipher.
|
||||
*
|
||||
* @return the padding character
|
||||
*/
|
||||
public char getPaddingChar() {
|
||||
return PADDING_CHAR;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
package com.thealgorithms.ciphers;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class PermutationCipherTest {
|
||||
|
||||
private final PermutationCipher cipher = new PermutationCipher();
|
||||
|
||||
@Test
|
||||
void testBasicEncryption() {
|
||||
// given
|
||||
String plaintext = "HELLO";
|
||||
int[] key = {3, 1, 2}; // Move 3rd position to 1st, 1st to 2nd, 2nd to 3rd
|
||||
|
||||
// when
|
||||
String encrypted = cipher.encrypt(plaintext, key);
|
||||
|
||||
// then
|
||||
// "HELLO" becomes "HEL" + "LOX" (padded)
|
||||
// "HEL" with key {3,1,2} becomes "LHE" (L=3rd, H=1st, E=2nd)
|
||||
// "LOX" with key {3,1,2} becomes "XLO" (X=3rd, L=1st, O=2nd)
|
||||
assertEquals("LHEXLO", encrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBasicDecryption() {
|
||||
// given
|
||||
String ciphertext = "LHEXLO";
|
||||
int[] key = {3, 1, 2};
|
||||
|
||||
// when
|
||||
String decrypted = cipher.decrypt(ciphertext, key);
|
||||
|
||||
// then
|
||||
assertEquals("HELLO", decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEncryptDecryptRoundTrip() {
|
||||
// given
|
||||
String plaintext = "THIS IS A TEST MESSAGE";
|
||||
int[] key = {4, 2, 1, 3};
|
||||
|
||||
// when
|
||||
String encrypted = cipher.encrypt(plaintext, key);
|
||||
String decrypted = cipher.decrypt(encrypted, key);
|
||||
|
||||
// then
|
||||
assertEquals("THISISATESTMESSAGE", decrypted); // Spaces are removed during encryption
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSingleCharacterKey() {
|
||||
// given
|
||||
String plaintext = "ABCDEF";
|
||||
int[] key = {1}; // Identity permutation
|
||||
|
||||
// when
|
||||
String encrypted = cipher.encrypt(plaintext, key);
|
||||
String decrypted = cipher.decrypt(encrypted, key);
|
||||
|
||||
// then
|
||||
assertEquals("ABCDEF", encrypted); // Should remain unchanged
|
||||
assertEquals("ABCDEF", decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLargerKey() {
|
||||
// given
|
||||
String plaintext = "PERMUTATION";
|
||||
int[] key = {5, 3, 1, 4, 2}; // 5-character permutation
|
||||
|
||||
// when
|
||||
String encrypted = cipher.encrypt(plaintext, key);
|
||||
String decrypted = cipher.decrypt(encrypted, key);
|
||||
|
||||
// then
|
||||
assertEquals("PERMUTATION", decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExactBlockSize() {
|
||||
// given
|
||||
String plaintext = "ABCDEF"; // Length 6, divisible by key length 3
|
||||
int[] key = {2, 3, 1};
|
||||
|
||||
// when
|
||||
String encrypted = cipher.encrypt(plaintext, key);
|
||||
String decrypted = cipher.decrypt(encrypted, key);
|
||||
|
||||
// then
|
||||
assertEquals("ABCDEF", decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmptyString() {
|
||||
// given
|
||||
String plaintext = "";
|
||||
int[] key = {2, 1, 3};
|
||||
|
||||
// when
|
||||
String encrypted = cipher.encrypt(plaintext, key);
|
||||
String decrypted = cipher.decrypt(encrypted, key);
|
||||
|
||||
// then
|
||||
assertEquals("", encrypted);
|
||||
assertEquals("", decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNullString() {
|
||||
// given
|
||||
String plaintext = null;
|
||||
int[] key = {2, 1, 3};
|
||||
|
||||
// when
|
||||
String encrypted = cipher.encrypt(plaintext, key);
|
||||
String decrypted = cipher.decrypt(encrypted, key);
|
||||
|
||||
// then
|
||||
assertEquals(null, encrypted);
|
||||
assertEquals(null, decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStringWithSpaces() {
|
||||
// given
|
||||
String plaintext = "A B C D E F";
|
||||
int[] key = {2, 1};
|
||||
|
||||
// when
|
||||
String encrypted = cipher.encrypt(plaintext, key);
|
||||
String decrypted = cipher.decrypt(encrypted, key);
|
||||
|
||||
// then
|
||||
assertEquals("ABCDEF", decrypted); // Spaces should be removed
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLowercaseConversion() {
|
||||
// given
|
||||
String plaintext = "hello world";
|
||||
int[] key = {3, 1, 2};
|
||||
|
||||
// when
|
||||
String encrypted = cipher.encrypt(plaintext, key);
|
||||
String decrypted = cipher.decrypt(encrypted, key);
|
||||
|
||||
// then
|
||||
assertEquals("HELLOWORLD", decrypted); // Should be converted to uppercase
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInvalidKeyNull() {
|
||||
// given
|
||||
String plaintext = "HELLO";
|
||||
int[] key = null;
|
||||
|
||||
// when & then
|
||||
assertThrows(IllegalArgumentException.class, () -> cipher.encrypt(plaintext, key));
|
||||
assertThrows(IllegalArgumentException.class, () -> cipher.decrypt(plaintext, key));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInvalidKeyEmpty() {
|
||||
// given
|
||||
String plaintext = "HELLO";
|
||||
int[] key = {};
|
||||
|
||||
// when & then
|
||||
assertThrows(IllegalArgumentException.class, () -> cipher.encrypt(plaintext, key));
|
||||
assertThrows(IllegalArgumentException.class, () -> cipher.decrypt(plaintext, key));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInvalidKeyOutOfRange() {
|
||||
// given
|
||||
String plaintext = "HELLO";
|
||||
int[] key = {1, 2, 4}; // 4 is out of range for key length 3
|
||||
|
||||
// when & then
|
||||
assertThrows(IllegalArgumentException.class, () -> cipher.encrypt(plaintext, key));
|
||||
assertThrows(IllegalArgumentException.class, () -> cipher.decrypt(plaintext, key));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInvalidKeyZero() {
|
||||
// given
|
||||
String plaintext = "HELLO";
|
||||
int[] key = {0, 1, 2}; // 0 is invalid (should be 1-based)
|
||||
|
||||
// when & then
|
||||
assertThrows(IllegalArgumentException.class, () -> cipher.encrypt(plaintext, key));
|
||||
assertThrows(IllegalArgumentException.class, () -> cipher.decrypt(plaintext, key));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInvalidKeyDuplicate() {
|
||||
// given
|
||||
String plaintext = "HELLO";
|
||||
int[] key = {1, 2, 2}; // Duplicate position
|
||||
|
||||
// when & then
|
||||
assertThrows(IllegalArgumentException.class, () -> cipher.encrypt(plaintext, key));
|
||||
assertThrows(IllegalArgumentException.class, () -> cipher.decrypt(plaintext, key));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInvalidKeyMissingPosition() {
|
||||
// given
|
||||
String plaintext = "HELLO";
|
||||
int[] key = {1, 3}; // Missing position 2
|
||||
|
||||
// when & then
|
||||
assertThrows(IllegalArgumentException.class, () -> cipher.encrypt(plaintext, key));
|
||||
assertThrows(IllegalArgumentException.class, () -> cipher.decrypt(plaintext, key));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReverseKey() {
|
||||
// given
|
||||
String plaintext = "ABCD";
|
||||
int[] key = {4, 3, 2, 1}; // Reverse order
|
||||
|
||||
// when
|
||||
String encrypted = cipher.encrypt(plaintext, key);
|
||||
String decrypted = cipher.decrypt(encrypted, key);
|
||||
|
||||
// then
|
||||
assertEquals("DCBA", encrypted); // Should be reversed
|
||||
assertEquals("ABCD", decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSpecificExampleFromDescription() {
|
||||
// given
|
||||
String plaintext = "HELLO";
|
||||
int[] key = {3, 1, 2};
|
||||
|
||||
// when
|
||||
String encrypted = cipher.encrypt(plaintext, key);
|
||||
|
||||
// then
|
||||
// Block 1: "HEL" -> positions {3,1,2} -> "LHE"
|
||||
// Block 2: "LOX" -> positions {3,1,2} -> "XLO"
|
||||
assertEquals("LHEXLO", encrypted);
|
||||
// Verify decryption
|
||||
String decrypted = cipher.decrypt(encrypted, key);
|
||||
assertEquals("HELLO", decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPaddingCharacterGetter() {
|
||||
// when
|
||||
char paddingChar = cipher.getPaddingChar();
|
||||
|
||||
// then
|
||||
assertEquals('X', paddingChar);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLongText() {
|
||||
// given
|
||||
String plaintext = "THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG";
|
||||
int[] key = {4, 1, 3, 2};
|
||||
|
||||
// when
|
||||
String encrypted = cipher.encrypt(plaintext, key);
|
||||
String decrypted = cipher.decrypt(encrypted, key);
|
||||
|
||||
// then
|
||||
assertEquals("THEQUICKBROWNFOXJUMPSOVERTHELAZYDOG", decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIdentityPermutation() {
|
||||
// given
|
||||
String plaintext = "IDENTITY";
|
||||
int[] key = {1, 2, 3, 4}; // Identity permutation
|
||||
|
||||
// when
|
||||
String encrypted = cipher.encrypt(plaintext, key);
|
||||
String decrypted = cipher.decrypt(encrypted, key);
|
||||
|
||||
// then
|
||||
assertEquals("IDENTITY", encrypted); // Should remain unchanged
|
||||
assertEquals("IDENTITY", decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmptyStringRemovePadding() {
|
||||
// given - Test to cover line 178 (empty string case in removePadding)
|
||||
String ciphertext = "";
|
||||
int[] key = {2, 1, 3};
|
||||
|
||||
// when
|
||||
String decrypted = cipher.decrypt(ciphertext, key);
|
||||
|
||||
// then
|
||||
assertEquals("", decrypted); // Should return empty string directly
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBlockShorterThanKey() {
|
||||
// given - Test to cover line 139 (block length != key length case)
|
||||
// This is a defensive case where permuteBlock might receive a block shorter than key
|
||||
// We can test this by manually creating a scenario with malformed ciphertext
|
||||
String malformedCiphertext = "AB"; // Length 2, but key length is 3
|
||||
int[] key = {3, 1, 2}; // Key length is 3
|
||||
|
||||
// when - This should trigger the padding logic in permuteBlock during decryption
|
||||
String decrypted = cipher.decrypt(malformedCiphertext, key);
|
||||
|
||||
// then - The method should handle the short block gracefully
|
||||
// "AB" gets padded to "ABX", then permuted with inverse key {2,3,1}
|
||||
// inverse key {2,3,1} means: pos 2→1st, pos 3→2nd, pos 1→3rd = "BXA"
|
||||
// Padding removal only removes trailing X's, so "BXA" remains as is
|
||||
assertEquals("BXA", decrypted);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user