#!/usr/bin/env python3
"""
Reto 01 - PIR-01 "Manifiesto Cifrado" (Criptografia)
Ataque: primo compartido entre dos claves RSA (common factor / shared prime).

Uso:
    python3 resolver_reto01.py            # usa la carpeta ./extraido_01
    python3 resolver_reto01.py <carpeta>  # carpeta con los .pem y el .enc

Si no existe la carpeta, el script descomprime material_01-manifiesto-cifrado.zip.
Requiere: pip install cryptography
"""
import sys, os, math, zipfile
from cryptography.hazmat.primitives import serialization, hashes, padding as sympad
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

HERE = os.path.dirname(os.path.abspath(__file__))

def preparar(carpeta):
    if os.path.isdir(carpeta) and any(f.endswith(".pem") for f in os.listdir(carpeta)):
        return carpeta
    zpath = os.path.join(HERE, "material_01-manifiesto-cifrado.zip")
    os.makedirs(carpeta, exist_ok=True)
    with zipfile.ZipFile(zpath) as z:
        z.extractall(carpeta)
    print(f"[*] Descomprimido {os.path.basename(zpath)} en {carpeta}")
    return carpeta

def main():
    carpeta = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "extraido_01")
    d = preparar(carpeta)

    load = lambda f: serialization.load_pem_public_key(
        open(os.path.join(d, f), "rb").read()).public_numbers()
    A = load("clave_proveedor_A.pem")
    B = load("clave_proveedor_B.pem")

    # 1) El fallo: ambas claves comparten un primo -> mcd(nA, nB) = p
    p = math.gcd(A.n, B.n)
    print(f"[1] mcd(nA, nB) -> primo comun de {p.bit_length()} bits: {'SI' if p > 1 else 'NO'}")
    assert p > 1, "Las claves no comparten factor"

    # 2) Reconstruir la clave privada de A
    q = A.n // p
    phi = (p - 1) * (q - 1)
    dA = pow(A.e, -1, phi)
    priv = rsa.RSAPrivateNumbers(
        p, q, dA, dA % (p - 1), dA % (q - 1), pow(q, -1, p), A).private_key()
    print("[2] Clave privada de A reconstruida")

    # 3) Formato del .enc: [256]=clave sesion RSA-OAEP | [16]=IV | [resto]=AES-256-CBC
    blob = open(os.path.join(d, "manifiesto_carga.enc"), "rb").read()
    wrapped, iv, body = blob[:256], blob[256:272], blob[272:]
    for h in (hashes.SHA256(), hashes.SHA1()):
        try:
            k = priv.decrypt(wrapped, padding.OAEP(
                mgf=padding.MGF1(h), algorithm=h, label=None))
            print(f"[3] Clave de sesion AES recuperada (RSA-OAEP {h.name})")
            break
        except Exception:
            continue

    # 4) Descifrar el cuerpo con AES-256-CBC y quitar padding PKCS#7
    dec = Cipher(algorithms.AES(k), modes.CBC(iv)).decryptor()
    pt = dec.update(body) + dec.finalize()
    u = sympad.PKCS7(128).unpadder()
    texto = (u.update(pt) + u.finalize()).decode()
    print("[4] Manifiesto descifrado:\n" + "=" * 60)
    print(texto)

if __name__ == "__main__":
    main()
