//CipherStreamJava/src/main/java/com/mattrixwv/cipherstream/monosubstitution/Porta.java //Mattrixwv // Created: 02-28-22 //Modified: 08-11-24 /* Copyright (C) 2024 Mattrixwv This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program. If not, see . */ package com.mattrixwv.cipherstream.monosubstitution; 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 Porta cipher, * which is a variant of the Vigenère cipher that uses a tableau of * alphabets to encode and decode messages. * *

* The Porta cipher uses a series of Caesar ciphers based on a repeating * keyword, with a pre-defined set of alphabetic shifts. This implementation * allows for encoding and decoding of messages while preserving or removing * certain characters based on configuration settings. *

*/ public class Porta{ private static final Logger logger = LoggerFactory.getLogger(Porta.class); /** Predefined alphabetic tableau used for encoding and decoding */ private static final String[] tableau = { "NOPQRSTUVWXYZABCDEFGHIJKLM", //A-B "OPQRSTUVWXYZNMABCDEFGHIJKL", //C-D "PQRSTUVWXYZNOLMABCDEFGHIJK", //E-F "QRSTUVWXYZNOPKLMABCDEFGHIJ", //G-H "RSTUVWXYZNOPQJKLMABCDEFGHI", //I-J "STUVWXYZNOPQRIJKLMABCDEFGH", //K-L "TUVWXYZNOPQRSHIJKLMABCDEFG", //M-N "UVWXYZNOPQRSTGHIJKLMABCDEF", //O-P "VWXYZNOPQRSTUFGHIJKLMABCDE", //Q-R "WXYZNOPQRSTUVEFGHIJKLMABCD", //S-T "XYZNOPQRSTUVWDEFGHIJKLMABC", //U-V "YZNOPQRSTUVWXCDEFGHIJKLMAB", //W-X "ZNOPQRSTUVWXYBCDEFGHIJKLMA" //Y-Z }; //?Fields /** The string that needs encoded/decoded */ protected String inputString; /** The encoded/decoded string */ protected String outputString; /** The keyword used to encode the input string */ protected String keyword; //?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; /** * Ensures all keyword constraints are followed. * * @param keyword the keyword to be used for encoding/decoding * @throws InvalidKeywordException if the keyword is null, empty, or less than 2 characters */ protected void setKeyword(String keyword) throws InvalidKeywordException{ //Make sure the keyword isn't null 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 and save the string logger.debug("Removing all non-letters"); keyword = keyword.replaceAll("[^A-Z]", ""); //Save the keyword this.keyword = keyword; logger.debug("Cleaned keyword '{}'", keyword); //If after eliminating all ususable characters the keyword is empty throw an exception if(this.keyword.isBlank() || (this.keyword.length() < 2)){ throw new InvalidKeywordException("Keyword must contain at least 2 letters"); } } /** * Ensures all input constraints are followed. * * @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{ //Ensure the input isn't null if(inputString == null){ throw new InvalidInputException("Input cannot be null"); } logger.debug("Original input string {}", inputString); //Apply removal options 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]", ""); } //Save the string logger.debug("Cleaned input string '{}'", inputString); this.inputString = inputString; //Ensure the string isn't blank if(this.inputString.isBlank()){ throw new InvalidInputException("Input must contain at least 1 letter"); } } /** * Returns the letter that replaces the passed-in letter based on the keyword and tableau. * * @param keywordCnt the index of the keyword character to use * @param letter the letter to be replaced * @return the replacement letter */ protected char getReplacer(int keywordCnt, char letter){ logger.debug("Getting letter that replaces {} at {}", letter, keywordCnt); char keyLetter = keyword.charAt(keywordCnt % keyword.length()); int tableauColumn = (Character.toUpperCase(letter) - 'A'); char replacer; switch(keyLetter){ case 'A', 'B' -> replacer = tableau[0].charAt(tableauColumn); case 'C', 'D' -> replacer = tableau[1].charAt(tableauColumn); case 'E', 'F' -> replacer = tableau[2].charAt(tableauColumn); case 'G', 'H' -> replacer = tableau[3].charAt(tableauColumn); case 'I', 'J' -> replacer = tableau[4].charAt(tableauColumn); case 'K', 'L' -> replacer = tableau[5].charAt(tableauColumn); case 'M', 'N' -> replacer = tableau[6].charAt(tableauColumn); case 'O', 'P' -> replacer = tableau[7].charAt(tableauColumn); case 'Q', 'R' -> replacer = tableau[8].charAt(tableauColumn); case 'S', 'T' -> replacer = tableau[9].charAt(tableauColumn); case 'U', 'V' -> replacer = tableau[10].charAt(tableauColumn); case 'W', 'X' -> replacer = tableau[11].charAt(tableauColumn); case 'Y', 'Z' -> replacer = tableau[12].charAt(tableauColumn); default -> replacer = letter; } logger.debug("Replacer {}", replacer); return replacer; } /** * Encodes the inputString and stores the result in outputString. * Encoding is the same as decoding for this cipher. */ protected void encode(){ logger.debug("Encoding"); //Encoding is the same as decoding code(); } /** * Decodes the inputString and stores the result in outputString. * Decoding is the same as encoding for this cipher. */ protected void decode(){ logger.debug("Decoding"); //Decoding is the same as encoding code(); } /** * Encodes or decodes the inputString based on the provided keyword. * Uses the tableau to replace characters. */ protected void code(){ StringBuilder output = new StringBuilder(); //Step through every character in the inputString and advance it the correct amount according to the keyword and tableau int keywordCnt = 0; for(char letter : inputString.toCharArray()){ logger.debug("Working character {}", letter); //If the character is a letter replace with the corresponding character from the tableau if(Character.isUpperCase(letter)){ logger.debug("Encoding uppercase"); letter = Character.toUpperCase(getReplacer(keywordCnt, letter)); ++keywordCnt; } else if(Character.isLowerCase(letter)){ logger.debug("Encoding lowercase"); letter = Character.toLowerCase(getReplacer(keywordCnt, letter)); ++keywordCnt; } //Add the current character to the output logger.debug("Encoded letter {}", letter); output.append(letter); } //Save the output outputString = output.toString(); logger.debug("Saving output string '{}'", outputString); } //?Constructor /** * Constructs a new {@code Porta} instance with default settings. */ public Porta(){ preserveCapitals = false; preserveWhitespace = false; preserveSymbols = false; reset(); } /** * Constructs a new {@code Porta} 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 Porta(boolean preserveCapitals, boolean preserveWhitespace, boolean preserveSymbols){ this.preserveCapitals = preserveCapitals; this.preserveWhitespace = preserveWhitespace; this.preserveSymbols = preserveSymbols; reset(); } /** * Sets the keyword and inputString and encodes the message. * * @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{ //Set the parameters reset(); setKeyword(keyword); setInputString(inputString); //Encode and return the message encode(); return outputString; } /** * Sets the keyword and inputString and decodes the message. * * @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{ //Set the parameters reset(); setKeyword(keyword); setInputString(inputString); //Decode and return the message 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; } /** * Resets all fields to their default values. */ public void reset(){ logger.debug("Resetting fields"); inputString = ""; outputString = ""; keyword = ""; } }