//CipherStreamJava/src/main/java/com/mattrixwv/cipherstream/polysubstitution/RailFence.java //Mattrixwv // Created: 03-21-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.polysubstitution; import java.math.BigDecimal; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mattrixwv.cipherstream.exceptions.InvalidBaseException; import com.mattrixwv.cipherstream.exceptions.InvalidInputException; /** * Represents the Rail Fence cipher encryption and decryption. * The Rail Fence cipher is a form of transposition cipher that writes the message in a zigzag pattern * across multiple "rails" and then reads it off row by row to encode or decode the message. */ public class RailFence{ private static final Logger logger = LoggerFactory.getLogger(RailFence.class); //?Fields /** The message that needs to be encoded/decoded */ protected String inputString; /** The encoded/decoded message */ protected String outputString; /** The fence used for encoding/decoding */ protected StringBuilder[] fence; //?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; /** * Strips invalid characters from the string that needs to be encoded/decoded. * * @param inputString the input string to be cleaned * @throws InvalidInputException if the input string is null or invalid */ protected void setInputString(String inputString) throws InvalidInputException{ //Ensure the input string 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("Clean 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"); } } /** * Ensures the number of rails is valid and sets up the fence. * * @param numRails the number of rails to be used * @throws InvalidBaseException if the number of rails is less than 2 */ protected void setNumRails(int numRails) throws InvalidBaseException{ if(numRails < 2){ throw new InvalidBaseException("You must use at least 2 rails"); } logger.debug("Creating {} rails", numRails); fence = new StringBuilder[numRails]; for(int cnt = 0;cnt < numRails;++cnt){ fence[cnt] = new StringBuilder(); } } /** * Strips the input string of all non-letter characters. * * @return the cleaned input string */ protected String getCleanInputString(){ logger.debug("Getting input string for encoding"); return inputString.replaceAll("[^a-zA-Z]", ""); } /** * Ensures capitals, lowercase, and symbols are displayed in the output string. * * @param outputString the encoded/decoded output string to be formatted */ protected void formatOutput(String outputString){ logger.debug("Formatting output string"); StringBuilder output = new StringBuilder(); int outputLoc = 0; for(char ch : inputString.toCharArray()){ logger.debug("Working character {}", ch); if(Character.isUpperCase(ch)){ logger.debug("Formatting uppercase"); output.append(Character.toUpperCase(outputString.charAt(outputLoc++))); } else if(Character.isLowerCase(ch)){ logger.debug("Formatting lowercase"); output.append(Character.toLowerCase(outputString.charAt(outputLoc++))); } else{ logger.debug("Inserting symbol"); output.append(ch); } } this.outputString = output.toString(); logger.debug("Formatted output '{}'", this.outputString); } /** * Returns the decoded string found in the fence after all characters are placed correctly. * * @return the decoded string */ protected String getDecodedStringFromFence(){ logger.debug("Getting decoded string from the fence"); boolean down = true; int rail = 0; int outsideCol = 0; int insideCol = -1; StringBuilder output = new StringBuilder(); while(true){ //Get the next character based on what rail you are currently using if(rail == 0){ if(outsideCol >= fence[rail].length()){ break; } output.append(fence[rail].charAt(outsideCol)); ++insideCol; } else if(rail == (fence.length - 1)){ if(outsideCol >= fence[rail].length()){ break; } output.append(fence[rail].charAt(outsideCol++)); ++insideCol; } else{ if(insideCol >= fence[rail].length()){ break; } output.append(fence[rail].charAt(insideCol)); } //Make sure you're still in bounds if(down){ ++rail; } else{ --rail; } if(rail >= fence.length){ down = false; rail -= 2; } else if(rail < 0){ down = true; rail += 2; } } logger.debug("Fence output '{}'", output); return output.toString(); } /** * Encodes the input string using the Rail Fence cipher and stores the result in the output string. */ protected void encode(){ logger.debug("Encoding"); boolean up = true; int rail = 0; for(char ch : getCleanInputString().toCharArray()){ logger.debug("Working character '{}'", ch); fence[rail].append(ch); //Advance to the next rail if(up){ logger.debug("Moving up"); ++rail; } else{ logger.debug("Moving down"); --rail; } //Make sure you're still in bounds if(rail == fence.length){ logger.debug("Swapping to down"); up = false; rail -= 2; } else if(rail == -1){ logger.debug("Swapping to up"); up = true; rail += 2; } } //Append the fence rows to come up with a single string logger.debug("Appending rows from the fence"); StringBuilder output = new StringBuilder(); for(StringBuilder segment : fence){ output.append(segment); } //Format the output formatOutput(output.toString()); } /** * Decodes the input string using the Rail Fence cipher and stores the result in the output string. */ protected void decode(){ logger.debug("Decoding"); //Determine the number of characters on each rail String cleanInputString = getCleanInputString(); int cycleLength = 2 * (fence.length - 1); BigDecimal k = new BigDecimal(cleanInputString.length()).divide(new BigDecimal(cycleLength)); int numInTopRail = (int)Math.ceil(k.doubleValue()); int numInMiddleRails = (numInTopRail * 2); int numInBottomRail = 0; boolean goingDown = true; int middleNum = 0; if(k.remainder(BigDecimal.ONE).compareTo(new BigDecimal("0.5")) <= 0){ numInMiddleRails -= 1; numInBottomRail = (int)Math.floor(k.doubleValue()); goingDown = true; middleNum = k.remainder(BigDecimal.ONE).multiply(new BigDecimal(cycleLength)).intValue() - 1; } else{ numInBottomRail = numInTopRail; goingDown = false; middleNum = (cycleLength - (k.remainder(BigDecimal.ONE).multiply(new BigDecimal(cycleLength))).intValue()); } logger.debug("Number of characters in the top rail {}", numInTopRail); logger.debug("Number of characters in the middle rails {}", numInMiddleRails); logger.debug("Number of characters in the bottom rail {}", numInBottomRail); //Add the correct number of characters to each rail logger.debug("Adding characters to the rails"); fence[0].append(cleanInputString.substring(0, numInTopRail)); int start = numInTopRail; int end = numInTopRail + numInMiddleRails; for(int cnt = 1;cnt < (fence.length - 1);++cnt){ if((!goingDown) && (middleNum >= cnt)){ end -= 1; } fence[cnt].append(cleanInputString.substring(start, end)); start = end; end += numInMiddleRails; } end = start + numInBottomRail; logger.debug("Appending the bottom rail"); fence[fence.length - 1].append(cleanInputString.substring(start, end)); //Get the decoded string from the constructed fence String output = getDecodedStringFromFence(); logger.debug("Fence output '{}'", output); formatOutput(output); } //?Constructor /** * Constructs a RailFence cipher instance with default settings. */ public RailFence(){ preserveCapitals = false; preserveWhitespace = false; preserveSymbols = false; reset(); } /** * Constructs a RailFence cipher instance with specified settings. * * @param preserveCapitals whether to preserve uppercase letters * @param preserveWhitespace whether to preserve whitespace * @param preserveSymbols whether to preserve symbols */ public RailFence(boolean preserveCapitals, boolean preserveWhitespace, boolean preserveSymbols){ this.preserveCapitals = preserveCapitals; this.preserveWhitespace = preserveWhitespace; this.preserveSymbols = preserveSymbols; reset(); } /** * Encodes the input string using a Rail Fence of the specified number of rails and returns the result. * * @param numRails the number of rails to use * @param inputString the message to encode * @return the encoded message * @throws InvalidBaseException if the number of rails is invalid * @throws InvalidInputException if the input string is invalid */ public String encode(int numRails, String inputString) throws InvalidBaseException, InvalidInputException{ //Set the parameters setNumRails(numRails); setInputString(inputString); //Encode encode(); return outputString; } /** * Decodes the input string using a Rail Fence of the specified number of rails and returns the result. * * @param numRails the number of rails to use * @param inputString the encoded message to decode * @return the decoded message * @throws InvalidBaseException if the number of rails is invalid * @throws InvalidInputException if the input string is invalid */ public String decode(int numRails, String inputString) throws InvalidBaseException, InvalidInputException{ //Set the parameters setNumRails(numRails); setInputString(inputString); //Decode decode(); return outputString; } /** * Resets all variables to their default values. */ public void reset(){ logger.debug("Resetting fields"); inputString = ""; outputString = ""; fence = null; } //?Getters /** * 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 the number of rails used in the Rail Fence cipher. * * @return the number of rails */ public int getNumRails(){ return fence.length; } }