Skip to content

Commit 32ea446

Browse files
committed
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.
1 parent 3eb521b commit 32ea446

File tree

2 files changed

+490
-0
lines changed

2 files changed

+490
-0
lines changed
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package com.thealgorithms.ciphers;
2+
3+
import java.util.Arrays;
4+
import java.util.HashSet;
5+
import java.util.Set;
6+
7+
/**
8+
* A Java implementation of Permutation Cipher.
9+
* It is a type of transposition cipher in which the plaintext is divided into blocks
10+
* and the characters within each block are rearranged according to a fixed permutation key.
11+
*
12+
* For example, with key {3, 1, 2} and plaintext "HELLO", the text is divided into blocks
13+
* of 3 characters: "HEL" and "LO" (with padding). The characters are then rearranged
14+
* according to the key positions.
15+
*
16+
* @author GitHub Copilot
17+
*/
18+
public class PermutationCipher {
19+
20+
private static final char PADDING_CHAR = 'X';
21+
22+
/**
23+
* Encrypts the given plaintext using the permutation cipher with the specified key.
24+
*
25+
* @param plaintext the text to encrypt
26+
* @param key the permutation key (array of integers representing positions)
27+
* @return the encrypted text
28+
* @throws IllegalArgumentException if the key is invalid
29+
*/
30+
public String encrypt(String plaintext, int[] key) {
31+
validateKey(key);
32+
33+
if (plaintext == null || plaintext.isEmpty()) {
34+
return plaintext;
35+
}
36+
37+
// Remove spaces and convert to uppercase for consistent processing
38+
String cleanText = plaintext.replaceAll("\\s+", "").toUpperCase();
39+
40+
// Pad the text to make it divisible by key length
41+
String paddedText = padText(cleanText, key.length);
42+
43+
StringBuilder encrypted = new StringBuilder();
44+
45+
// Process text in blocks of key length
46+
for (int i = 0; i < paddedText.length(); i += key.length) {
47+
String block = paddedText.substring(i, Math.min(i + key.length, paddedText.length()));
48+
encrypted.append(permuteBlock(block, key));
49+
}
50+
51+
return encrypted.toString();
52+
}
53+
54+
/**
55+
* Decrypts the given ciphertext using the permutation cipher with the specified key.
56+
*
57+
* @param ciphertext the text to decrypt
58+
* @param key the permutation key (array of integers representing positions)
59+
* @return the decrypted text
60+
* @throws IllegalArgumentException if the key is invalid
61+
*/
62+
public String decrypt(String ciphertext, int[] key) {
63+
validateKey(key);
64+
65+
if (ciphertext == null || ciphertext.isEmpty()) {
66+
return ciphertext;
67+
}
68+
69+
// Create the inverse permutation
70+
int[] inverseKey = createInverseKey(key);
71+
72+
StringBuilder decrypted = new StringBuilder();
73+
74+
// Process text in blocks of key length
75+
for (int i = 0; i < ciphertext.length(); i += key.length) {
76+
String block = ciphertext.substring(i, Math.min(i + key.length, ciphertext.length()));
77+
decrypted.append(permuteBlock(block, inverseKey));
78+
}
79+
80+
// Remove padding characters from the end
81+
return removePadding(decrypted.toString());
82+
}
83+
84+
/**
85+
* Validates that the permutation key is valid.
86+
* A valid key must contain all integers from 1 to n exactly once, where n is the key length.
87+
*
88+
* @param key the permutation key to validate
89+
* @throws IllegalArgumentException if the key is invalid
90+
*/
91+
private void validateKey(int[] key) {
92+
if (key == null || key.length == 0) {
93+
throw new IllegalArgumentException("Key cannot be null or empty");
94+
}
95+
96+
Set<Integer> keySet = new HashSet<>();
97+
for (int position : key) {
98+
if (position < 1 || position > key.length) {
99+
throw new IllegalArgumentException("Key must contain integers from 1 to " + key.length);
100+
}
101+
if (!keySet.add(position)) {
102+
throw new IllegalArgumentException("Key must contain each position exactly once");
103+
}
104+
}
105+
}
106+
107+
/**
108+
* Pads the text with padding characters to make its length divisible by the block size.
109+
*
110+
* @param text the text to pad
111+
* @param blockSize the size of each block
112+
* @return the padded text
113+
*/
114+
private String padText(String text, int blockSize) {
115+
int remainder = text.length() % blockSize;
116+
if (remainder == 0) {
117+
return text;
118+
}
119+
120+
int paddingNeeded = blockSize - remainder;
121+
StringBuilder padded = new StringBuilder(text);
122+
for (int i = 0; i < paddingNeeded; i++) {
123+
padded.append(PADDING_CHAR);
124+
}
125+
126+
return padded.toString();
127+
}
128+
129+
/**
130+
* Applies the permutation to a single block of text.
131+
*
132+
* @param block the block to permute
133+
* @param key the permutation key
134+
* @return the permuted block
135+
*/
136+
private String permuteBlock(String block, int[] key) {
137+
if (block.length() != key.length) {
138+
// Handle case where block is shorter than key (shouldn't happen with proper padding)
139+
block = padText(block, key.length);
140+
}
141+
142+
char[] result = new char[key.length];
143+
char[] blockChars = block.toCharArray();
144+
145+
for (int i = 0; i < key.length; i++) {
146+
// Key positions are 1-based, so subtract 1 for 0-based array indexing
147+
result[i] = blockChars[key[i] - 1];
148+
}
149+
150+
return new String(result);
151+
}
152+
153+
/**
154+
* Creates the inverse permutation key for decryption.
155+
*
156+
* @param key the original permutation key
157+
* @return the inverse key
158+
*/
159+
private int[] createInverseKey(int[] key) {
160+
int[] inverse = new int[key.length];
161+
162+
for (int i = 0; i < key.length; i++) {
163+
// The inverse key maps each position to where it should go
164+
inverse[key[i] - 1] = i + 1;
165+
}
166+
167+
return inverse;
168+
}
169+
170+
/**
171+
* Removes padding characters from the end of the decrypted text.
172+
*
173+
* @param text the text to remove padding from
174+
* @return the text without padding
175+
*/
176+
private String removePadding(String text) {
177+
if (text.isEmpty()) {
178+
return text;
179+
}
180+
181+
int i = text.length() - 1;
182+
while (i >= 0 && text.charAt(i) == PADDING_CHAR) {
183+
i--;
184+
}
185+
186+
return text.substring(0, i + 1);
187+
}
188+
189+
/**
190+
* Gets the padding character used by this cipher.
191+
*
192+
* @return the padding character
193+
*/
194+
public char getPaddingChar() {
195+
return PADDING_CHAR;
196+
}
197+
}

0 commit comments

Comments
 (0)