#!/usr/bin/env python3
"""
PoC : reutilisation de flux OFB INTRA-fichier chez Trigona.

Confirme par desassemblage sur le build 2023 (i386) et le build 2026 (x64) :
la fonction de chiffrement par blocs de 1 Mio (FUN_004e3a08+FUN_004e37bc sur
i386, FUN_00441870/FUN_00441960+FUN_00441360 sur x64) reinitialise le
registre de retroaction OFB depuis le MEME IV de depart a CHAQUE appel, un
appel par chunk de 1 Mio. Aucune progression de l'IV ni de l'etat OFB n'est
transmise d'un chunk au suivant : chaque chunk de 1 Mio d'un meme fichier est
donc chiffre avec EXACTEMENT le meme flux de cle, independamment du nombre
de chunks.

Consequence : pour tout fichier chiffre de plus de 1 Mio (i386 : voir note
plus bas sur la taille reelle du premier segment), XORer deux chunks du
CHIFFRE (pas besoin de la cle ni de l'IV) donne directement le XOR des deux
chunks du CLAIR correspondant -- une attaque "two-time pad" classique,
deterministe, sans aucune inconnue cryptographique a lever.

Ce script :
  1. simule le comportement du code reel (memes tailles de chunk, meme
     reinitialisation par chunk) avec un vrai AES-OFB (pycryptodome)
  2. chiffre un fichier synthetique de 2 chunks avec CE comportement bugue
  3. demontre la recuperation du XOR clair1 xor clair2 a partir du chiffre
     seul, puis, avec un "crib" (fragment de clair connu ou devine pour un
     seul chunk, realiste pour un fichier de format connu), la recuperation
     complete de l'autre chunk
"""
import sys
from Crypto.Cipher import AES

CHUNK = 1024 * 1024  # 0x100000, confirme identique sur i386 (2023) et x64 (2026)


def encrypt_file_buggy(key: bytes, iv: bytes, plaintext: bytes) -> bytes:
    """Reproduit exactement le bug : un objet AES-OFB REINITIALISE avec le
    meme IV pour chaque chunk de 1 Mio, au lieu de faire progresser l'etat
    OFB (ou l'IV) d'un chunk au suivant."""
    out = bytearray()
    for offset in range(0, len(plaintext), CHUNK):
        chunk = plaintext[offset : offset + CHUNK]
        cipher = AES.new(key, AES.MODE_OFB, iv=iv)  # meme (cle, IV) a CHAQUE chunk
        out += cipher.encrypt(chunk)
    return bytes(out)


def main():
    key = bytes(range(32))          # cle AES-256 arbitraire (inconnue de l'attaquant simule)
    iv = bytes(range(100, 116))     # IV arbitraire (peu importe sa valeur pour cette demo)

    plaintext1 = (b"RAPPORT FINANCIER CONFIDENTIEL - " * 40000)[:CHUNK]
    plaintext2 = (b"BASE DE DONNEES CLIENTS EXPORT   - " * 40000)[:CHUNK]
    plaintext = plaintext1 + plaintext2
    assert len(plaintext) == 2 * CHUNK

    ciphertext = encrypt_file_buggy(key, iv, plaintext)
    chunk1_ct = ciphertext[:CHUNK]
    chunk2_ct = ciphertext[CHUNK:]

    print("=== Attaque : uniquement le chiffre, sans cle ni IV ===")
    xor_plain = bytes(a ^ b for a, b in zip(chunk1_ct, chunk2_ct))
    xor_expected = bytes(a ^ b for a, b in zip(plaintext1, plaintext2))
    assert xor_plain == xor_expected
    print("XOR(chunk1_chiffre, chunk2_chiffre) == XOR(chunk1_clair, chunk2_clair)")
    print("verifie sur", len(xor_plain), "octets, correspondance exacte.\n")

    print("=== Avec un `crib` (fragment de clair devine/connu pour chunk1) ===")
    # Cas realiste : chunk1 est le debut d'un format connu (en-tete Office/PDF/
    # SQL dump...), ou un fragment recupere par ailleurs (sauvegarde partielle,
    # cache, memoire). Ici on suppose connu seulement le tout debut de chunk1.
    known_prefix_len = 64
    known_plain1_prefix = plaintext1[:known_prefix_len]
    recovered_plain2_prefix = bytes(
        c ^ k for c, k in zip(xor_plain[:known_prefix_len], known_plain1_prefix)
    )
    print("Fragment de clair connu pour chunk1 (", known_prefix_len, "octets) :")
    print(" ", known_plain1_prefix)
    print("Fragment de chunk2 RECUPERE (sans cle, sans IV, sans chunk1 complet) :")
    print(" ", recovered_plain2_prefix)
    assert recovered_plain2_prefix == plaintext2[:known_prefix_len]
    print("\n=> correspond exactement au vrai debut de chunk2.")
    print("Si le clair complet de chunk1 est connu/reconstruit (cas frequent pour")
    print("des formats de fichiers structures, ou par recoupement avec une")
    print("sauvegarde partielle), CHUNK2 ENTIER se recupere de la meme maniere,")
    print("et ainsi de suite pour tout chunk supplementaire de 1 Mio du fichier,")
    print("sans jamais avoir besoin de la cle AES ni de l'IV reels.")


if __name__ == "__main__":
    main()
