516 lines
22 KiB
Python
516 lines
22 KiB
Python
"""
|
|
Lab 3: Modos de operacion AES - herramienta pedagogica
|
|
|
|
Pestana 1 ECB vs CBC ........ patron visual, entropia de Shannon e histograma
|
|
Pestana 2 Maleabilidad ...... bit-flipping sobre CBC, y por que GCM lo detiene
|
|
Pestana 3 Reuso de nonce .... dos imagenes con el mismo nonce en CTR
|
|
|
|
Ninguna de estas construcciones debe usarse en produccion: claves fijas,
|
|
IV predecibles y ausencia de autenticacion son parte de la demostracion.
|
|
"""
|
|
|
|
import math
|
|
import tkinter as tk
|
|
from collections import Counter
|
|
from tkinter import filedialog, messagebox, ttk
|
|
|
|
from Crypto.Cipher import AES
|
|
from Crypto.Random import get_random_bytes
|
|
from Crypto.Util.Padding import pad
|
|
from PIL import Image, ImageTk
|
|
|
|
# --- Parametros ajustables -------------------------------------------------
|
|
IMG_SIZE = 400 # lado de las imagenes de la pestana 1 (px)
|
|
SMALL_SIZE = 280 # lado de las imagenes de la pestana 3 (px)
|
|
HIST_H = 55 # alto del histograma (px)
|
|
BLOCK = AES.block_size # 16 bytes
|
|
STEP_BYTES = IMG_SIZE * 3 * 2 # avance por frame de animacion (2 filas RGB)
|
|
FRAME_DELAY = 10 # ms entre frames
|
|
|
|
KEY = b'Sixteen byte key' # clave ASCII de baja entropia: SOLO demostracion
|
|
IV_FIJO = b'Sixteen byte iv.'
|
|
|
|
|
|
# --- Metricas y utilidades -------------------------------------------------
|
|
def shannon_entropy(data: bytes) -> float:
|
|
"""Entropia de Shannon en bits/byte. Maximo teorico: 8.0"""
|
|
if not data:
|
|
return 0.0
|
|
n = len(data)
|
|
return -sum((c / n) * math.log2(c / n) for c in Counter(data).values())
|
|
|
|
|
|
def block_stats(data: bytes):
|
|
"""Total, unicos y repetidos de bloques de 16 bytes."""
|
|
usable = len(data) - (len(data) % BLOCK)
|
|
blocks = [data[i:i + BLOCK] for i in range(0, usable, BLOCK)]
|
|
total = len(blocks)
|
|
unique = len(set(blocks))
|
|
return total, unique, total - unique
|
|
|
|
|
|
def format_metrics(data: bytes) -> str:
|
|
h = shannon_entropy(data)
|
|
total, unique, repeated = block_stats(data)
|
|
pct = (repeated / total * 100) if total else 0.0
|
|
return (f"Entropia Shannon: {h:.4f} / 8.0 bits/byte\n"
|
|
f"Bloques 16B: {total:,} | unicos: {unique:,}\n"
|
|
f"Repetidos: {repeated:,} ({pct:.1f}%)")
|
|
|
|
|
|
def xor_bytes(a: bytes, b: bytes) -> bytes:
|
|
return bytes(x ^ y for x, y in zip(a, b))
|
|
|
|
|
|
def printable(data: bytes) -> str:
|
|
"""Bytes no imprimibles se muestran como punto medio."""
|
|
return ''.join(chr(c) if 32 <= c < 127 else '.' for c in data)
|
|
|
|
|
|
def draw_histogram(canvas, data, color="#2c6fbb"):
|
|
"""Histograma de los 256 valores de byte, dibujado en un Canvas."""
|
|
canvas.delete("all")
|
|
w = int(canvas.cget("width"))
|
|
h = int(canvas.cget("height"))
|
|
if not data:
|
|
return
|
|
counts = Counter(data)
|
|
max_c = max(counts.values())
|
|
bar_w = w / 256.0
|
|
for b in range(256):
|
|
c = counts.get(b, 0)
|
|
if not c:
|
|
continue
|
|
bh = (c / max_c) * (h - 2)
|
|
x0 = b * bar_w
|
|
canvas.create_rectangle(x0, h - bh, x0 + bar_w, h,
|
|
fill=color, outline="")
|
|
canvas.create_line(0, h - 1, w, h - 1, fill="#888")
|
|
|
|
|
|
# ===========================================================================
|
|
class AESLab:
|
|
def __init__(self, root):
|
|
self.root = root
|
|
self.root.title("Lab 3: Modos de Operacion AES")
|
|
self.root.geometry("1380x800")
|
|
|
|
nb = ttk.Notebook(root)
|
|
nb.pack(fill="both", expand=True, padx=8, pady=6)
|
|
|
|
self.tab_modes = tk.Frame(nb)
|
|
self.tab_flip = tk.Frame(nb)
|
|
self.tab_ctr = tk.Frame(nb)
|
|
nb.add(self.tab_modes, text=" 1. ECB vs CBC ")
|
|
nb.add(self.tab_flip, text=" 2. Maleabilidad de CBC ")
|
|
nb.add(self.tab_ctr, text=" 3. Reuso de nonce (CTR) ")
|
|
|
|
self.build_tab_modes()
|
|
self.build_tab_flip()
|
|
self.build_tab_ctr()
|
|
|
|
# =======================================================================
|
|
# PESTANA 1: ECB vs CBC
|
|
# =======================================================================
|
|
def build_tab_modes(self):
|
|
t = self.tab_modes
|
|
self.img_bytes_raw = None
|
|
self.original_img = None
|
|
self.busy = False
|
|
self.random_iv = tk.BooleanVar(value=False)
|
|
|
|
panels = tk.Frame(t)
|
|
panels.pack(pady=8)
|
|
self.p_orig = self.make_panel(panels, "Original (plaintext)", IMG_SIZE)
|
|
self.p_ecb = self.make_panel(panels, "ECB - INSEGURO", IMG_SIZE)
|
|
self.p_cbc = self.make_panel(panels, "CBC - sin patron visible", IMG_SIZE)
|
|
|
|
ctrl = tk.Frame(t)
|
|
ctrl.pack(pady=6)
|
|
self.btn_load = tk.Button(ctrl, text="1. Cargar Imagen",
|
|
command=self.load_image)
|
|
self.btn_load.pack(side=tk.LEFT, padx=8)
|
|
self.btn_ecb = tk.Button(ctrl, text="2. Ejecutar ECB",
|
|
command=self.run_ecb)
|
|
self.btn_ecb.pack(side=tk.LEFT, padx=8)
|
|
self.btn_cbc = tk.Button(ctrl, text="3. Ejecutar CBC",
|
|
command=self.run_cbc)
|
|
self.btn_cbc.pack(side=tk.LEFT, padx=8)
|
|
tk.Checkbutton(ctrl, text="IV aleatorio por ejecucion",
|
|
variable=self.random_iv).pack(side=tk.LEFT, padx=15)
|
|
|
|
info = tk.Frame(t)
|
|
info.pack(fill="x", padx=20, pady=4)
|
|
tk.Label(info, font=("Courier", 9), anchor="w",
|
|
text=f"Llave (Hex): {KEY.hex().upper()} "
|
|
f"[ASCII de baja entropia: solo demostracion]"
|
|
).pack(anchor="w")
|
|
self.lbl_status = tk.Label(info, text="Estado: esperando imagen...",
|
|
font=("Arial", 10, "bold"), fg="blue",
|
|
anchor="w")
|
|
self.lbl_status.pack(anchor="w")
|
|
tk.Label(info, justify="left", fg="#444", font=("Arial", 9), anchor="w",
|
|
text=("La entropia y el histograma de ECB y CBC son practicamente "
|
|
"identicos: NO sirven para detectar el modo inseguro.\n"
|
|
"El indicador valido es 'Repetidos': ECB conserva los bloques "
|
|
"duplicados del original, CBC los elimina por encadenamiento.")
|
|
).pack(anchor="w")
|
|
|
|
def make_panel(self, parent, title, size, with_hist=True):
|
|
f = tk.Frame(parent)
|
|
f.pack(side=tk.LEFT, padx=10)
|
|
tk.Label(f, text=title, font=("Arial", 10, "bold")).pack()
|
|
|
|
# Contenedor de tamano fijo EN PIXELES: un Label sin imagen mide su
|
|
# width en caracteres, no en pixeles.
|
|
holder = tk.Frame(f, width=size, height=size, bg="#ccc")
|
|
holder.pack()
|
|
holder.pack_propagate(False)
|
|
img_lbl = tk.Label(holder, bg="#ccc", borderwidth=0)
|
|
img_lbl.pack(fill="both", expand=True)
|
|
|
|
stats_lbl = tk.Label(f, text="Entropia: -\nBloques: -\nRepetidos: -",
|
|
font=("Courier", 9), justify="left",
|
|
anchor="w", width=42)
|
|
stats_lbl.pack(pady=(4, 0))
|
|
|
|
hist = None
|
|
if with_hist:
|
|
hist = tk.Canvas(f, width=size, height=HIST_H, bg="#f7f7f7",
|
|
highlightthickness=1, highlightbackground="#ddd")
|
|
hist.pack()
|
|
tk.Label(f, text="histograma de bytes 0 ... 255",
|
|
font=("Arial", 7), fg="#777").pack()
|
|
|
|
return {"img": img_lbl, "stats": stats_lbl, "hist": hist}
|
|
|
|
def update_panel_metrics(self, panel, data, color="#2c6fbb"):
|
|
panel["stats"].config(text=format_metrics(data))
|
|
if panel["hist"] is not None:
|
|
draw_histogram(panel["hist"], data, color)
|
|
|
|
def clear_panel(self, panel):
|
|
panel["img"].config(image='', bg="#ccc")
|
|
panel["img"].image = None
|
|
panel["stats"].config(text="Entropia: -\nBloques: -\nRepetidos: -")
|
|
if panel["hist"] is not None:
|
|
panel["hist"].delete("all")
|
|
|
|
def set_busy(self, busy):
|
|
self.busy = busy
|
|
state = tk.DISABLED if busy else tk.NORMAL
|
|
for b in (self.btn_load, self.btn_ecb, self.btn_cbc):
|
|
b.config(state=state)
|
|
|
|
def load_image(self):
|
|
path = filedialog.askopenfilename(
|
|
initialdir=".",
|
|
filetypes=[("Images", ("*.png", "*.jpg", "*.jpeg", "*.bmp")),
|
|
("All Files", "*.*")])
|
|
if not path:
|
|
return
|
|
try:
|
|
img = (Image.open(path)
|
|
.resize((IMG_SIZE, IMG_SIZE), Image.LANCZOS)
|
|
.convert('RGB'))
|
|
self.img_bytes_raw = img.tobytes()
|
|
self.original_img = img
|
|
|
|
self.show(self.p_orig, img)
|
|
self.update_panel_metrics(self.p_orig, self.img_bytes_raw, "#4a7c3f")
|
|
self.clear_panel(self.p_ecb)
|
|
self.clear_panel(self.p_cbc)
|
|
|
|
total = len(self.img_bytes_raw) // BLOCK
|
|
self.lbl_status.config(
|
|
text=f"Imagen cargada. Bytes: {len(self.img_bytes_raw):,} | "
|
|
f"Bloques de 16B: {total:,}")
|
|
except Exception as e:
|
|
messagebox.showerror("Error", f"No se pudo cargar la imagen: {e}")
|
|
|
|
def check_ready(self):
|
|
if self.busy:
|
|
return False
|
|
if self.img_bytes_raw is None:
|
|
messagebox.showwarning("Aviso", "Primero carga una imagen.")
|
|
return False
|
|
return True
|
|
|
|
def run_ecb(self):
|
|
if not self.check_ready():
|
|
return
|
|
ct = AES.new(KEY, AES.MODE_ECB).encrypt(pad(self.img_bytes_raw, BLOCK))
|
|
self.lbl_status.config(text="Iniciando cifrado ECB...")
|
|
self.set_busy(True)
|
|
self.animate(self.p_ecb, ct[:len(self.img_bytes_raw)],
|
|
self.img_bytes_raw, mode_name="ECB", color="#b03030")
|
|
|
|
def run_cbc(self):
|
|
if not self.check_ready():
|
|
return
|
|
iv = get_random_bytes(BLOCK) if self.random_iv.get() else IV_FIJO
|
|
ct = AES.new(KEY, AES.MODE_CBC, iv).encrypt(pad(self.img_bytes_raw, BLOCK))
|
|
self.lbl_status.config(
|
|
text=f"Iniciando cifrado CBC... IV = {iv.hex().upper()}")
|
|
self.set_busy(True)
|
|
self.animate(self.p_cbc, ct[:len(self.img_bytes_raw)],
|
|
self.img_bytes_raw, mode_name="CBC", color="#2c6fbb")
|
|
|
|
def animate(self, panel, target, original, current_idx=0,
|
|
mode_name="", color="#2c6fbb"):
|
|
if current_idx >= len(original):
|
|
self.show_bytes(panel, target)
|
|
self.update_panel_metrics(panel, target, color)
|
|
self.lbl_status.config(text=f"Cifrado {mode_name} completado.")
|
|
self.set_busy(False)
|
|
return
|
|
|
|
self.show_bytes(panel, target[:current_idx] + original[current_idx:])
|
|
self.lbl_status.config(
|
|
text=f"Cifrando {mode_name}... bloque {current_idx // BLOCK:,}"
|
|
f" / {len(original) // BLOCK:,}")
|
|
self.root.after(FRAME_DELAY,
|
|
lambda: self.animate(panel, target, original,
|
|
current_idx + STEP_BYTES,
|
|
mode_name, color))
|
|
|
|
def show(self, panel, img_obj):
|
|
tk_img = ImageTk.PhotoImage(img_obj)
|
|
panel["img"].config(image=tk_img)
|
|
panel["img"].image = tk_img
|
|
|
|
def show_bytes(self, panel, data, size=IMG_SIZE):
|
|
try:
|
|
self.show(panel, Image.frombytes('RGB', (size, size), data))
|
|
except Exception as e:
|
|
print(f"Error mostrando bytes: {e}")
|
|
|
|
# =======================================================================
|
|
# PESTANA 2: maleabilidad de CBC (bit-flipping)
|
|
# =======================================================================
|
|
def build_tab_flip(self):
|
|
t = self.tab_flip
|
|
self.flip_iv = None
|
|
self.flip_ct = None
|
|
self.flip_blocks = ["usuario=esteban;", "rol=invitado;;;;",
|
|
"saldo=0000001000"]
|
|
|
|
tk.Label(t, font=("Arial", 10), justify="left", anchor="w",
|
|
text=("CBC entrega confidencialidad, NO integridad. Un atacante que no conoce la clave "
|
|
"puede alterar el bloque de texto claro N modificando el bloque de cifrado N-1.\n"
|
|
"El precio es que el bloque N-1 queda destruido; si ese bloque es relleno o poco "
|
|
"revisado, el ataque pasa desapercibido.")
|
|
).pack(anchor="w", padx=20, pady=(12, 8))
|
|
|
|
pt_frame = tk.LabelFrame(t, text="Mensaje original (3 bloques de 16 bytes, sin relleno)",
|
|
padx=10, pady=8)
|
|
pt_frame.pack(fill="x", padx=20)
|
|
for i, b in enumerate(self.flip_blocks):
|
|
tk.Label(pt_frame, font=("Courier", 11), anchor="w",
|
|
text=f" Bloque {i}: |{b}|").pack(anchor="w")
|
|
|
|
atk = tk.LabelFrame(t, text="Ataque", padx=10, pady=8)
|
|
atk.pack(fill="x", padx=20, pady=10)
|
|
tk.Label(atk, text="El atacante quiere que el bloque 1 diga "
|
|
"(exactamente 16 caracteres):",
|
|
font=("Arial", 10)).pack(anchor="w")
|
|
self.flip_target = tk.Entry(atk, font=("Courier", 11), width=24)
|
|
self.flip_target.insert(0, "rol=admin;;;;;;;")
|
|
self.flip_target.pack(anchor="w", pady=4)
|
|
|
|
btns = tk.Frame(atk)
|
|
btns.pack(anchor="w", pady=4)
|
|
tk.Button(btns, text="1. Cifrar con CBC",
|
|
command=self.flip_encrypt).pack(side=tk.LEFT, padx=6)
|
|
tk.Button(btns, text="2. Manipular el ciphertext",
|
|
command=self.flip_attack).pack(side=tk.LEFT, padx=6)
|
|
tk.Button(btns, text="3. Repetir el ataque contra AES-GCM",
|
|
command=self.flip_gcm).pack(side=tk.LEFT, padx=6)
|
|
|
|
self.flip_out = tk.Text(t, height=16, font=("Courier", 10),
|
|
bg="#1e1e1e", fg="#dcdcdc",
|
|
insertbackground="#dcdcdc", wrap="none")
|
|
self.flip_out.pack(fill="both", expand=True, padx=20, pady=(0, 14))
|
|
self.flip_log("Pulsa '1. Cifrar con CBC' para comenzar.")
|
|
|
|
def flip_log(self, text, clear=False):
|
|
if clear:
|
|
self.flip_out.delete("1.0", tk.END)
|
|
self.flip_out.insert(tk.END, text + "\n")
|
|
self.flip_out.see(tk.END)
|
|
|
|
def flip_plaintext(self) -> bytes:
|
|
return "".join(self.flip_blocks).encode()
|
|
|
|
def flip_encrypt(self):
|
|
self.flip_iv = get_random_bytes(BLOCK)
|
|
self.flip_ct = AES.new(KEY, AES.MODE_CBC,
|
|
self.flip_iv).encrypt(self.flip_plaintext())
|
|
self.flip_log("=== CIFRADO LEGITIMO (CBC) ===", clear=True)
|
|
self.flip_log(f"IV : {self.flip_iv.hex().upper()}")
|
|
for i in range(3):
|
|
c = self.flip_ct[i * BLOCK:(i + 1) * BLOCK]
|
|
self.flip_log(f"C{i} : {c.hex().upper()}")
|
|
self.flip_log("")
|
|
self.flip_log("El atacante intercepta el ciphertext. No conoce la clave.")
|
|
|
|
def flip_attack(self):
|
|
if self.flip_ct is None:
|
|
messagebox.showwarning("Aviso", "Primero cifra el mensaje (paso 1).")
|
|
return
|
|
target = self.flip_target.get().encode()
|
|
if len(target) != BLOCK:
|
|
messagebox.showerror(
|
|
"Error",
|
|
f"El texto objetivo debe medir exactamente {BLOCK} caracteres "
|
|
f"(actualmente mide {len(target)}).")
|
|
return
|
|
|
|
# P1' = D(C1) xor C0' => C0' = C0 xor P1 xor P1'
|
|
delta = xor_bytes(self.flip_blocks[1].encode(), target)
|
|
forged = bytearray(self.flip_ct)
|
|
for j in range(BLOCK):
|
|
forged[j] ^= delta[j]
|
|
forged = bytes(forged)
|
|
|
|
recovered = AES.new(KEY, AES.MODE_CBC,
|
|
self.flip_iv).decrypt(forged)
|
|
|
|
self.flip_log("")
|
|
self.flip_log("=== ATAQUE DE BIT-FLIPPING ===")
|
|
self.flip_log(f"Delta aplicado a C0 : {delta.hex().upper()}")
|
|
self.flip_log(f"C0 manipulado : {forged[:BLOCK].hex().upper()}")
|
|
self.flip_log("")
|
|
self.flip_log("Descifrado en el servidor (la clave nunca se toco):")
|
|
for i in range(3):
|
|
b = recovered[i * BLOCK:(i + 1) * BLOCK]
|
|
mark = " <-- DESTRUIDO" if i == 0 else (" <-- CONTROLADO POR EL ATACANTE"
|
|
if i == 1 else "")
|
|
self.flip_log(f" Bloque {i}: |{printable(b)}|{mark}")
|
|
self.flip_log("")
|
|
self.flip_log("El atacante escribio el bloque 1 a voluntad sin conocer la clave.")
|
|
self.flip_log("CBC sin MAC no detecta nada: descifra y entrega el resultado.")
|
|
|
|
def flip_gcm(self):
|
|
pt = self.flip_plaintext()
|
|
nonce = get_random_bytes(12)
|
|
ct, tag = AES.new(KEY, AES.MODE_GCM, nonce=nonce).encrypt_and_digest(pt)
|
|
|
|
target = self.flip_target.get().encode()
|
|
if len(target) != BLOCK:
|
|
target = b"rol=admin;;;;;;;"
|
|
# En CTR/GCM el flip es aun mas directo: se altera el bloque objetivo.
|
|
delta = xor_bytes(self.flip_blocks[1].encode(), target)
|
|
forged = bytearray(ct)
|
|
for j in range(BLOCK):
|
|
forged[BLOCK + j] ^= delta[j]
|
|
|
|
self.flip_log("")
|
|
self.flip_log("=== MISMO ATAQUE CONTRA AES-GCM ===")
|
|
self.flip_log(f"Nonce: {nonce.hex().upper()}")
|
|
self.flip_log(f"Tag : {tag.hex().upper()}")
|
|
try:
|
|
rec = AES.new(KEY, AES.MODE_GCM,
|
|
nonce=nonce).decrypt_and_verify(bytes(forged), tag)
|
|
self.flip_log(f"Descifrado: {printable(rec)}")
|
|
except ValueError:
|
|
self.flip_log("")
|
|
self.flip_log(" MAC INVALIDO -> el mensaje se rechaza por completo.")
|
|
self.flip_log(" GCM verifica la integridad ANTES de entregar texto claro.")
|
|
self.flip_log("")
|
|
self.flip_log("Conclusion: el cifrado autenticado (AEAD) no es un extra,")
|
|
self.flip_log("es el requisito minimo para cualquier canal real.")
|
|
|
|
# =======================================================================
|
|
# PESTANA 3: reuso de nonce en CTR
|
|
# =======================================================================
|
|
def build_tab_ctr(self):
|
|
t = self.tab_ctr
|
|
self.ctr_a = None
|
|
self.ctr_b = None
|
|
|
|
tk.Label(t, font=("Arial", 10), justify="left", anchor="w",
|
|
text=("CTR convierte AES en un cifrado de flujo: C = P xor keystream. "
|
|
"Si dos mensajes usan el mismo nonce comparten keystream, y entonces\n"
|
|
" C_A xor C_B = P_A xor P_B ... el keystream se cancela y el "
|
|
"atacante ve la relacion entre ambos textos claros.")
|
|
).pack(anchor="w", padx=20, pady=(12, 8))
|
|
|
|
panels = tk.Frame(t)
|
|
panels.pack(pady=4)
|
|
self.p_a = self.make_panel(panels, "Imagen A", SMALL_SIZE, with_hist=False)
|
|
self.p_b = self.make_panel(panels, "Imagen B", SMALL_SIZE, with_hist=False)
|
|
self.p_xor_same = self.make_panel(panels, "C_A xor C_B (MISMO nonce)",
|
|
SMALL_SIZE, with_hist=False)
|
|
self.p_xor_diff = self.make_panel(panels, "C_A xor C_B (nonces distintos)",
|
|
SMALL_SIZE, with_hist=False)
|
|
|
|
ctrl = tk.Frame(t)
|
|
ctrl.pack(pady=8)
|
|
tk.Button(ctrl, text="Cargar Imagen A",
|
|
command=lambda: self.ctr_load('a')).pack(side=tk.LEFT, padx=8)
|
|
tk.Button(ctrl, text="Cargar Imagen B",
|
|
command=lambda: self.ctr_load('b')).pack(side=tk.LEFT, padx=8)
|
|
tk.Button(ctrl, text="Cifrar y comparar",
|
|
command=self.ctr_run).pack(side=tk.LEFT, padx=8)
|
|
|
|
self.ctr_status = tk.Label(t, text="Carga dos imagenes distintas.",
|
|
font=("Arial", 10, "bold"), fg="blue")
|
|
self.ctr_status.pack(pady=4)
|
|
|
|
def ctr_load(self, which):
|
|
path = filedialog.askopenfilename(
|
|
initialdir=".",
|
|
filetypes=[("Images", ("*.png", "*.jpg", "*.jpeg", "*.bmp")),
|
|
("All Files", "*.*")])
|
|
if not path:
|
|
return
|
|
try:
|
|
img = (Image.open(path)
|
|
.resize((SMALL_SIZE, SMALL_SIZE), Image.LANCZOS)
|
|
.convert('RGB'))
|
|
data = img.tobytes()
|
|
if which == 'a':
|
|
self.ctr_a = data
|
|
self.show(self.p_a, img)
|
|
self.p_a["stats"].config(text=format_metrics(data))
|
|
else:
|
|
self.ctr_b = data
|
|
self.show(self.p_b, img)
|
|
self.p_b["stats"].config(text=format_metrics(data))
|
|
self.ctr_status.config(text="Imagen cargada.")
|
|
except Exception as e:
|
|
messagebox.showerror("Error", f"No se pudo cargar la imagen: {e}")
|
|
|
|
def ctr_run(self):
|
|
if self.ctr_a is None or self.ctr_b is None:
|
|
messagebox.showwarning("Aviso", "Carga las dos imagenes primero.")
|
|
return
|
|
|
|
nonce = get_random_bytes(8)
|
|
ca = AES.new(KEY, AES.MODE_CTR, nonce=nonce).encrypt(self.ctr_a)
|
|
cb = AES.new(KEY, AES.MODE_CTR, nonce=nonce).encrypt(self.ctr_b)
|
|
xor_same = xor_bytes(ca, cb)
|
|
|
|
c2a = AES.new(KEY, AES.MODE_CTR,
|
|
nonce=get_random_bytes(8)).encrypt(self.ctr_a)
|
|
c2b = AES.new(KEY, AES.MODE_CTR,
|
|
nonce=get_random_bytes(8)).encrypt(self.ctr_b)
|
|
xor_diff = xor_bytes(c2a, c2b)
|
|
|
|
self.show_bytes(self.p_xor_same, xor_same, SMALL_SIZE)
|
|
self.p_xor_same["stats"].config(text=format_metrics(xor_same))
|
|
self.show_bytes(self.p_xor_diff, xor_diff, SMALL_SIZE)
|
|
self.p_xor_diff["stats"].config(text=format_metrics(xor_diff))
|
|
|
|
self.ctr_status.config(
|
|
text=f"Nonce reutilizado: {nonce.hex().upper()} -> el XOR de los "
|
|
f"ciphertexts es exactamente P_A xor P_B (ambas siluetas visibles). "
|
|
f"Con nonces distintos, solo ruido.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
root = tk.Tk()
|
|
app = AESLab(root)
|
|
root.mainloop()
|