Logic & Computation — Assignment

Dynamic Substitution Cipher

Java  ·  File I/O  ·  HashMaps  ·  Encryption

Overview

You will build a Java program that can encrypt and decrypt text files using a randomly generated substitution cipher. Each time your program encrypts a file, it produces a unique, random character mapping — meaning the same input encrypted twice will produce two completely different outputs.

Your program saves this mapping as a key file, which is later required to decrypt. This is the fundamental idea behind symmetric-key encryption: whoever holds the key can lock and unlock the message; everyone else sees gibberish.

Learning Objectives

Program Specification

Your program should be run from the command line in two modes:

Encrypt Mode

$ java DynamicCipher encrypt <input_file>

Behavior:

  1. Read the contents of <input_file> (a .txt file of arbitrary length).
  2. Generate a random permutation of all printable ASCII characters (codes 32–126). This creates a one-to-one mapping where every character maps to exactly one other character, and no two characters map to the same target.
  3. Use this mapping to replace every character in the input, producing the ciphertext.
  4. Write the ciphertext to a new file named <input_file>.enc
    (e.g., message.txtmessage.txt.enc).
  5. Write the key (the character mapping) to a new file named <input_file>.key
    (e.g., message.txtmessage.txt.key).
  6. Print a confirmation message to the console.

Decrypt Mode

$ java DynamicCipher decrypt <encrypted_file> <key_file>

Behavior:

  1. Read the key file and reconstruct the character mapping.
  2. Invert the mapping (if 'A' → 'x' in the encryption map, then 'x' → 'A' in the decryption map).
  3. Read the encrypted file and apply the inverted mapping to recover the original plaintext.
  4. Write the result to a new file named <encrypted_file>.dec
    (e.g., message.txt.encmessage.txt.enc.dec).
  5. Print a confirmation message to the console.

Detailed Requirements

Character Range

Your cipher must handle all printable ASCII characters — codes 32 (space) through 126 (~). That is 95 characters total. Characters outside this range (newlines, tabs, etc.) should be passed through unchanged.

Random Key Generation

Your mapping must be a true permutation — every character in the range maps to exactly one unique character, with no repeats. The simplest approach:

  1. Create a char[] of all 95 printable ASCII characters in order.
  2. Shuffle the array using the Fisher-Yates algorithm (or use Collections.shuffle() on an ArrayList<Character>).
  3. The original-order array and the shuffled array, read side by side, define your mapping.

Key File Format

Design Decision — You Choose

Your key file must store enough information for the decrypt mode to perfectly reconstruct the mapping. Here are some approaches to consider (pick one, or invent your own):

  • Write each mapping pair as a line: <original><delimiter><mapped>
  • Write two lines: the original character sequence and the permuted sequence
  • Write a single line: the permuted sequence (if the original order is always the same known order)

Whatever format you choose, document it in a comment at the top of your source code.

Error Handling

Your program should print a helpful error message and exit cleanly if:

Milestones

Work through these in order. Test each one before moving on.

Milestone 1

Hardcoded Cipher

Create a HashMap<Character, Character> with a simple, fixed mapping (e.g., shift every letter by 1). Encrypt and decrypt a short test file. Verify the decrypted output matches the original exactly.

Milestone 2

Random Key Generation

Replace the hardcoded map with a randomly generated permutation. Print the mapping to the console so you can visually verify that every character maps to a unique target.

Milestone 3

Key File Serialization

Design your key file format. Write the key to a file during encryption. Read it back during decryption and verify that the reconstructed map matches the original.

Milestone 4

Full Integration

Wire everything together with command-line argument parsing. Test the full round trip:

$ java DynamicCipher encrypt secret.txt
$ java DynamicCipher decrypt secret.txt.enc secret.txt.key

Then diff the original against the decrypted file — they should be identical.

Test Strategy

Use these test cases to verify correctness:

TestWhat to Check
Short file (1 sentence)Basic round-trip: original == decrypted
Empty fileProgram handles gracefully, produces empty output
All 95 printable ASCII charsEvery character encrypts and decrypts correctly
File with newlines and tabsNon-printable characters pass through unchanged
Encrypt same file twiceTwo different .enc files are produced (different keys)
Decrypt with wrong keyOutput should be garbled (not crash)
Sample Test File

Create a file called test_input.txt with the following contents:

The quick brown fox jumps over the lazy dog.
ABCDEFGHIJKLMNOPQRSTUVWXYZ
abcdefghijklmnopqrstuvwxyz
0123456789
Special chars: !@#$%^&*()_+-=[]{}|;':",.<>?/`~

This covers uppercase, lowercase, digits, punctuation, and spaces — a good stress test for your full character range.

Extension Challenges

If you finish the base assignment, try one or more of these:

Moderate Password Layer (Vigenère on Top)

After substitution, apply a Vigenère cipher using a user-provided password. The password is not saved anywhere — the user must remember it. Now decryption requires both the key file and the correct password.

Moderate Key Space Analysis

Calculate and print the total number of possible keys your cipher could generate (hint: it is 95!). Research why this makes brute force infeasible, and write a brief explanation as a comment in your code.

Hard Frequency Analysis Attack

Write a third mode, analyze, that takes an encrypted file without a key and attempts to crack it using English letter frequency analysis. How long does the file need to be before this works reliably? This is a great introduction to why substitution ciphers are historically breakable.

Submission Checklist