#!/usr/bin/env python3
"""
romper_proteccion.py - Herramienta INTERNA de auditoria (no se publica).

Intenta romper la proteccion generada por proteger_codigo.py, exactamente
como lo haria alguien con acceso completo al codigo publicado, y reporta
con honestidad que tan lejos llega. El objetivo de este script no es
"demostrar que la proteccion es segura" -- es lo contrario: asumir que no
lo es, y dejar por escrito que puede reconstruir alguien con tiempo y
conocimientos de Python.

Que hace:
  1. Extrae el bytecode marshaled de un archivo "_protegido.py" (revierte
     el XOR + zlib + base64 que aplica proteger_codigo.py).
  2. Confirma que ese bytecode es un objeto codigo de Python real y
     valido (co_names, co_consts, nombres de funciones/variables, etc.
     quedan expuestos -- el bytecode de Python retiene mucha informacion
     legible, esto NO es un binario nativo compilado).
  3. Reporta que fraccion del codigo original es recuperable sin
     decompilador (nombres de funciones/clases/variables, strings
     literales) y aclara que con un decompilador de bytecode (ej.
     `decompyle3`, `uncompyle6`, `pycdc`) se puede llegar a una version
     aproximada del codigo fuente casi completa.

Conclusion ya conocida (documentada en AUDITORIA_SEGURIDAD_INTERNA.md):
la proteccion de proteger_codigo.py detiene la copia/edicion casual
("abrir el .py y ver texto plano"), no a alguien dispuesto a invertir
media hora con las herramientas correctas. No tratar esto como una
medida de seguridad real para nada sensible.

Uso:
    python3 herramientas/romper_proteccion.py ruta/al/archivo_protegido.py
"""
import sys
import os
import marshal
import dis


def extraer_codigo(ruta_protegido):
    """Revierte manualmente el mismo pipeline que arma
    proteger_codigo.py (chunks base64 + rotacion de bits + XOR con
    clave derivada + zlib) leyendo directamente los literales del
    propio archivo protegido -- sin ejecutarlo. Los nombres de variable
    y la cantidad de trozos cambian en cada build, asi que se identifica
    cada literal por su ROL en el codigo (que funcion se le aplica),
    no por un nombre fijo -- exactamente lo que haria alguien leyendo
    el archivo a mano en vez de greppeando un patron rigido."""
    import re
    import base64
    import zlib
    import hashlib
    import ast as ast_mod

    with open(ruta_protegido, "r", encoding="utf-8") as f:
        contenido = f.read()

    # 1) La sal y el nombre de archivo son los dos primeros literales
    #    bytes/str que se usan juntos en una llamada a hashlib.sha256(...).
    m_clave = re.search(
        r"hashlib\.sha256\(\s*(\w+)\s*\+\s*(\w+)\.encode",
        contenido,
    )
    if not m_clave:
        raise ValueError("No se encontro el patron de derivacion de clave (sha256(sal + nombre)).")
    var_sal, var_nombre = m_clave.groups()

    def _valor_de(nombre_var):
        m = re.search(rf"\b{re.escape(nombre_var)}\s*=\s*(b?'(?:[^'\\]|\\.)*'|b?\"(?:[^\"\\]|\\.)*\")", contenido)
        if not m:
            raise ValueError(f"No se encontro el valor asignado a '{nombre_var}'.")
        return ast_mod.literal_eval(m.group(1))

    sal = _valor_de(var_sal)
    nombre = _valor_de(var_nombre)

    # 2) La cantidad de rotacion es el entero que se compara contra 0
    #    en la linea del "if <var> else".
    m_rot_var = re.search(r"\(\((\w+)\s*>>\s*(\w+)\)", contenido)
    if not m_rot_var:
        raise ValueError("No se encontro el patron de rotacion de bits.")
    var_rotacion = m_rot_var.group(2)
    m_rot_valor = re.search(rf"\b{re.escape(var_rotacion)}\s*=\s*(\d+)", contenido)
    rotacion = int(m_rot_valor.group(1)) if m_rot_valor else 0

    # 3) Los trozos base64 viven en una variable que se le pasa a
    #    "".join(...) dentro de base64.b64decode(...); resolvemos esa
    #    variable a su lista de literales.
    m_var_chunks = re.search(r'base64\.b64decode\(\s*"".join\((\w+)\)\s*\)', contenido)
    if not m_var_chunks:
        raise ValueError("No se encontro la variable de trozos del payload.")
    var_chunks = m_var_chunks.group(1)
    m_chunks = re.search(rf"\b{re.escape(var_chunks)}\s*=\s*(\[[^\]]*\])", contenido, re.DOTALL)
    if not m_chunks:
        raise ValueError("No se encontro la lista de trozos del payload.")
    trozos = ast_mod.literal_eval(m_chunks.group(1))
    payload_bytes = base64.b64decode("".join(trozos))

    if rotacion:
        payload_bytes = bytes(((b >> rotacion) | (b << (8 - rotacion))) & 0xFF for b in payload_bytes)

    clave = hashlib.sha256(sal + nombre.encode("utf-8")).digest()
    datos = bytes(b ^ clave[i % len(clave)] for i, b in enumerate(payload_bytes))
    return marshal.loads(zlib.decompress(datos))


def reportar(codigo, ruta_protegido):
    print(f"=== Auditoria de: {ruta_protegido} ===\n")
    print("[OK] El bytecode se extrajo y cargo sin ningun secreto adicional")
    print("     (la clave se deriva de datos que ya estan en el propio archivo).\n")

    nombres_top = [n for n in codigo.co_names if not n.startswith("__")]
    print(f"Nombres visibles en el modulo principal ({len(nombres_top)}):")
    print("  " + ", ".join(nombres_top[:40]) + ("..." if len(nombres_top) > 40 else ""))

    consts_codigo = [c for c in codigo.co_consts if hasattr(c, "co_name")]
    print(f"\nFunciones/clases anidadas encontradas: {len(consts_codigo)}")
    for c in consts_codigo[:15]:
        print(f"  - {c.co_name}({', '.join(c.co_varnames[:c.co_argcount])})")
    if len(consts_codigo) > 15:
        print(f"  ... y {len(consts_codigo) - 15} mas")

    strings = [c for c in codigo.co_consts if isinstance(c, str) and len(c) > 3]
    print(f"\nStrings literales recuperables (docstrings, mensajes, etc.): {len(strings)}")

    print("\n--- Conclusion ---")
    print("Todos los nombres de funciones, clases, variables y strings")
    print("literales del archivo original quedan legibles en el bytecode,")
    print("con o sin decompilador. Con un decompilador real (uncompyle6,")
    print("decompyle3, pycdc, etc.) el resultado es codigo fuente Python")
    print("aproximado y casi completo, no solo nombres sueltos.")
    print("\nEsta proteccion sirve para evitar la copia/edicion casual,")
    print("NO para proteger un secreto real. No asumir lo contrario.")


def main():
    if len(sys.argv) < 2:
        print("Uso: python3 herramientas/romper_proteccion.py ruta/al/archivo_protegido.py")
        sys.exit(1)

    ruta = sys.argv[1]
    if not os.path.exists(ruta):
        print(f"No existe: {ruta}")
        sys.exit(1)

    codigo = extraer_codigo(ruta)
    reportar(codigo, ruta)

    if "--dis" in sys.argv:
        print("\n=== Desensamblado completo (dis.dis) ===")
        dis.dis(codigo)


if __name__ == "__main__":
    main()
