""" Experiment runner - wraps existing cryptanalysis code with progress reporting and returns base64 plots instead of saving to disk. """ import sys, os, io, base64, random import numpy as np import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt import seaborn as sns import pandas as pd import torch from torch.utils.data import DataLoader, TensorDataset from sklearn.metrics import confusion_matrix ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, ROOT) from cipher_implementations.ciphers import get_all_cipher_names, get_cipher from dataset_generation.generate_dataset import generate_dataset from input_representations.data_processing import prepare_representation from ml_models.models import MLP, CNN, SiameseNet, MINE from distinguisher_experiments.train_evaluate import train_model DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu') REP_LABELS = {1:"Raw",2:"Diff",3:"Concat",4:"Bit-Slice",5:"Word", 6:"Intermed",7:"Noisy",8:"Joint P-C",9:"Stats",10:"Sequential"} DARK_BG = '#0d1117' DARK_AX = '#161b22' NEON_GRN = '#00f5a0' NEON_PRP = '#7b2fff' TEXT_CLR = '#e8eaf6' def _setup_dark_fig(w=12, h=6): fig, ax = plt.subplots(figsize=(w, h)) fig.patch.set_facecolor(DARK_BG) ax.set_facecolor(DARK_AX) ax.tick_params(colors=TEXT_CLR) ax.xaxis.label.set_color(TEXT_CLR) ax.yaxis.label.set_color(TEXT_CLR) ax.title.set_color(TEXT_CLR) for spine in ax.spines.values(): spine.set_edgecolor('#30363d') return fig, ax def _fig_to_b64(fig): buf = io.BytesIO() fig.savefig(buf, format='png', dpi=120, bbox_inches='tight', facecolor=DARK_BG) buf.seek(0) data = base64.b64encode(buf.read()).decode() plt.close(fig) return f"data:image/png;base64,{data}" def _log(job, msg, progress=None): evt = {"type": "log", "message": msg} if progress is not None: evt["progress"] = progress job.log_queue.put(evt) def _gen_eval(job, cipher, rounds, rep_id, model_type, n_samples, epochs): _log(job, f"📊 Generating {n_samples} samples for {cipher.upper()} R={rounds}...") ds = generate_dataset(cipher, num_samples=n_samples, rounds=rounds, include_intermediates=(rep_id in [6,10])) _log(job, f"🔧 Preparing representation: {REP_LABELS.get(rep_id, rep_id)}...") data = prepare_representation(ds, rep_id, block_size=ds['block_size']) labels = ds['labels'] sp = int(0.8 * len(data)) tl = DataLoader(TensorDataset(torch.tensor(data[:sp], dtype=torch.float32), torch.tensor(labels[:sp], dtype=torch.float32)), batch_size=256, shuffle=True) vl = DataLoader(TensorDataset(torch.tensor(data[sp:], dtype=torch.float32), torch.tensor(labels[sp:], dtype=torch.float32)), batch_size=256, shuffle=False) ishape = data.shape[1:] mt = model_type if mt == 'MLP': m = MLP(int(np.prod(ishape)), hidden_dim=128) elif mt == 'CNN': ch = 1 if len(ishape)==1 else ishape[0] ln = ishape[0] if len(ishape)==1 else ishape[1] if ch > ln: ch, ln = ln, ch m = CNN(input_channels=ch, seq_len=ln) elif mt == 'SiameseNet': if len(ishape)>1 and data.shape[1]==2: m = SiameseNet(branch_dim=data.shape[2]) else: m = MLP(int(np.prod(ishape))); mt='MLP' elif mt == 'MINE': if len(ishape)>1 and data.shape[1]==2: m = MINE(x_dim=data.shape[2], y_dim=data.shape[2]) else: m = MLP(int(np.prod(ishape))); mt='MLP' else: m = MLP(int(np.prod(ishape))) _log(job, f"🧠 Training {mt} ({epochs} epochs)...") acc = train_model(m, tl, vl, model_type=mt, epochs=epochs) _log(job, f"✅ Accuracy: {acc:.4f}") m.eval(); yt, yp = [], [] with torch.no_grad(): for d, l in vl: d = d.to(DEVICE) if mt=='MINE': pr = (m(d[:,0,:],d[:,1,:]).squeeze()>0).float().cpu() else: pr = (torch.sigmoid(m(d))>0.5).float().cpu() yt.extend(l.numpy()); yp.extend(pr.numpy()) return acc, np.array(yt), np.array(yp) # ── Experiment 1: Representation Analysis ───────────────────────────────────── def run_rep_analysis(job, ciphers, models, rounds, rep_ids, n_samples, epochs): results, total = [], len(ciphers)*len(models)*len(rounds)*len(rep_ids) done = 0 for c in ciphers: for mt in models: for r in rounds: for rep in rep_ids: _log(job, f"⚙️ {c.upper()} | {mt} | R={r} | Rep={REP_LABELS.get(rep,rep)}", int(done/total*90)) acc,_,_ = _gen_eval(job,c,r,rep,mt,n_samples,epochs) results.append({'Representation':REP_LABELS.get(rep,str(rep)), 'Accuracy':acc, 'Config':f"{c.upper()}|{mt}|R{r}"}) done += 1 df = pd.DataFrame(results) pal = sns.color_palette("cool", len(df['Config'].unique())) fig, ax = _setup_dark_fig(14, 7) sns.barplot(data=df, x='Representation', y='Accuracy', hue='Config', palette=pal, ax=ax) ax.axhline(0.5, color='#ff4f7b', linestyle='--', lw=1.5, label='Random (0.50)') ax.set_title('Input Representation Analysis', fontsize=14, color=TEXT_CLR) ax.set_ylim(0.4, 1.0) ax.legend(bbox_to_anchor=(1.02,1), loc='upper left', framealpha=0.2, labelcolor=TEXT_CLR, facecolor=DARK_AX) plt.xticks(rotation=30) plt.tight_layout() return {"plot": _fig_to_b64(fig), "data": results} # ── Experiment 2: Model Comparison ──────────────────────────────────────────── def run_model_comparison(job, ciphers, models, rounds, rep_ids, n_samples, epochs): results, total = [], len(ciphers)*len(rounds)*len(rep_ids)*len(models) done = 0 for c in ciphers: for r in rounds: for rep in rep_ids: for mt in models: _log(job, f"⚙️ {c.upper()} | {mt} | R={r} | Rep{rep}", int(done/total*90)) acc,_,_ = _gen_eval(job,c,r,rep,mt,n_samples,epochs) results.append({'Model':mt,'Accuracy':acc, 'Config':f"{c.upper()}|R{r}|Rep{rep}"}) done += 1 df = pd.DataFrame(results) pal = [NEON_GRN, NEON_PRP, '#00cfff', '#ff4f7b'] fig, ax = _setup_dark_fig(10, 6) sns.barplot(data=df, x='Model', y='Accuracy', hue='Config', palette='cool', ax=ax) ax.axhline(0.5, color='#ff4f7b', linestyle='--', lw=1.5, label='Random (0.50)') ax.set_title('Model Architecture Comparison', fontsize=14, color=TEXT_CLR) ax.set_ylim(0.4, 1.0) ax.legend(bbox_to_anchor=(1.02,1), loc='upper left', framealpha=0.2, labelcolor=TEXT_CLR, facecolor=DARK_AX) plt.tight_layout() return {"plot": _fig_to_b64(fig), "data": results} # ── Experiment 3: Round Analysis ────────────────────────────────────────────── def run_round_analysis(job, ciphers, models, rep_ids, round_list, n_samples, epochs): results, total = [], len(ciphers)*len(models)*len(rep_ids)*len(round_list) done = 0 for c in ciphers: for mt in models: for rep in rep_ids: for r in round_list: _log(job, f"⚙️ {c.upper()} | {mt} | R={r}", int(done/total*90)) acc,_,_ = _gen_eval(job,c,r,rep,mt,n_samples,epochs) results.append({'Round':r,'Accuracy':acc, 'Config':f"{c.upper()}|{mt}|Rep{rep}"}) done += 1 df = pd.DataFrame(results) fig, ax = _setup_dark_fig(12, 6) for cfg, grp in df.groupby('Config'): ax.plot(grp['Round'], grp['Accuracy'], marker='o', lw=2, label=cfg) ax.axhline(0.5, color='#ff4f7b', linestyle='--', lw=1.5, label='Random (0.50)') ax.set_title('Distinguisher Accuracy vs Rounds', fontsize=14, color=TEXT_CLR) ax.set_xlabel('Rounds', color=TEXT_CLR) ax.set_ylabel('Accuracy', color=TEXT_CLR) ax.set_ylim(0.4, 1.0) ax.legend(bbox_to_anchor=(1.02,1), loc='upper left', framealpha=0.2, labelcolor=TEXT_CLR, facecolor=DARK_AX) plt.tight_layout() return {"plot": _fig_to_b64(fig), "data": results} # ── Experiment 4: Confusion Matrix ──────────────────────────────────────────── def run_confusion_matrix(job, ciphers, model_type, rep_id, rounds, n_samples, epochs): n = len(ciphers) cols = min(n, 3); rows = (n+cols-1)//cols fig, axes = plt.subplots(rows, cols, figsize=(6*cols, 5*rows), squeeze=False) fig.patch.set_facecolor(DARK_BG) for idx, c in enumerate(ciphers): r, col = divmod(idx, cols) ax = axes[r][col] _log(job, f"🔢 Confusion matrix: {c.upper()} | {model_type} | R={rounds}", int(idx/n*90)) acc, yt, yp = _gen_eval(job,c,rounds,rep_id,model_type,n_samples,epochs) cm = confusion_matrix(yt, yp) sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=['Pred Random','Pred Cipher'], yticklabels=['True Random','True Cipher'], ax=ax) ax.set_title(f"{c.upper()} (Acc={acc:.3f})", color=TEXT_CLR) ax.set_facecolor(DARK_AX) for idx in range(n, rows*cols): r, col = divmod(idx, cols) axes[r][col].set_visible(False) plt.suptitle(f"Confusion Matrices | {model_type} | Rep{rep_id} | R={rounds}", color=TEXT_CLR, fontsize=13) plt.tight_layout() return {"plot": _fig_to_b64(fig)} # ── Dataset Distribution ─────────────────────────────────────────────────────── def run_dataset_distribution(job, cipher_name, rounds, n_samples): _log(job, f"📊 Generating {n_samples} samples for {cipher_name.upper()}...") ds = generate_dataset(cipher_name, num_samples=n_samples, rounds=rounds) C, Cp = ds['C'], ds['C_prime'] bs = ds['block_size'] dC = C ^ Cp hw_fn = np.vectorize(lambda x: bin(int(x) & ((1< best_score: best_score = score; best_dp = dp fig, ax = _setup_dark_fig(10, 5) xs = [t['trial'] for t in trial_log] ys = [t['score'] for t in trial_log] ax.plot(xs, ys, color=NEON_GRN, lw=2, label='Trial Score') ax.axhline(0.5, color='#ff4f7b', linestyle='--', lw=1.5, label='Random') ax.set_title(f'Difference Search — {cipher_name.upper()} R={rounds}', color=TEXT_CLR) ax.set_xlabel('Trial', color=TEXT_CLR); ax.set_ylabel('Score', color=TEXT_CLR) ax.legend(framealpha=0.2, labelcolor=TEXT_CLR, facecolor=DARK_AX) plt.tight_layout() return {"plot": _fig_to_b64(fig), "best_dp": hex(best_dp), "best_score": round(best_score,4), "trials": trial_log} # ── Bonus 2: Classical vs ML ────────────────────────────────────────────────── def run_classical_comparison(job, cipher_name, rounds, n_samples): import random as pyrandom c = get_cipher(cipher_name, rounds=rounds) bs = min(c.BLOCK_SIZE, 64); mask = (1<> (bs-1-i)) & 1 for i in range(bs)] test_pairs.append(bits) tp = torch.tensor(test_pairs, dtype=torch.float32) with torch.no_grad(): s = torch.sigmoid(dist_model(tp)).mean().item() scores.append(s) best_guess = int(np.argmax(scores)) _log(job, f"✅ Best guess: 0x{best_guess:02x} True: 0x{true_key:02x} " f"Match: {'YES ✅' if best_guess==true_key else 'NO ❌'}", 95) fig, ax = _setup_dark_fig(12, 5) ax.bar(range(256), scores, color=NEON_PRP, alpha=0.6) ax.bar(best_guess, scores[best_guess], color=NEON_GRN, label=f'Best guess 0x{best_guess:02x}') ax.bar(true_key, scores[true_key], color='#ff4f7b', alpha=0.8, label=f'True key 0x{true_key:02x}') ax.set_title(f'Key Recovery — {cipher_name.upper()} R={rounds}', color=TEXT_CLR) ax.set_xlabel('Partial Subkey (8-bit)', color=TEXT_CLR) ax.set_ylabel('Distinguisher Score', color=TEXT_CLR) ax.legend(framealpha=0.2, labelcolor=TEXT_CLR, facecolor=DARK_AX) plt.tight_layout() return {"plot": _fig_to_b64(fig), "best_guess": f"0x{best_guess:02x}", "true_key": f"0x{true_key:02x}", "match": best_guess == true_key, "scores": [round(s,4) for s in scores]}