diff --git a/app/substitution-breaker/page.tsx b/app/substitution-breaker/page.tsx new file mode 100644 index 00000000..9e8c5f5e --- /dev/null +++ b/app/substitution-breaker/page.tsx @@ -0,0 +1,22 @@ +import type { Metadata } from "next"; +import SubstitutionBreaker from "../../components/cryptanalysis/SubstitutionBreaker"; +import Navbar from "../../components/layout/Navbar"; +import Footer from "../../components/layout/footer"; + +export const metadata: Metadata = { + title: "Substitution Cipher Breaker — Automated Cryptanalysis | CryptoViz", + description: + "Automatically crack monoalphabetic substitution ciphers using frequency analysis and hill climbing with simulated annealing. Interactive educational cryptanalysis tool.", +}; + +export default function SubstitutionBreakerPage() { + return ( +
+ +
+ +
+
+ ); +} diff --git a/components/cryptanalysis/SubstitutionBreaker.tsx b/components/cryptanalysis/SubstitutionBreaker.tsx new file mode 100644 index 00000000..152c6fcf --- /dev/null +++ b/components/cryptanalysis/SubstitutionBreaker.tsx @@ -0,0 +1,721 @@ +"use client"; + +import { useState, useCallback, useRef, useMemo } from "react"; +import { + breakSubstitution, + frequencyAnalysisSeed, + applyKey, + buildKeyMapping, + scoreKey, + randomKey, + identityKey, + type BreakerResult, + type SubstitutionKey, + type ConvergencePoint, + DEFAULT_CONFIG, +} from "../../lib/cryptanalysis/substitutionBreaker"; +import { + Play, + Pause, + RotateCcw, + Zap, + Target, + BarChart3, + Key, + ArrowRight, + ChevronDown, + ChevronUp, + AlertTriangle, + Clock, + Brain, + Check, + Copy, +} from "lucide-react"; +import { cn } from "../../lib/utils"; + +/* ─── Sample Ciphertexts ──────────────────────────────────────────────────── */ + +const SAMPLES = [ + { + label: "Simple (short)", + ciphertext: "GUVF VF N GRFG", + hint: "Caesar shift — frequency seed can crack it instantly.", + }, + { + label: "Medium length", + ciphertext: + "XJCRU JWQCU YQTFI YJ QJTWF YMJ JWQ XZJI JSYJW JXJQJW YMFY JWSJ YMJ JFQUJW YMJ QJFWI JSYJW JXJQJW YMFY JWSJ YMJ JFQUJW", + hint: "Longer text gives better frequency analysis.", + }, + { + label: "Substitution cipher", + ciphertext: + "KZ BRXU VKRSC YLBYB RXU YLBYB RXU KZ YBFX VRZZ YBFX YLBYB RXU KZ YBFX VRZZ", + hint: "General monoalphabetic substitution — needs hill climbing.", + }, + { + label: "Block of English", + ciphertext: + "THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG", + hint: "Already plaintext — the key is the identity mapping.", + }, +]; + +/* ─── Key Mapping Visual ───────────────────────────────────────────────────── */ + +function KeyMappingVisual({ + key, + label, + compact = false, +}: { + key: SubstitutionKey; + label?: string; + compact?: boolean; +}) { + const mapping = buildKeyMapping(key); + return ( +
+ {label && ( +

+ {label} +

+ )} +
+ {mapping.map((m) => ( + + {m.cipher} + + {m.plain} + + + ))} +
+
+ ); +} + +/* ─── Convergence Chart ────────────────────────────────────────────────────── */ + +function ConvergenceChart({ + history, +}: { + history: ConvergencePoint[]; +}) { + if (history.length < 2) return null; + + const scores = history.map((h) => h.score); + const minScore = Math.min(...scores); + const maxScore = Math.max(...scores); + const range = maxScore - minScore || 1; + + const svgWidth = 600 + const svgHeight = 120 + const padding = { top: 10, right: 10, bottom: 20, left: 50 } + const plotWidth = svgWidth - padding.left - padding.right + const plotHeight = svgHeight - padding.top - padding.bottom + + const points = history.map((h, i) => ({ + x: padding.left + (i / (history.length - 1)) * plotWidth, + y: padding.top + plotHeight - ((h.score - minScore) / range) * plotHeight, + })) + + const pathD = points.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`).join(" ") + + return ( +
+ + {/* Grid lines */} + {[0, 0.25, 0.5, 0.75, 1].map((frac) => { + const y = padding.top + plotHeight * (1 - frac) + const val = minScore + range * frac + return ( + + + + {val.toFixed(0)} + + + ) + })} + + {/* Line */} + + + {/* End dot */} + {points.length > 0 && ( + + )} + + {/* X axis label */} + + Iteration + + +
+ ); +} + +/* ─── Candidate Result Card ────────────────────────────────────────────────── */ + +function CandidateCard({ + result, + rank, + isExpanded, + onToggle, +}: { + result: { + key: SubstitutionKey; + plaintext: string; + score: number; + restartIndex: number; + iterations: number; + converged: boolean; + }; + rank: number; + isExpanded: boolean; + onToggle: () => void; +}) { + const [copied, setCopied] = useState(false); + + const handleCopy = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + navigator.clipboard.writeText(result.plaintext); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + }, + [result.plaintext] + ); + + return ( +
+ + {isExpanded ? ( + + ) : ( + + )} + + {isExpanded && ( +
+ {/* Full plaintext */} +
+

+ Full Decrypted Text +

+

+ {result.plaintext} +

+
+ {/* Key mapping */} + +
+ )} +
+ ); +} + +/* ─── Main Component ──────────────────────────────────────────────────────── */ + +export default function SubstitutionBreaker() { + const [ciphertext, setCiphertext] = useState( + "KZ BRXU VKRSC YLBYB RXU YLBYB RXU KZ YBFX VRZZ YBFX YLBYB RXU KZ YBFX VRZZ" + ); + const [result, setResult] = useState(null); + const [isRunning, setIsRunning] = useState(false); + const [expandedIndex, setExpandedIndex] = useState(0); + const [convergenceHistory, setConvergenceHistory] = useState([]); + const abortRef = useRef(null); + + const initialKey = useMemo(() => { + if (!ciphertext.trim()) return identityKey(); + return frequencyAnalysisSeed(ciphertext); + }, [ciphertext]); + + const initialPlaintext = useMemo(() => { + return applyKey(ciphertext, initialKey); + }, [ciphertext, initialKey]); + + const handleBreak = useCallback(() => { + if (!ciphertext.trim()) return; + setIsRunning(true); + setConvergenceHistory([]); + + // Run synchronously (non-blocking for short texts, runs in main thread) + requestAnimationFrame(() => { + const res = breakSubstitution(ciphertext, DEFAULT_CONFIG, (point) => { + setConvergenceHistory((prev) => [...prev, point]); + }); + setResult(res); + setExpandedIndex(0); + setIsRunning(false); + }); + }, [ciphertext]); + + const handleReset = useCallback(() => { + setResult(null); + setConvergenceHistory([]); + setExpandedIndex(null); + setIsRunning(false); + }, []); + + const handleSample = useCallback((text: string) => { + setCiphertext(text); + setResult(null); + setConvergenceHistory([]); + setExpandedIndex(null); + }, []); + + const keyMapping = useMemo( + () => (result ? buildKeyMapping(result.best.key) : []), + [result] + ); + + return ( +
+ {/* Header Banner */} +
+
+
+

+ Cryptanalysis Tool +

+
+
+

+ Substitution Cipher Breaker +

+

+ Automatically crack monoalphabetic substitution ciphers using frequency analysis for the initial seed, then hill climbing with simulated annealing to refine the key mapping. Works on any fixed-letter-substitution cipher. +

+
+
+

+ How It Works +

+

+ The breaker maps the most frequent ciphertext letters to expected English frequencies, then iteratively improves the mapping by swapping letter assignments and scoring with quadgram log-likelihood. +

+
+
+
+
+ + {/* Sample Ciphertexts */} +
+

+ Quick Examples — Click to Load +

+
+ {SAMPLES.map((sample) => ( + + ))} +
+
+ +
+ {/* Left Column: Input & Controls */} +
+ {/* Input Section */} +
+

+ Ciphertext Input +

+

+ Paste a monoalphabetic substitution cipher for automated breaking. +

+
+