import java.io.*; import java.util.*; /** * Solitaire Encryption Algorithm * for CS 201, Spring 2007 * University of Illinois at Chicago * * This program implements the Solitaire Encryption Algorithm. * * This implementation can both encrypt and decrypt. There are two commandline * arguments which can be passed, both of them optional: * * The first is the name of a file in which the initial card deck order can be * found as a line of space-dilineated numbers. * * The second is one of "encrypt", "decrypt", or "ask", specifying the program's * mode of operation, respectively, encryption, decryption, or prompt the user. * * If the deck order file is given but not the mode, it defaults to "decrypt". * * If neither is given, the user is promped for the deck order as well as the * mode. * * Copyright (c) 2007 William R. Fraser * All rights reserved. * * @author William R. Fraser * @version 2007.03.29 */ public class SolitaireEncrypt { private CardDeck deck; public static final int DECRYPT = 0; public static final int ENCRYPT = 1; public static final int MODEPROMPT = 2; // If true, will perform a thorough check of the deck after each step in the // key generation. public static boolean DOSANITYCHECKS = false; // how many times has getNextKey() been called? private static int iterations = 0; /** * Performs the encrypting or decrypting * * @param deckFilename File to read deck order from, or - to prompt the user * @param encryptOrDecrypt Set to ENCRYPT or DECRYPT to do that. */ private void go(String deckFilename, int encryptOrDecrypt) { Scanner keyboard = new Scanner(System.in); String deckString = ""; if (deckFilename.equals("-")) { // for debug purposes System.out.print("Enter deck: "); deckString = keyboard.nextLine(); } else { try { FileReader fr = new FileReader(deckFilename); BufferedReader br = new BufferedReader(fr); deckString = br.readLine(); fr.close(); } catch (FileNotFoundException ex) { System.err.println("File not found!"); System.exit(1); } catch (IOException ex) { System.err.println("I/O error!"); System.exit(2); } } deck = new CardDeck(deckString); // runTest(100000); String input = ""; while (encryptOrDecrypt == MODEPROMPT) { System.out.print("(E)ncrypt or (D)ecrypt? "); String response = keyboard.nextLine(); response = response.toLowerCase(); if (response.equals("e") || response.equals("encrypt")) { encryptOrDecrypt = ENCRYPT; } else if (response.equals("d") || response.equals("decrypt")) { encryptOrDecrypt = DECRYPT; } else { System.out.println("That's not a valid response."); } } if (encryptOrDecrypt == ENCRYPT) System.out.print("Enter text (XXXXX to quit): "); else System.out.print("Enter encrypted text (XXXXX to quit): "); while (!(input = keyboard.nextLine()).equals("XXXXX")) { if (input.length() % 5 != 0 && encryptOrDecrypt == DECRYPT) System.err.println("WARNING: This message may be corrupt!\n"); if (encryptOrDecrypt == ENCRYPT) { System.out.println(encrypt(input)); System.out.print("Enter text (XXXXX to quit): "); } else { System.out.println(decrypt(input)); System.out.print("Enter encrypted text (XXXXX to quit): "); } } System.out.println("Bye!"); // all done. } /** * Decrypt the given string by generating a keystream from the current deck * state and adding it to the encrypted string. * * @param encrypted Encrypted string * @return Decrypted string */ public String decrypt(String encrypted) { Integer[] ints = stringToInts(encrypted); for (int i = 0; i < ints.length; i++) { int key = getNextKey(); ints[i] -= key; if (ints[i] < 1) ints[i] += 26; } return intsToString(ints); } /** * Encrypt the given string by generating a keystream from the current deck * state and subtracting it from the plaintext. * * @param plain Plaintext to encrypt * @return Encrypted text */ public String encrypt(String plain) { Integer[] ints = stringToInts(plain); for (int i = 0; i < ints.length; i++) { int key = getNextKey(); ints[i] += key; if (ints[i] > 26) ints[i] -= 26; } return intsToString(ints); } // note to self: to make an Eclipse expression watchpoint: // try { if (EXPRESSION) throw new RuntimeException(); } // catch (RuntimeException ex) { } /** * Get the next key from the current deck state by performing the 5 steps. * @return Next key in keystream */ public int getNextKey() { iterations++; stepOne(); // O(1) doSanityCheck("one"); stepTwo(); // O(1) doSanityCheck("two"); stepThree(); // O(1) doSanityCheck("three"); stepFour(); // O(n) doSanityCheck("four"); int i = stepFive(); // O(n) if (i == CardDeck.FIRSTJOKER || i == CardDeck.SECONDJOKER) { return getNextKey(); } else { return i; } } /** * Perform a sanity check on the deck. * @param step Which step was just performed? */ private void doSanityCheck(String step) { if (DOSANITYCHECKS) { String badness = deck.sanityCheck(); if (!badness.equals("")) { System.err.println("Oops! Deck got messed up on iteration " + iterations +" after step " + step + ":\n" + badness + "\nExiting!"); System.exit(-2); } } } /** * Generate a whole bunch of keys to expose errors. * @param howMany How many keys to generate */ public void runTest(int howMany) { boolean oldCheckStatus = DOSANITYCHECKS; // save old check status DOSANITYCHECKS = true; // enable sanity checks System.out.printf("TESTING: generating %,d keys:\n", howMany); long stime = System.currentTimeMillis(); for (int i = 0; i < howMany; i++) { System.out.printf("%,d:\t%d\n", i + 1, getNextKey()); } long etime = System.currentTimeMillis(); System.out.printf("Time: %.3f seconds.\n", (float) (etime - stime) / 1000); DOSANITYCHECKS = oldCheckStatus; // restore old state } public void stepOne() { deck.firstJokerSwapNext(); } public void stepTwo() { deck.secondJokerSwapNextTwice(); } public void stepThree() { deck.tripleCut(); } public void stepFour() { deck.countCut(); } public int stepFive() { int cardPos; if (deck.getHead() == deck.getSecondJoker()) { cardPos = deck.getFirstJoker().getDatum(); } else { cardPos = deck.getHead().getDatum(); } Card theCard = deck.getCardAt(cardPos); // O(n) return theCard.getDatum(); } /** * Convert a string into an array of integers in the range 1-26. * * This has the effect of discarding all non-alphabetic characters and * shifting to upper case. * * @param s * @return */ public Integer[] stringToInts(String s) { char[] ca = s.toCharArray(); ArrayList list = new ArrayList(); for (int i = 0; i < ca.length; i++) { if (ca[i] < 65 || (ca[i] > 90 && ca[i] < 97) || ca[i] > 122) continue; // non-alphabetical character if (ca[i] >= 97 && ca[i] <= 122) ca[i] -= 32; // shift to lower case ca[i] -= 64; // move down to 1-26 list.add((int) ca[i]); } // ensure return length is a multiple of 5 for (int i = 0; i < list.size() % 5; i++) list.add(((int) 'X') - 64); // pad with 'X' characters return list.toArray(new Integer[] {}); } /** * Convert an array of integers in the range 1-26 into a string of uppercase * letters. * @param ia Integer array * @return String of uppercase letters represented by the integers. */ public String intsToString(Integer[] ints) { StringBuffer buf = new StringBuffer(); for (int i = 0; i < ints.length; i++) { // move ints back up to the 'A' - 'Z' range buf.append((char) (ints[i].intValue() + 64)); } return buf.toString(); } /** * Let's light this candle. * @param args */ public static void main(String[] args) { SolitaireEncrypt app = new SolitaireEncrypt(); /* * if we aren't given any arguments, prompt for everything. */ if (args.length == 0) { args = new String[2]; args[0] = "-"; args[1] = "ask"; } int enc_dec; if (args.length >= 2 && args[1].equals("encrypt")) { enc_dec = ENCRYPT; } else if (args.length >= 2 && args[1].equals("ask")) { enc_dec = MODEPROMPT; } else { // default to DECRYPT if only one argument was given enc_dec = DECRYPT; } app.go(args[0], enc_dec); } }