package com.mattrixwv.cipherstream.polysubstitution; import java.util.StringJoiner; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mattrixwv.cipherstream.exceptions.InvalidCharacterException; import com.mattrixwv.cipherstream.exceptions.InvalidInputException; import com.mattrixwv.cipherstream.exceptions.InvalidKeywordException; /** * Represents the Polybius square cipher encryption and decryption. * The Polybius square cipher is a classical encryption method that uses a 5x5 grid * to encode and decode messages based on the positions of letters in the grid. */ public class PolybiusSquare{ private static final Logger logger = LoggerFactory.getLogger(PolybiusSquare.class); /** A class representing the location of a character in the grid */ protected class CharLocation{ /** The x location in the grid */ private int x; /** The y location in the grid */ private int y; /** * Constructs a CharLocation with the specified x and y coordinates. * * @param x the x-coordinate of the character's location * @param y the y-coordinate of the character's location */ public CharLocation(int x, int y){ this.x = x; this.y = y; } /** * Returns the x-coordinate of the character's location. * * @return the x-coordinate */ public int getX(){ return x; } /** * Returns the y-coordinate of the character's location. * * @return the y-coordinate */ public int getY(){ return y; } } //?Fields /** The message that needs to be encoded/decoded */ protected String inputString; /** The encoded/decoded message */ protected String outputString; /** The keyword used to create the grid */ protected String keyword; /** The grid used to encode/decode the message */ protected char[][] grid; /** The letter that will need to be replaced in the grid and any input string or keyword */ protected char replaced; /** The letter that replaces replaced in the input string or keyword */ protected char replacer; //?Settings /** Persist whitespace in the output string */ protected boolean preserveWhitespace; /** Persist symbols in the output string */ protected boolean preserveSymbols; /** * Validates the Replaced character. * * @param replaced the character to be validated * @throws InvalidCharacterException if the character is not a letter or is invalid * @return the validated character */ private char validateReplaced(char replaced) throws InvalidCharacterException{ logger.debug("Validating replaced character {}", replaced); if(!Character.isAlphabetic(replaced)){ throw new InvalidCharacterException("The replaced character must be a letter"); } logger.debug("Checking replacer"); if(replaced == replacer){ throw new InvalidCharacterException("The replaced letter cannot be the same as the replacing letter"); } replaced = Character.toUpperCase(replaced); logger.debug("Cleaned character {}", replaced); return replaced; } /** * Sets the replaced character. * * @param replaced the character to be replaced * @throws InvalidCharacterException if the character is not a letter or is invalid */ protected void setReplaced(char replaced) throws InvalidCharacterException{ logger.debug("Setting replaced"); this.replaced = validateReplaced(replaced); } /** * Validates the Replacer character. * * @param replacer the character to be validated * @throws InvalidCharacterException if the character is not a letter or is invalid * @return the validated character */ private char validateReplacer(char replacer) throws InvalidCharacterException{ logger.debug("Validating replacer character {}", replacer); if(!Character.isAlphabetic(replacer)){ throw new InvalidCharacterException("The replacer character must be a letter"); } logger.debug("Checking replaced"); if(replaced == replacer){ throw new InvalidCharacterException("The replacer letter cannot be the same as the replaced letter"); } replacer = Character.toUpperCase(replacer); logger.debug("Cleaned character {}", replacer); return replacer; } /** * Sets the replacer character. * * @param replacer the character the replaces replaced * @throws InvalidCharacterException if the character is not a letter or is invalid */ protected void setReplacer(char replacer) throws InvalidCharacterException{ logger.debug("Setting replacer"); this.replacer = validateReplacer(replacer); } /** * Creates the grid from the keyword. */ protected void createGrid(){ logger.debug("Creating grid from keyword"); for(int row = 0;row < 5;++row){ for(int col = 0;col < 5;++col){ char letter = keyword.charAt((5 * row) + col); grid[row][col] = letter; } } logger.debug("Created grid\n{}", getGrid()); } /** * Strips invalid characters from the string that needs encoding/decoding. * * @param inputString the input string to be cleaned * @throws InvalidCharacterException if an invalid character is found * @throws InvalidInputException if the input string is invalid */ protected void setInputStringEncode(String inputString) throws InvalidCharacterException, InvalidInputException{ if(inputString == null){ throw new InvalidInputException("Input cannot be null"); } logger.debug("Setting input string for encoding '{}'", inputString); //Make sure the string doesn't contain any numbers logger.debug("Checking for digits"); for(char ch : inputString.toCharArray()){ if(Character.isDigit(ch)){ throw new InvalidInputException("Inputs for encoding cannot contain numbers"); } } //Change to upper case logger.debug("Removing case"); inputString = inputString.toUpperCase(); //Remove any whitespace if selected if(!preserveWhitespace){ logger.debug("Removing whitespace"); inputString = inputString.replaceAll("\\s", ""); } //Remove any symbols if selected if(!preserveSymbols){ logger.debug("Removing symbols"); inputString = inputString.replaceAll("[^a-zA-Z\\s]", ""); } if(!preserveWhitespace && !preserveSymbols){ //Add whitespace after every character for the default look StringJoiner spacedString = new StringJoiner(" "); for(int cnt = 0;cnt < inputString.length();++cnt){ spacedString.add(Character.toString(inputString.charAt(cnt))); } inputString = spacedString.toString(); } //Replace any characters that need replaced logger.debug("Replacing {} with {}", replaced, replacer); inputString = inputString.replace(Character.toString(replaced), Character.toString(replacer)); //Save the string logger.debug("Cleaned input string '{}'", inputString); this.inputString = inputString; if(this.inputString.isBlank() || getPreparedInputStringEncode().isBlank()){ throw new InvalidInputException("Input must contain at least 1 letter"); } } /** * Strips invalid characters from the string that needs decoding. * * @param inputString the input string to be cleaned * @throws InvalidCharacterException if an invalid character is found * @throws InvalidInputException if the input string is invalid */ protected void setInputStringDecode(String inputString) throws InvalidCharacterException, InvalidInputException{ if(inputString == null){ throw new InvalidInputException("Input cannot be null"); } logger.debug("Setting input string for decoding '{}'", inputString); //Make sure the string contains an even number of digits and no letters logger.debug("Checking for letters"); int numberOfDigits = 0; for(int cnt = 0;cnt < inputString.length();++cnt){ char ch = inputString.charAt(cnt); if(Character.isDigit(ch)){ ++numberOfDigits; } else if(Character.isAlphabetic(ch)){ throw new InvalidInputException("Inputs for decoding cannot contains letters"); } } if((numberOfDigits % 2) != 0){ throw new InvalidInputException("There must be an even number of digits in an encoded string"); } //Remove any whitespace if selected if(!preserveWhitespace){ logger.debug("Removing whitespace"); inputString = inputString.replaceAll("\\s", ""); } //Remove any symbols if selected if(!preserveSymbols){ logger.debug("Removing symbols"); inputString = inputString.replaceAll("[^0-9\\s]", ""); } //Save the string logger.debug("Cleaned input string '{}'", inputString); this.inputString = inputString; if(this.inputString.isBlank() || getPreparedInputStringDecode().isBlank()){ throw new InvalidInputException("Input must contain at least 1 letter"); } } /** * Returns the input string ready for encoding. * * @return the prepared input string */ protected String getPreparedInputStringEncode(){ logger.debug("Preparing input string for encoding"); String cleanString = inputString.toUpperCase(); cleanString = cleanString.replaceAll("[^A-Z]", ""); logger.debug("Prepared string '{}'", cleanString); return cleanString; } /** * Returns the input string ready for decoding. * * @return the prepared input string */ protected String getPreparedInputStringDecode(){ logger.debug("Preparing input string for decoding"); String cleanString = inputString.replaceAll("\\D", ""); logger.debug("Prepared string '{}'", cleanString); return cleanString; } /** * Strips invalid characters from the keyword and creates the grid. * * @param keyword the keyword to be processed */ protected void setKeyword(String keyword){ if(keyword == null){ throw new InvalidKeywordException("Keyword cannot be null"); } logger.debug("Original keyword {}", keyword); //Change everything to uppercase logger.debug("Removing case"); keyword = keyword.toUpperCase(); //Remove everything except capital letters logger.debug("Removing all non-letter characters"); keyword = keyword.replaceAll("[^A-Z]", ""); //Add all letters in the alphabet to the key logger.debug("Appending entire alphabet"); keyword += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; //Replace all replaced characters logger.debug("Replacing {} with {}", replaced, replacer); keyword = keyword.replaceAll(Character.toString(replaced), Character.toString(replacer)); //Remove all duplicate characters StringBuilder uniqueKey = new StringBuilder(); keyword.chars().distinct().forEach(c -> uniqueKey.append((char)c)); this.keyword = uniqueKey.toString(); logger.debug("Cleaned keyword {}", this.keyword); //Create the grid from the sanitized keyword createGrid(); } /** * Returns the location of the given character in the grid. * * @param letter the character whose location is to be found * @return the location of the character in the grid * @throws InvalidInputException if the character is not found in the grid */ protected CharLocation findChar(char letter) throws InvalidInputException{ logger.debug("Finding {} in grid", letter); for(int row = 0;row < grid.length;++row){ for(int col = 0;col < grid[row].length;++col){ if(grid[row][col] == letter){ logger.debug("Found at {}, {}", row, col); return new CharLocation(row, col); } } } //If it was not found something went wrong throw new InvalidInputException("The character '" + letter + "' was not found in the grid"); } /** * Adds characters that aren't letters to the output during encoding. * * @param cleanString the cleaned string to be formatted */ protected void addCharactersToCleanStringEncode(String cleanString){ logger.debug("Formatting output string for encoding"); int outputCnt = 0; StringBuilder fullOutput = new StringBuilder(); for(char inputChar : inputString.toCharArray()){ logger.debug("Working character {}", inputChar); //Add both numbers of any letters to the output if(Character.isAlphabetic(inputChar)){ logger.debug("Adding encoded characters"); fullOutput.append(cleanString.charAt(outputCnt++)); fullOutput.append(cleanString.charAt(outputCnt++)); } //Add any other characters that appear to the output else{ logger.debug("Adding symbols"); fullOutput.append(inputChar); } } outputString = fullOutput.toString(); logger.debug("Formatted output '{}'", outputString); } /** * Adds characters that aren't letters to the output during decoding. * * @param cleanString the cleaned string to be formatted */ protected void addCharactersToCleanStringDecode(String cleanString){ logger.debug("Formatting output string for decoding"); int outputCnt = 0; StringBuilder fullOutput = new StringBuilder(); for(int inputCnt = 0;inputCnt < inputString.length();++inputCnt){ logger.debug("Working character {}", inputString.charAt(inputCnt)); //Add the letter to the output and skip the second number if(Character.isDigit(inputString.charAt(inputCnt))){ logger.debug("Adding decoded characters"); fullOutput.append(cleanString.charAt(outputCnt++)); ++inputCnt; } //Add any other characters that appear to the output else{ logger.debug("Adding symbols"); fullOutput.append(inputString.charAt(inputCnt)); } } outputString = fullOutput.toString(); logger.debug("Formatted output '{}'", outputString); } /** * Encodes the input string using the Polybius cipher and stores the result in the output string. * * @throws InvalidInputException if the input string is invalid */ protected void encode() throws InvalidInputException{ logger.debug("Encoding"); StringBuilder output = new StringBuilder(); String cleanString = getPreparedInputStringEncode(); for(int cnt = 0;cnt < cleanString.length();++cnt){ //Get the next character to be encoded char ch = cleanString.charAt(cnt); logger.debug("Current working character {}", ch); //Find the letter in the grid CharLocation location = findChar(ch); logger.debug("Location {}, {}", location.getX() + 1, location.getY() + 1); //Add the grid location to the output output.append(location.getX() + 1); output.append(location.getY() + 1); } //Add other characters to the output string addCharactersToCleanStringEncode(output.toString()); } /** * Decodes the input string using the Polybius cipher and stores the result in the output string. * * @throws InvalidInputException if the input string is invalid */ protected void decode(){ logger.debug("Decoding"); StringBuilder output = new StringBuilder(); String cleanString = getPreparedInputStringDecode(); for(int cnt = 0;cnt < cleanString.length();){ //Get the digits indicationg the location of the next character char firstDigit = cleanString.charAt(cnt++); char secondDigit = cleanString.charAt(cnt++); logger.debug("Digits to decode {} {}", firstDigit, secondDigit); //Get the next character char letter = grid[Integer.valueOf(Character.toString(firstDigit)) - 1][Integer.valueOf(Character.toString(secondDigit)) - 1]; logger.debug("Decoded letter {}", letter); //Add the new letter to the output output.append(letter); } //Add other characters to the output addCharactersToCleanStringDecode(output.toString()); } //?Constructors /** * Constructs a PolybiusSquare cipher instance with default settings. * * @throws InvalidCharacterException if default characters are invalid */ public PolybiusSquare() throws InvalidCharacterException{ grid = new char[5][5]; inputString = ""; outputString = ""; keyword = ""; this.replaced = validateReplaced('J'); this.replacer = validateReplacer('I'); preserveWhitespace = false; preserveSymbols = false; } /** * Constructs a PolybiusSquare cipher instance with specified settings. * * @param preserveWhitespace whether to preserve whitespace * @param preserveSymbols whether to preserve symbols * @throws InvalidCharacterException if default characters are invalid */ public PolybiusSquare(boolean preserveWhitespace, boolean preserveSymbols) throws InvalidCharacterException{ grid = new char[5][5]; inputString = ""; outputString = ""; keyword = ""; this.replaced = validateReplaced('J'); this.replacer = validateReplacer('I'); this.preserveWhitespace = preserveWhitespace; this.preserveSymbols = preserveSymbols; } /** * Constructs a PolybiusSquare cipher instance with specified settings and characters. * * @param preserveWhitespace whether to preserve whitespace * @param preserveSymbols whether to preserve symbols * @param replaced the character to be replaced * @param replacer the character to replace the replaced character * @throws InvalidCharacterException if any character is invalid */ public PolybiusSquare(boolean preserveWhitespace, boolean preserveSymbols, char replaced, char replacer) throws InvalidCharacterException{ grid = new char[5][5]; inputString = ""; outputString = ""; keyword = ""; this.replaced = validateReplaced(replaced); this.replacer = validateReplacer(replacer); this.preserveWhitespace = preserveWhitespace; this.preserveSymbols = preserveSymbols; } /** * Sets the keyword and input string and encodes the message. * * @param inputString the message to encode * @return the encoded message * @throws InvalidCharacterException if any character is invalid * @throws InvalidInputException if the input string is invalid */ public String encode(String inputString) throws InvalidCharacterException, InvalidInputException{ return encode("", inputString); } /** * Sets the keyword and input string and encodes the message. * * @param keyword the keyword for the cipher * @param inputString the message to encode * @return the encoded message * @throws InvalidCharacterException if any character is invalid * @throws InvalidInputException if the input string is invalid */ public String encode(String keyword, String inputString) throws InvalidCharacterException, InvalidInputException{ reset(); setKeyword(keyword); setInputStringEncode(inputString); encode(); return outputString; } /** * Sets the keyword and input string and decodes the message. * * @param inputString the encoded message to decode * @return the decoded message * @throws InvalidCharacterException if any character is invalid * @throws InvalidInputException if the input string is invalid */ public String decode(String inputString) throws InvalidCharacterException, InvalidInputException{ return decode("", inputString); } /** * Sets the keyword and input string and decodes the message. * * @param keyword the keyword for the cipher * @param inputString the encoded message to decode * @return the decoded message * @throws InvalidCharacterException if any character is invalid * @throws InvalidInputException if the input string is invalid */ public String decode(String keyword, String inputString) throws InvalidCharacterException, InvalidInputException{ reset(); setKeyword(keyword); setInputStringDecode(inputString); decode(); return outputString; } /** * Resets all variables to their default values. */ public void reset(){ logger.debug("Resetting fields"); grid = new char[5][5]; inputString = ""; outputString = ""; keyword = ""; } //?Getters /** * Returns the replaced character. * * @return the replaced character */ public char getReplaced(){ return replaced; } /** * Returns the replacer character. * * @return the replacer character */ public char getReplacer(){ return replacer; } /** * Returns the keyword used in the cipher. * * @return the keyword */ public String getKeyword(){ return keyword; } /** * Returns the input string that was set for encoding/decoding. * * @return the input string */ public String getInputString(){ return inputString; } /** * Returns the output string after encoding/decoding. * * @return the output string */ public String getOutputString(){ return outputString; } /** * Returns a string representation of the grid. * * @return the grid as a string */ public String getGrid(){ logger.debug("Creating string from grid"); StringJoiner gridString = new StringJoiner("\n"); for(char[] row : grid){ StringJoiner rowString = new StringJoiner(" ", "[", "]"); for(char col : row){ rowString.add(Character.toString(col)); } gridString.add(rowString.toString()); } return gridString.toString(); } }