Files
Java/src/main/java/com/thealgorithms/ciphers/PermutationCipher.java
Dheeraj Kumar c06dc292fe feat(ciphers): Add PermutationCipher implementation(enhance #6697) (#6700)
* 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>
2025-10-08 08:42:58 +02:00

195 lines
6.4 KiB
Java

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;
}
}