crack.py
#!/usr/bin/env python3
"""
==============================================================================
Ultimate D'Agapeyeff Cipher Solver (Parallel Memetic Algorithm)
==============================================================================
Author: Cryptography Research Community (Synthesized & Optimized)
Year: 2026
Architecture:
- Outer Loop: Genetic/Population representation across Multiprocessing workers.
- Inner Loop: Iterated Local Search (Simulated Annealing) for rapid convergence.
- Search Space: Double Columnar Transposition (14!) x Polybius Square (25!)
- Optimization: Pre-computed index mapping replaces string dictionary lookups.
==============================================================================
"""
import math
import random
import time
import sys
import os
import multiprocessing as mp
import signal
# ============================================================================
# CIPHERTEXT CONFIGURATION
# ============================================================================
# Standard 395-digit D'Agapeyeff Cipher.
# Theory dictates truncating the trailing '000' yields exactly 196 pairs (14x14 grid)
RAW_CIPHER = (
"756282859162916481649174858464747482848381638181747482626475838284917574658375"
"757593636565816381758575756462829285746382757483816581848564856485856382726283"
"628181728164637582816483638285816363630474819191846385846564856562946262859185"
"917491727564657571658362647481828462826491819365626484849183857491816572748383"
"858283646272626562837592726382827272838285847582818372846282837581647574858162"
"92000"
)
GRID_SIZE = 14
ROWS =['6', '7', '8', '9', '0']
COLS =['1', '2', '3', '4', '5']
ALPHABET = "ABCDEFGHIKLMNOPQRSTUVWXYZ" # 25 letters (I/J combined)
# ============================================================================
# QUADGRAM & FITNESS SCORING
# ============================================================================
def load_quadgrams(filename="english_quadgrams.txt"):
"""Load quadgrams from file, or use a robust built-in fallback."""
quadgrams = {}
if os.path.exists(filename):
try:
total = 0
with open(filename, 'r') as f:
for line in f:
q, c = line.strip().split()
c = int(c)
quadgrams[q] = c
total += c
for q in quadgrams:
quadgrams[q] = math.log10(quadgrams[q] / total)
floor = math.log10(0.01 / total)
print(f"[+] Loaded {len(quadgrams)} quadgrams from {filename}")
return quadgrams, floor
except Exception as e:
print(f"[-] Error loading {filename}: {e}. Using fallback.")
# --- Robust Fallback for standalone execution ---
common =[
('TION', -2.1), ('NTHE', -2.2), ('THER', -2.3), ('THAT', -2.3),
('OFTH', -2.4), ('FTHE', -2.4), ('THES', -2.5), ('WITH', -2.5),
('INTH', -2.5), ('ATIO', -2.6), ('OTHE', -2.6), ('TTHE', -2.7),
('DTHE', -2.7), ('INGT', -2.8), ('ETHE', -2.8), ('SAND', -2.8),
('STHE', -2.8), ('HERE', -2.9), ('THEC', -2.9), ('MENT', -2.9),
('THIS', -2.9), ('OULD', -3.0), ('IGHT', -3.0), ('HAVE', -3.0),
('WHIC', -3.1), ('FROM', -3.1), ('CODE', -3.1), ('CIPH', -3.2)
]
for q, score in common:
quadgrams[q] = score
print("[!] Using built-in approximation quadgrams.")
return quadgrams, -5.0
def fast_score(text, quadgrams, floor):
"""Highly optimized text scoring function."""
score = 0.0
# Quadgram scoring
for i in range(len(text) - 3):
score += quadgrams.get(text[i:i+4], floor)
# English word heuristics bonuses
if "THE" in text: score += 1.5
if "AND" in text: score += 1.0
if "THAT" in text: score += 2.0
if "WITH" in text: score += 2.0
if "CIPHER" in text: score += 5.0
if "CODE" in text: score += 5.0
# Penalize extreme letter repeats
max_run = 1
current_run = 1
for i in range(1, len(text)):
if text[i] == text[i-1]:
current_run += 1
max_run = max(max_run, current_run)
else:
current_run = 1
if max_run > 4:
score -= (max_run * 2.0)
return score
# ============================================================================
# FAST INDEX-BASED TRANSPOSITION & POLYBIUS
# ============================================================================
def prepare_cipher(raw_text):
"""Converts the raw cipher into integer indices (0-24) for O(1) lookups."""
# Truncate the '000' padding at the end to fit the perfect 196 pair grid
clean = raw_text.replace(" ", "")
if clean.endswith("000"):
clean = clean[:-3]
pairs = [clean[i:i+2] for i in range(0, len(clean), 2)]
indices =[]
for p in pairs:
try:
r = ROWS.index(p[0])
c = COLS.index(p[1])
indices.append(r * 5 + c)
except ValueError:
indices.append(-1) # Handle invalid pairs gracefully
return indices
def double_transpose(indices, row_key, col_key):
"""Fast 1D array double transposition (Row & Column)."""
out = [0] * (GRID_SIZE * GRID_SIZE)
idx = 0
for col in col_key:
for row in row_key:
out[idx] = indices[row * GRID_SIZE + col]
idx += 1
return out
# ============================================================================
# SIMULATED ANNEALING WORKER
# ============================================================================
def init_worker():
# Ignore Ctrl+C in child processes so main handles it gracefully
signal.signal(signal.SIGINT, signal.SIG_IGN)
def annealing_worker(args):
"""Inner loop: Runs Iterated Local Search (ILS)."""
base_indices, quadgrams, floor, iterations = args
# Random initial state
curr_row_key = list(range(GRID_SIZE))
curr_col_key = list(range(GRID_SIZE))
curr_square = list(ALPHABET)
random.shuffle(curr_row_key)
random.shuffle(curr_col_key)
random.shuffle(curr_square)
# Score initial state
transposed = double_transpose(base_indices, curr_row_key, curr_col_key)
text = "".join(curr_square[idx] if idx != -1 else '?' for idx in transposed)
curr_score = fast_score(text, quadgrams, floor)
# Best tracking
best_row_key = curr_row_key[:]
best_col_key = curr_col_key[:]
best_square = curr_square[:]
best_score = curr_score
best_text = text
temp = 50.0
cooling = 0.9995
for i in range(iterations):
# 1. Mutate State
new_row_key = curr_row_key[:]
new_col_key = curr_col_key[:]
new_square = curr_square[:]
mutation_type = random.random()
if mutation_type < 0.3:
# Mutate Column Transposition
a, b = random.sample(range(GRID_SIZE), 2)
new_col_key[a], new_col_key[b] = new_col_key[b], new_col_key[a]
elif mutation_type < 0.6:
# Mutate Polybius Square
a, b = random.sample(range(25), 2)
new_square[a], new_square[b] = new_square[b], new_square[a]
else:
# Mutate Row Transposition
a, b = random.sample(range(GRID_SIZE), 2)
new_row_key[a], new_row_key[b] = new_row_key[b], new_row_key[a]
# 2. Evaluate
transposed = double_transpose(base_indices, new_row_key, new_col_key)
text = "".join(new_square[idx] if idx != -1 else '?' for idx in transposed)
score = fast_score(text, quadgrams, floor)
# 3. Metropolis Acceptance Criterion
delta = score - curr_score
if delta > 0 or (temp > 0.001 and random.random() < math.exp(delta / temp)):
curr_row_key = new_row_key
curr_col_key = new_col_key
curr_square = new_square
curr_score = score
if score > best_score:
best_row_key = curr_row_key[:]
best_col_key = curr_col_key[:]
best_square = curr_square[:]
best_score = score
best_text = text
# 4. Cooling & ILS Reheating
temp *= cooling
if temp < 0.05:
temp = 20.0 # Reheat to escape local minimum
return best_score, best_row_key, best_col_key, best_square, best_text
# ============================================================================
# MAIN ORCHESTRATOR
# ============================================================================
def display_polybius(square):
"""Format the Polybius square beautifully."""
print(" 1 2 3 4 5")
print(" +---+---+---+---+---+")
for r_idx, r_label in enumerate(ROWS):
row_str = f" {r_label} |"
for c_idx in range(5):
row_str += f" {square[r_idx * 5 + c_idx]} |"
print(row_str)
print(" +---+---+---+---+---+")
def crack_cipher():
print("=" * 75)
print(" ULTIMATE D'AGAPEYEFF CIPHER SOLVER (PARALLEL MEMETIC ALGORITHM) ")
print("=" * 75)
base_indices = prepare_cipher(RAW_CIPHER)
quadgrams, floor = load_quadgrams()
workers = mp.cpu_count()
print(f"[+] Launching {workers} parallel workers...")
print(f"[+] Search space: 14! x 14! x 25! (Double Transposition + Polybius)")
print("=" * 75)
pool = mp.Pool(workers, initializer=init_worker)
best_overall_score = -float('inf')
start_time = time.time()
generation = 1
try:
while True:
# Dispatch generation (50,000 inner loops per worker)
jobs =[(base_indices, quadgrams, floor, 50000) for _ in range(workers)]
results = pool.map(annealing_worker, jobs)
# Process results
for score, rk, ck, sq, text in results:
if score > best_overall_score:
best_overall_score = score
elapsed = int(time.time() - start_time)
# 1. Print to console
print(f"\n[*** NEW GLOBAL BEST FOUND ***] - Gen {generation} | Elapsed: {elapsed}s")
print(f"Fitness Score: {score:.4f}")
print(f"Row Key: {rk}")
print(f"Col Key: {ck}")
print("\nPolybius Square:")
display_polybius(sq)
print("\nDecoded Snippet:")
print(f"--> {text[:100]}...\n")
# 2. Save to file so you don't lose it!
with open("daga_best_finds.txt", "a", encoding="utf-8") as f:
f.write(f"\n[{time.strftime('%Y-%m-%d %H:%M:%S')}] Gen {generation} | Score: {score:.4f}\n")
f.write(f"Row Key: {rk}\nCol Key: {ck}\n")
f.write(f"Square: {''.join(sq)}\n")
f.write(f"Text: {text}\n")
f.write("-" * 50 + "\n")
generation += 1
# Non-obtrusive alive ping
if generation % 10 == 0:
elapsed = int(time.time() - start_time)
sys.stdout.write(f"\r[~] Running Generation {generation} (Elapsed: {elapsed}s). Best: {best_overall_score:.4f}")
sys.stdout.flush()
except KeyboardInterrupt:
print("\n\n[!] Interrupted by user. Shutting down gracefully...")
pool.terminate()
pool.join()
print("[+] Final Best Score achieved:", best_overall_score)
if __name__ == "__main__":
# Guard ensures multiprocessing works securely on Windows/MacOS
crack_cipher()