package com.mattrixwv.cipherstream.monosubstitution; import java.util.ArrayList; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mattrixwv.cipherstream.exceptions.InvalidInputException; import com.mattrixwv.cipherstream.exceptions.InvalidKeywordException; /** * A class for encoding and decoding strings using the Vigenère cipher. * This cipher uses a keyword to determine the shift for each character in the input string. * *

* The Vigenère cipher applies a series of Caesar ciphers based on the letters of a keyword. * The keyword determines the shift for each letter in the input string. *

*/ public class Vigenere{ private static final Logger logger = LoggerFactory.getLogger(Vigenere.class); //Fields /** This is the string that needs encoded/decoded */ protected String inputString; /** This is the string that is output after encoding/decoding */ protected String outputString; /** This is the keyword that is resposible for determining the offsets that you change each character by */ protected String keyword; /** Holds the offsets coputed from each character in the keyword */ protected ArrayList offset; //Settings /** Persist capitals in the output string */ protected boolean preserveCapitals; /** Persist whitespace in the output string */ protected boolean preserveWhitespace; /** Persist symbols in the output string */ protected boolean preserveSymbols; /** * Calculates the offsets for the Vigenère cipher from the keyword. */ protected void setOffset(){ logger.debug("Setting offset array from keyword"); //Reserve the correct size to increase speed later offset.ensureCapacity(keyword.length()); //Loop through every letter in keyword and get the offset from A for(int cnt = 0;cnt < keyword.length();++cnt){ char letter = keyword.charAt(cnt); offset.add((letter - 'A') % 26); } logger.debug("Offset {}", offset); } /** * Sets the input string and applies transformation settings. * * @param inputString the string to be encoded/decoded * @throws InvalidInputException if the input string is null or blank */ protected void setInputString(String inputString) throws InvalidInputException{ if(inputString == null){ throw new InvalidInputException("Input cannot be null"); } logger.debug("Original input string '{}'", inputString); if(!preserveCapitals){ logger.debug("Removing case"); inputString = inputString.toUpperCase(); } if(!preserveWhitespace){ logger.debug("Removing whitespace"); inputString = inputString.replaceAll("\\s", ""); } if(!preserveSymbols){ logger.debug("Removing symbols"); inputString = inputString.replaceAll("[^a-zA-Z\\s]", ""); } logger.debug("Cleaned input string '{}'", inputString); this.inputString = inputString; if(this.inputString.isBlank()){ throw new InvalidInputException("Input must contain at least 1 letter"); } } /** * Sets the keyword for the Vigenère cipher and computes the offsets. * * @param keyword the keyword to be used for encoding/decoding * @throws InvalidKeywordException if the keyword is null, too short, or contains invalid characters */ protected void setKeyword(String keyword) throws InvalidKeywordException{ if(keyword == null){ throw new InvalidKeywordException("Keyword cannot be null"); } logger.debug("Original keyword '{}'", keyword); //Convert all letters to uppercase logger.debug("Removing case"); keyword = keyword.toUpperCase(); //Remove all characters except capital letters logger.debug("Removing all non-letter characters"); keyword = keyword.replaceAll("[^A-Z]", ""); //Save the string logger.debug("Clean keyword '{}'", keyword); this.keyword = keyword; //Make sure offset is empty before adding to it offset.clear(); setOffset(); //If after all the eliminating of unusable characters the keyword is empty throw an exception if(this.keyword.length() < 2){ throw new InvalidKeywordException("Keyword must contain at least 2 letters"); } } /** * Encodes the input string using the Vigenère cipher and stores the result. */ protected void encode(){ logger.debug("Encoding"); StringBuilder output = new StringBuilder(); //Step through every character in the inputString and advance it the correct amount, according to offset int offsetCnt = 0; for(int inputCnt = 0;inputCnt < inputString.length();++inputCnt){ char letter = inputString.charAt(inputCnt); logger.debug("Working character {}", letter); if(Character.isUpperCase(letter)){ logger.debug("Encoding uppercase"); letter += offset.get((offsetCnt++) % offset.size()); //Make sure the character is still a letter, if not, wrap around if(letter > 'Z'){ logger.debug("Wrapping around to A"); letter -= 26; } } else if(Character.isLowerCase(letter)){ logger.debug("Encoding lowercase"); letter += offset.get((offsetCnt++) % offset.size()); //Make sure the character is still a letter, if not, wrap around if(letter > 'z'){ logger.debug("Wrapping around to a"); letter -= 26; } } logger.debug("Encoded character {}", letter); output.append(letter); } //Save output outputString = output.toString(); logger.debug("Encoded message '{}'", outputString); } /** * Decodes the input string using the Vigenère cipher and stores the result. */ protected void decode(){ logger.debug("Decoding"); StringBuilder output = new StringBuilder(); //Step through every character in the inputString and advance it the correct amount, according to offset int offsetCnt = 0; for(int letterCnt = 0;letterCnt < inputString.length();++letterCnt){ char letter = inputString.charAt(letterCnt); logger.debug("Working character {}", letter); if(Character.isUpperCase(letter)){ logger.debug("Decoding uppercase"); letter -= offset.get((offsetCnt++) % offset.size()); if(letter < 'A'){ logger.debug("Wrapping around to Z"); letter += 26; } } else if(Character.isLowerCase(letter)){ logger.debug("Decoding lowercase"); letter -= offset.get((offsetCnt++) % offset.size()); if(letter < 'a'){ logger.debug("Wrapping around to z"); letter += 26; } } //Add letter to output logger.debug("Decoded character {}", letter); output.append(letter); } //Save output outputString = output.toString(); logger.debug("Decoded message '{}'", outputString); } //?Constructor /** * Constructs a new {@code Vigenere} instance with default settings. */ public Vigenere(){ offset = new ArrayList<>(); inputString = ""; outputString = ""; keyword = ""; offset.clear(); preserveCapitals = false; preserveWhitespace = false; preserveSymbols = false; } /** * Constructs a new {@code Vigenere} instance with specified settings. * * @param preserveCapitals whether to preserve capitalization in the output * @param preserveWhitespace whether to preserve whitespace in the output * @param preserveSymbols whether to preserve symbols in the output */ public Vigenere(boolean preserveCapitals, boolean preserveWhitespace, boolean preserveSymbols){ offset = new ArrayList<>(); inputString = ""; outputString = ""; keyword = ""; offset.clear(); this.preserveCapitals = preserveCapitals; this.preserveWhitespace = preserveWhitespace; this.preserveSymbols = preserveSymbols; } /** * Encodes the input string using the provided keyword. * * @param keyword the keyword to use for encoding * @param inputString the string to be encoded * @return the encoded string * @throws InvalidKeywordException if the keyword is invalid * @throws InvalidInputException if the input string is invalid */ public String encode(String keyword, String inputString) throws InvalidKeywordException, InvalidInputException{ reset(); setKeyword(keyword); setInputString(inputString); encode(); return outputString; } /** * Decodes the input string using the provided keyword. * * @param keyword the keyword to use for decoding * @param inputString the string to be decoded * @return the decoded string * @throws InvalidKeywordException if the keyword is invalid * @throws InvalidInputException if the input string is invalid */ public String decode(String keyword, String inputString) throws InvalidKeywordException, InvalidInputException{ reset(); setKeyword(keyword); setInputString(inputString); decode(); return outputString; } //?Getters /** * Gets the current input string. * * @return the input string */ public String getInputString(){ return inputString; } /** * Gets the current output string. * * @return the output string */ public String getOutputString(){ return outputString; } /** * Gets the current keyword. * * @return the keyword */ public String getKeyword(){ return keyword; } /** * Gets the current offset list. * * @return the offset list */ public List getOffsets(){ return offset; } /** * Resets all fields to their default values. */ public void reset(){ logger.debug("Resetting fields"); inputString = ""; outputString = ""; keyword = ""; offset.clear(); } }