Cybersecurity-Projects/PROJECTS/beginner/caesar-cipher/learn/02-ARCHITECTURE.md

452 lines
16 KiB
Markdown

# System Architecture
This document breaks down how the system is designed and why certain architectural decisions were made.
## High Level Architecture
```
┌─────────────────┐
│ CLI Layer │ (main.py)
│ - encrypt cmd │ Typer commands, Rich output
│ - decrypt cmd │
│ - crack cmd │
└────────┬────────┘
┌─────────────────┐
│ Cipher Layer │ (cipher.py)
│ - CaesarCipher │ Core algorithm
│ - shift logic │ Encryption/decryption
└────────┬────────┘
┌─────────────────┐
│ Analysis Layer │ (analyzer.py)
│ - chi-squared │ Statistical scoring
│ - rank results │ Frequency comparison
└─────────────────┘
┌─────────────────┐
│ Utils Layer │ (utils.py, constants.py)
│ - file I/O │ Support functions
│ - validation │ Reference data
└─────────────────┘
```
### Component Breakdown
**CLI Layer (main.py)**
- Purpose: User-facing commands that parse arguments and call core functions
- Responsibilities: Input validation, file handling, formatted output with Rich tables
- Interfaces: Exposes three commands via Typer (encrypt, decrypt, crack)
**Cipher Layer (cipher.py)**
- Purpose: Implements the actual Caesar cipher algorithm
- Responsibilities: Character shifting, key validation, brute force generation
- Interfaces: `CaesarCipher` class with `encrypt()`, `decrypt()`, and static `crack()` method
**Analysis Layer (analyzer.py)**
- Purpose: Statistical analysis to identify correct decryptions
- Responsibilities: Chi-squared calculation, candidate ranking by English frequency
- Interfaces: `FrequencyAnalyzer` class that scores and ranks text
**Utils Layer (utils.py, constants.py)**
- Purpose: Shared functionality and reference data
- Responsibilities: Reading from files/stdin, writing output, storing English letter frequencies
- Interfaces: Standalone functions and constants used across other layers
## Data Flow
### Encryption Flow
Step by step walkthrough of encrypting text:
```
1. User Input → CLI Parser (main.py:34-56)
Validates key is 0-25, reads text from arg/file/stdin
2. CLI → CaesarCipher (cipher.py:13-28)
Creates cipher instance with validated key
3. CaesarCipher → Encryption Loop (cipher.py:43-46)
Shifts each character, preserves non-letters
4. Encrypted Text → Output (main.py:56-60)
Writes to file or prints with Rich formatting
```
Example with code references:
```
1. User runs: caesar-cipher encrypt "HELLO" --key 3
main.py:34 → validates key with utils.validate_key()
main.py:47 → reads input via read_input()
2. main.py:48 → Creates CaesarCipher(key=3)
cipher.py:16 → Stores key % 26 to handle wrapping
3. main.py:49 → Calls cipher.encrypt("HELLO")
cipher.py:43-46 → Iterates each char with _shift_char()
cipher.py:31-38 → Shifts H→K, E→H, L→O, L→O, O→R
4. main.py:52 → Outputs "KHOOR" via console.print()
```
### Cracking Flow
More complex: tries all keys and ranks by frequency.
```
1. User Input → CLI (main.py:108-128)
Reads ciphertext, gets options (--top N, --all)
2. CLI → Brute Force (cipher.py:53-60)
Generates all 26 possible decryptions
3. Candidates → Frequency Analysis (analyzer.py:53-60)
Scores each with chi-squared test
4. Ranked Results → Table Output (main.py:135-148)
Displays top matches in Rich table
```
## Design Patterns
### Strategy Pattern (Implicit)
**What it is:**
Separating the algorithm (cipher operations) from its application (CLI commands).
**Where we use it:**
The `CaesarCipher` class in `cipher.py` is independent of how it's invoked. You could use it from a web API, GUI, or CLI without changing the cipher code.
**Why we chose it:**
Separation of concerns makes testing easier. The cipher logic has zero dependencies on Typer or Rich. You can test `cipher.py` without dealing with command line argument parsing.
**Trade-offs:**
- Pros: Clean interfaces, easy to test, reusable components
- Cons: More files than a single script, slightly more complex for a simple project
### Factory Pattern (Static Methods)
**What it is:**
Using `@staticmethod` to create variations without instantiation.
**Where we use it:**
```python
# cipher.py:52-60
@staticmethod
def crack(ciphertext: str) -> list[tuple[int, str]]:
results = []
for shift in range(ALPHABET_SIZE):
cipher = CaesarCipher(key=shift)
decrypted = cipher.decrypt(ciphertext)
results.append((shift, decrypted))
return results
```
**Why we chose it:**
The `crack()` method doesn't belong to any particular key value. It generates all possible instances. Making it static makes the API clearer: `CaesarCipher.crack()` reads like "try all Caesar keys" without needing an instance.
## Layer Separation
```
┌────────────────────────────────────┐
│ CLI Layer (main.py) │
│ - User interaction │
│ - Does not do crypto │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ Business Logic (cipher.py) │
│ - Core algorithm │
│ - Does not know about CLI │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ Support (utils.py) │
│ - Generic helpers │
│ - No domain logic │
└────────────────────────────────────┘
```
### Why Layers?
Prevents dependencies from becoming circular. CLI can import cipher, but cipher doesn't import CLI. This means:
- You can test the cipher without mocking Typer
- You could use the cipher in a different interface (web, GUI) without modification
- Changes to output formatting don't affect cryptographic correctness
### What Lives Where
**CLI Layer (main.py):**
- Files: `main.py`
- Imports: Can import from cipher, analyzer, utils
- Forbidden: No crypto logic, no direct alphabet manipulation
**Business Layer (cipher.py, analyzer.py):**
- Files: `cipher.py`, `analyzer.py`
- Imports: Can import constants, utils. Cannot import main.
- Forbidden: No command line parsing, no Rich formatting
**Support Layer (utils.py, constants.py):**
- Files: `utils.py`, `constants.py`
- Imports: Standard library only
- Forbidden: No domain logic, stays generic
## Data Models
### CaesarCipher
```python
# cipher.py:13-28
class CaesarCipher:
def __init__(self, key: int, alphabet: str | None = None) -> None:
if not -25 <= key <= 26:
raise ValueError("Key must be between -25 and 26")
self.key = key % ALPHABET_SIZE
self.alphabet = alphabet or (UPPERCASE_LETTERS + LOWERCASE_LETTERS)
```
**Fields explained:**
- `key`: The shift amount, normalized to 0-25 via modulo. Storing it this way means encryption never has to handle wrapping again.
- `alphabet`: Normally just A-Z + a-z, but configurable for custom alphabets. Not used in CLI but extensible.
**Relationships:**
- Uses constants from `constants.py` (ALPHABET_SIZE, letter sets)
- Used by all three CLI commands
- Has no dependencies on analyzer (one-way relationship)
### FrequencyAnalyzer
```python
# analyzer.py:11-16
class FrequencyAnalyzer:
def __init__(self) -> None:
self.reference_frequencies = ENGLISH_LETTER_FREQUENCIES
```
**Fields explained:**
- `reference_frequencies`: Dictionary mapping 'A'-'Z' to their expected percentages in English text. Loaded from `constants.py:17-43`.
**Relationships:**
- Used only by the `crack` command
- Operates on output from `CaesarCipher.crack()`
- No dependencies on the cipher itself
## Security Architecture
### Threat Model
What we're protecting against:
1. **None** - This is an educational tool. The cipher is intentionally weak.
2. **Input Validation** - Prevents crashes from bad keys or missing input
3. **File Injection** - Uses Path objects and explicit encoding to avoid issues
What we're NOT protecting against (out of scope):
- Cryptanalysis - The cipher is meant to be broken
- Side-channel attacks - This is Python, timing isn't constant
- Key recovery - The key space is trivially small
### Defense Layers
This project doesn't have security defenses because it's teaching cryptanalysis, not building secure crypto. But it does have input validation:
```
Layer 1: Key validation (utils.py:36-40, cipher.py:19-22)
Layer 2: Input source validation (utils.py:11-24)
Layer 3: File encoding (utils.py:17, 32)
```
The validation ensures the program doesn't crash, but there's no security boundary. Don't use Caesar cipher for actual secrets.
## Storage Strategy
### No Persistent Storage
This tool is stateless. Everything happens in memory. Input comes from arguments/files/stdin, output goes to stdout/files, and nothing is saved.
**Why this choice:**
Simplicity. There's no need to track history or save state. Each command is independent.
## Configuration
### Environment Variables
None. The project uses command line arguments exclusively.
### Configuration Strategy
**Development:**
```bash
pip install -e . # Editable install
```
All configuration is in `pyproject.toml`. Dependencies, linter settings, test config all live there.
**Production:**
```bash
pip install . # Regular install
```
Same configuration. This isn't deployed to production because it's a teaching tool, but if it were, the config doesn't change.
## Performance Considerations
### Bottlenecks
Where this system gets slow under load:
1. **Frequency analysis on huge files** - The chi-squared calculation is O(n) where n is text length. For multi-MB files, this adds up when run 26 times.
2. **Rich table rendering** - Printing 26 rows of output is slower than plain text.
Neither matters in practice. The cipher itself is so fast that I/O dominates.
### Optimizations
What we did to make it faster:
- **List comprehension in encrypt()**: Using `"".join(self._shift_char(char, self.key) for char in plaintext)` in `cipher.py:46` instead of building a list and joining is more memory efficient.
- **Early return in chi-squared**: If there are no letters at all, return infinity immediately (analyzer.py:33) instead of trying to calculate on empty data.
### Scalability
**Vertical scaling:**
Doesn't apply. Single-threaded Python processing text. More CPU doesn't help.
**Horizontal scaling:**
You could parallelize the crack command to try all 26 shifts in parallel, but it's already instant. Not worth the complexity.
## Design Decisions
### Decision 1: Separate Cipher and Analyzer Classes
**What we chose:**
Keep encryption logic in `CaesarCipher`, statistical analysis in `FrequencyAnalyzer`.
**Alternatives considered:**
- Put everything in one class - Rejected because mixing crypto and cryptanalysis in the same object is conceptually wrong
- Make analyzer functions instead of a class - Rejected because the reference frequencies are shared state
**Trade-offs:**
We get cleaner separation and better testability. The cost is two imports instead of one when you want to crack messages.
### Decision 2: CLI with Typer Instead of argparse
**What we chose:**
Use Typer for automatic help generation and type hints.
**Alternatives considered:**
- argparse (stdlib) - More verbose, no type hints
- click - Similar to Typer but without the type hint magic
**Trade-offs:**
Typer gives clean code at the cost of an extra dependency. For a learning project, the better code readability is worth it.
### Decision 3: Preserve Case and Non-Letters
**What we chose:**
Encrypt only A-Z and a-z, leave spaces/punctuation/numbers unchanged.
**Alternatives considered:**
- Convert everything to uppercase - Loses information, makes output uglier
- Encrypt spaces too - Historical Caesar didn't do this, less authentic
**Trade-offs:**
Preserving case makes the output more readable but slightly complicates the shifting logic (need to check uppercase vs lowercase separately).
## Deployment Architecture
This is a local CLI tool, not a deployed service. Installation is via pip:
```bash
pip install caesar-salad-cipher
```
The `pyproject.toml:31-32` entry point makes the command available:
```toml
[project.scripts]
caesar-cipher = "caesar_cipher.main:app"
```
After installation, `caesar-cipher` is in your PATH and calls `main.py:app()`.
## Error Handling Strategy
### Error Types
1. **Invalid Key** - Key outside -25 to 26 range
- Raised by `cipher.py:20-22` and `utils.py:36-40`
- Caught in `main.py:59, 97, 152` and printed as error
2. **Missing Input** - No text provided
- Raised by `utils.py:23` if all sources (arg, file, stdin) are empty
- Caught in main commands
3. **File Not Found** - Input file doesn't exist
- Raised by `Path.read_text()` in `utils.py:17`
- Caught generically as OSError
### Recovery Mechanisms
There's no automatic recovery. The tool exits with code 1 on error:
```python
# main.py:59-60
except (ValueError, OSError) as e:
console.print(f"[red]Error:[/red] {e}")
raise typer.Exit(code=1) from None
```
**Why exit instead of retry:**
It's a CLI tool. If the user gave a bad key, they need to fix it and run again. No point staying alive.
## Extensibility
### Where to Add Features
Want to add support for numbers? Here's where it goes:
1. Modify `constants.py` to include '0'-'9' in the alphabet
2. Update `cipher.py:31-40` _shift_char() to handle digits
3. Adjust tests in `test_cipher.py` to verify digit shifting
Want to add Vigenère cipher?
1. Create `vigenere.py` with a similar class structure
2. Add `vigenere` command in `main.py`
3. Reuse `utils.py` for I/O, but frequency analysis won't work (Vigenère is polyalphabetic)
## Limitations
Current architectural limitations:
1. **Only works on Latin alphabet** - No support for Cyrillic, Arabic, or ideographic scripts. Fixing this would require multi-alphabet constants and different frequency tables.
2. **No key derivation** - The key is literally just a number. Can't use passwords. Would need a KDF (but that's overkill for Caesar).
3. **Single-threaded** - Can't take advantage of multiple cores. Not worth fixing when crack() runs in under a millisecond anyway.
These are not bugs, they're conscious trade-offs. The project is for learning classical crypto, not building production tools.
## Comparison to Similar Systems
### ROT13 Online Tools
How we're different:
- ROT13 tools only do shift=13. We support any key 0-25.
- We have frequency analysis built in for cracking.
Why we made different choices:
ROT13 is a special case. We're teaching the general algorithm and how to break it.
### CyberChef
CyberChef is a Swiss Army knife with dozens of encodings including Caesar. Our tool is purpose-built for learning cryptanalysis, so we include the statistical scoring and ranking that CyberChef doesn't emphasize.
## Key Files Reference
Quick map of where to find things:
- `src/caesar_cipher/cipher.py` - Core algorithm: _shift_char(), encrypt(), decrypt(), crack()
- `src/caesar_cipher/analyzer.py` - Chi-squared calculation and candidate ranking
- `src/caesar_cipher/main.py` - CLI commands and Rich table formatting
- `src/caesar_cipher/constants.py` - English letter frequencies (the key to breaking Caesar)
- `tests/test_cipher.py` - Encryption/decryption roundtrip tests
- `tests/test_analyzer.py` - Frequency analysis correctness tests
## Next Steps
Now that you understand the architecture:
1. Read [03-IMPLEMENTATION.md](./03-IMPLEMENTATION.md) for code walkthrough
2. Try modifying the shift algorithm to rotate in reverse (negative keys already work, but make the default behavior different)