174 lines
5.2 KiB
Python
174 lines
5.2 KiB
Python
"""
|
|
ⒸAngelaMos | 2026
|
|
main.py
|
|
"""
|
|
|
|
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()
|