Cybersecurity-Projects/PROJECTS/beginner/caesar-cipher/src/caesar_cipher/main.py

189 lines
5.8 KiB
Python

"""
©AngelaMos | 2026
main.py
CLI entry point with encrypt, decrypt, and crack commands via Typer
Wires together input reading, cipher operations, and formatted Rich output
for three commands. The encrypt and decrypt commands require a key; crack
tries all 26 shifts and displays the ranked results as a table with the
best match highlighted.
Key exports:
app - Typer application, registered as the caesar-cipher CLI entry point
Connects to:
cipher.py - instantiates CaesarCipher for all three commands
analyzer.py - crack command uses FrequencyAnalyzer to rank candidates
utils.py - imports read_input, validate_key, write_output
"""
from pathlib import Path
from typing import Annotated
import typer
from rich.console import Console
from rich.table import Table
from caesar_cipher.analyzer import FrequencyAnalyzer
from caesar_cipher.cipher import CaesarCipher
from caesar_cipher.utils import read_input, validate_key, write_output
app = typer.Typer(
name = "caesar-cipher",
help = "Caesar cipher encryption, decryption, and brute-force cracking tool",
no_args_is_help = True,
)
console = Console()
@app.command()
def encrypt(
text: Annotated[
str | None,
typer.Argument(help = "Text to encrypt (or use --input-file or stdin)"),
] = None,
key: Annotated[int,
typer.Option("--key",
"-k",
help = "Shift key (0-25)")] = 3,
input_file: Annotated[
Path | None,
typer.Option("--input-file",
"-i",
help = "Input file path")] = None,
output_file: Annotated[
Path | None,
typer.Option("--output-file",
"-o",
help = "Output file path")] = None,
quiet: Annotated[
bool,
typer.Option("--quiet",
"-q",
help = "Suppress output messages")] = False,
) -> None:
"""
Encrypt text using Caesar cipher with specified shift key
"""
try:
validate_key(key)
plaintext = read_input(text, input_file)
cipher = CaesarCipher(key = key)
encrypted = cipher.encrypt(plaintext)
if not quiet and not output_file:
console.print(f"[green]Encrypted:[/green] {encrypted}")
else:
write_output(encrypted, output_file, quiet)
except (ValueError, OSError) as e:
console.print(f"[red]Error:[/red] {e}")
raise typer.Exit(code = 1) from None
@app.command()
def decrypt(
text: Annotated[
str | None,
typer.Argument(help = "Text to decrypt (or use --input-file or stdin)"),
] = None,
key: Annotated[int,
typer.Option("--key",
"-k",
help = "Shift key (0-25)")] = 3,
input_file: Annotated[
Path | None,
typer.Option("--input-file",
"-i",
help = "Input file path")] = None,
output_file: Annotated[
Path | None,
typer.Option("--output-file",
"-o",
help = "Output file path")] = None,
quiet: Annotated[
bool,
typer.Option("--quiet",
"-q",
help = "Suppress output messages")] = False,
) -> None:
"""
Decrypt text using Caesar cipher with specified shift key
"""
try:
validate_key(key)
ciphertext = read_input(text, input_file)
cipher = CaesarCipher(key = key)
decrypted = cipher.decrypt(ciphertext)
if not quiet and not output_file:
console.print(f"[blue]Decrypted:[/blue] {decrypted}")
else:
write_output(decrypted, output_file, quiet)
except (ValueError, OSError) as e:
console.print(f"[red]Error:[/red] {e}")
raise typer.Exit(code = 1) from None
@app.command()
def crack(
text: Annotated[
str | None,
typer.Argument(help = "Text to crack (or use --input-file or stdin)"),
] = None,
input_file: Annotated[
Path | None,
typer.Option("--input-file",
"-i",
help = "Input file path")] = None,
top: Annotated[int,
typer.Option("--top",
"-t",
help = "Show top N candidates")] = 5,
show_all: Annotated[
bool,
typer.Option("--all",
"-a",
help = "Show all 26 possible shifts")] = False,
) -> None:
"""
Brute-force decrypt text by trying all shifts with frequency analysis ranking
"""
try:
ciphertext = read_input(text, input_file)
candidates = CaesarCipher.crack(ciphertext)
analyzer = FrequencyAnalyzer()
ranked = analyzer.rank_candidates(candidates)
table = Table(title = "Caesar Cipher Brute Force Results")
table.add_column("Rank", style = "cyan", justify = "right")
table.add_column("Shift", style = "magenta", justify = "right")
table.add_column("Score", style = "yellow", justify = "right")
table.add_column("Decrypted Text", style = "green")
display_count = len(ranked) if show_all else min(top, len(ranked))
for rank, (shift, text_result, score) in enumerate(ranked[:display_count], 1):
table.add_row(
str(rank),
str(shift),
f"{score:.2f}",
text_result[: 80]
)
console.print(table)
console.print(
f"\n[bold]Best match (Shift {ranked[0][0]}):[/bold] {ranked[0][1]}"
)
except (ValueError, OSError) as e:
console.print(f"[red]Error:[/red] {e}")
raise typer.Exit(code = 1) from None
if __name__ == "__main__":
app()