# Fichier: python_cheats/cheatsheets/cryptography.txt
# Cheatsheet Cryptography Python - Guide Complet


[OK] INSTALLATION & IMPORTS

# Installation
pip install cryptography

# Imports de base
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa, padding, ec, dsa
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.backends import default_backend
from cryptography.x509 import (
    CertificateBuilder, Name, NameAttribute,
    SubjectAlternativeName, DNSName, load_pem_x509_certificate
)
from cryptography.x509.oid import NameOID, ExtensionOID
import os
import base64
from datetime import datetime, timedelta


[OK] FERNET - CHIFFREMENT SYMÉTRIQUE SIMPLE (RECOMMANDÉ)

# === Génération de clé ===
# Génère une clé Fernet (URL-safe base64-encoded 32-byte key)
key = Fernet.generate_key()
print(key)  # b'...'

# Sauvegarder la clé
with open('secret.key', 'wb') as key_file:
    key_file.write(key)

# Charger la clé
with open('secret.key', 'rb') as key_file:
    key = key_file.read()

# === Chiffrement de base ===
# Créer instance Fernet
cipher = Fernet(key)

# Chiffrer
message = b"Secret message"
encrypted = cipher.encrypt(message)
print(encrypted)  # b'gAAAAAB...'

# Déchiffrer
decrypted = cipher.decrypt(encrypted)
print(decrypted)  # b'Secret message'

# Chiffrer string (pas bytes)
message_str = "Mon message secret"
encrypted = cipher.encrypt(message_str.encode())
decrypted_str = cipher.decrypt(encrypted).decode()

# === Chiffrement avec timestamp (TTL) ===
import time

# Chiffrer
encrypted = cipher.encrypt(message)

# Déchiffrer avec vérification d'âge (30 secondes max)
try:
    decrypted = cipher.decrypt(encrypted, ttl=30)
    print("Message valide:", decrypted)
except InvalidToken:
    print("Token expiré ou invalide")

# Simuler expiration
time.sleep(31)
try:
    decrypted = cipher.decrypt(encrypted, ttl=30)
except InvalidToken:
    print("Token expiré!")

# === MultiFernet - Rotation de clés ===
from cryptography.fernet import MultiFernet

# Créer plusieurs clés
key1 = Fernet.generate_key()
key2 = Fernet.generate_key()
key3 = Fernet.generate_key()

# Créer MultiFernet (première clé = chiffrement)
f = MultiFernet([Fernet(key1), Fernet(key2), Fernet(key3)])

# Chiffrer (utilise key1)
encrypted = f.encrypt(b"Message")

# Déchiffrer (essaie toutes les clés)
decrypted = f.decrypt(encrypted)

# Rotation: rechiffrer avec nouvelle clé
rotated = f.rotate(encrypted)  # Rechiffre avec key1

# === Exemple complet ===
class SecureStorage:
    def __init__(self, key_file='secret.key'):
        self.key_file = key_file
        self.key = self._load_or_generate_key()
        self.cipher = Fernet(self.key)
    
    def _load_or_generate_key(self):
        if os.path.exists(self.key_file):
            with open(self.key_file, 'rb') as f:
                return f.read()
        else:
            key = Fernet.generate_key()
            with open(self.key_file, 'wb') as f:
                f.write(key)
            return key
    
    def encrypt(self, data: str) -> bytes:
        return self.cipher.encrypt(data.encode())
    
    def decrypt(self, encrypted: bytes) -> str:
        return self.cipher.decrypt(encrypted).decode()
    
    def encrypt_file(self, input_file, output_file):
        with open(input_file, 'rb') as f:
            data = f.read()
        encrypted = self.cipher.encrypt(data)
        with open(output_file, 'wb') as f:
            f.write(encrypted)
    
    def decrypt_file(self, input_file, output_file):
        with open(input_file, 'rb') as f:
            encrypted = f.read()
        decrypted = self.cipher.decrypt(encrypted)
        with open(output_file, 'wb') as f:
            f.write(decrypted)

# Usage
storage = SecureStorage()
encrypted = storage.encrypt("Données sensibles")
decrypted = storage.decrypt(encrypted)


[OK] HASHING - FONCTIONS DE HACHAGE

# === Hachage simple ===
from cryptography.hazmat.primitives import hashes

# Créer digest
digest = hashes.Hash(hashes.SHA256())
digest.update(b"Message part 1")
digest.update(b"Message part 2")
hash_value = digest.finalize()
print(hash_value.hex())

# === Algorithmes disponibles ===
# SHA-2 famille (recommandé)
hashes.SHA224()
hashes.SHA256()      # Recommandé pour usage général
hashes.SHA384()
hashes.SHA512()
hashes.SHA512_224()
hashes.SHA512_256()

# SHA-3 famille
hashes.SHA3_224()
hashes.SHA3_256()
hashes.SHA3_384()
hashes.SHA3_512()

# BLAKE2 (rapide et sécurisé)
hashes.BLAKE2b(digest_size=64)  # 1-64 bytes
hashes.BLAKE2s(digest_size=32)  # 1-32 bytes

# Anciens (éviter en production)
hashes.SHA1()        # Déprécié
hashes.MD5()         # Déprécié

# === Fonction helper ===
def hash_data(data: bytes, algorithm=hashes.SHA256()) -> str:
    """Hash des données et retourne hex string"""
    digest = hashes.Hash(algorithm)
    digest.update(data)
    return digest.finalize().hex()

# Usage
hash1 = hash_data(b"Test")
hash2 = hash_data(b"Test", hashes.SHA3_256())
hash3 = hash_data(b"Test", hashes.BLAKE2b(digest_size=32))

# === Vérification d'intégrité ===
def verify_file_integrity(file_path, expected_hash):
    digest = hashes.Hash(hashes.SHA256())
    with open(file_path, 'rb') as f:
        while chunk := f.read(8192):
            digest.update(chunk)
    file_hash = digest.finalize().hex()
    return file_hash == expected_hash

# === HMAC - Hash-based Message Authentication Code ===
from cryptography.hazmat.primitives import hmac

# Créer HMAC
key = os.urandom(32)
h = hmac.HMAC(key, hashes.SHA256())
h.update(b"Message to authenticate")
signature = h.finalize()

# Vérifier HMAC
h = hmac.HMAC(key, hashes.SHA256())
h.update(b"Message to authenticate")
h.verify(signature)  # Lève exception si invalide

# Avec copy() pour vérifications multiples
h = hmac.HMAC(key, hashes.SHA256())
h.update(b"Message")
h_copy = h.copy()
signature = h.finalize()
# h_copy peut encore être utilisé

# === Fonction HMAC helper ===
def create_hmac(key: bytes, message: bytes) -> bytes:
    h = hmac.HMAC(key, hashes.SHA256())
    h.update(message)
    return h.finalize()

def verify_hmac(key: bytes, message: bytes, signature: bytes) -> bool:
    h = hmac.HMAC(key, hashes.SHA256())
    h.update(message)
    try:
        h.verify(signature)
        return True
    except Exception:
        return False


[OK] KDF - KEY DERIVATION FUNCTIONS

# === PBKDF2 - Password-Based KDF (standard) ===
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC

def derive_key_pbkdf2(password: str, salt: bytes = None) -> tuple:
    """Dérive une clé depuis un mot de passe"""
    if salt is None:
        salt = os.urandom(16)
    
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA256(),
        length=32,
        salt=salt,
        iterations=480000,  # OWASP recommandation 2023
    )
    key = kdf.derive(password.encode())
    return key, salt

# Usage
password = "mon_super_password"
key, salt = derive_key_pbkdf2(password)

# Vérifier password
def verify_password_pbkdf2(password: str, salt: bytes, expected_key: bytes) -> bool:
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA256(),
        length=32,
        salt=salt,
        iterations=480000,
    )
    try:
        kdf.verify(password.encode(), expected_key)
        return True
    except Exception:
        return False

# === Scrypt - KDF résistant aux GPU/ASIC ===
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt

def derive_key_scrypt(password: str, salt: bytes = None) -> tuple:
    if salt is None:
        salt = os.urandom(16)
    
    kdf = Scrypt(
        salt=salt,
        length=32,
        n=2**14,      # CPU/memory cost (2^14 = 16384)
        r=8,          # Block size
        p=1,          # Parallelization
    )
    key = kdf.derive(password.encode())
    return key, salt

# === HKDF - HMAC-based KDF ===
from cryptography.hazmat.primitives.kdf.hkdf import HKDF

def derive_key_hkdf(input_key: bytes, info: bytes = b"", salt: bytes = None) -> bytes:
    """Dérive une clé cryptographique depuis un matériel existant"""
    if salt is None:
        salt = os.urandom(16)
    
    hkdf = HKDF(
        algorithm=hashes.SHA256(),
        length=32,
        salt=salt,
        info=info,  # Context info
    )
    return hkdf.derive(input_key)

# Dériver plusieurs clés
def derive_multiple_keys(master_key: bytes, num_keys: int = 2):
    keys = []
    for i in range(num_keys):
        key = derive_key_hkdf(master_key, info=f"key-{i}".encode())
        keys.append(key)
    return keys

# === ConcatKDF - Concatenation KDF ===
from cryptography.hazmat.primitives.kdf.concatkdf import ConcatKDFHash

kdf = ConcatKDFHash(
    algorithm=hashes.SHA256(),
    length=32,
    otherinfo=b"context info",
)
key = kdf.derive(b"input key material")

# === Exemple: Système de stockage de passwords ===
class PasswordManager:
    def __init__(self):
        self.iterations = 480000
    
    def hash_password(self, password: str) -> str:
        """Retourne salt:hash encodé en base64"""
        salt = os.urandom(16)
        kdf = PBKDF2HMAC(
            algorithm=hashes.SHA256(),
            length=32,
            salt=salt,
            iterations=self.iterations,
        )
        key = kdf.derive(password.encode())
        # Combine salt + hash
        combined = salt + key
        return base64.b64encode(combined).decode()
    
    def verify_password(self, password: str, stored_hash: str) -> bool:
        """Vérifie un password contre le hash stocké"""
        combined = base64.b64decode(stored_hash)
        salt = combined[:16]
        expected_key = combined[16:]
        
        kdf = PBKDF2HMAC(
            algorithm=hashes.SHA256(),
            length=32,
            salt=salt,
            iterations=self.iterations,
        )
        try:
            kdf.verify(password.encode(), expected_key)
            return True
        except Exception:
            return False

# Usage
pm = PasswordManager()
stored = pm.hash_password("my_password")
print(pm.verify_password("my_password", stored))      # True
print(pm.verify_password("wrong_password", stored))   # False


[OK] CHIFFREMENT SYMÉTRIQUE (AES, ChaCha20, etc.)

# === AES - Advanced Encryption Standard ===
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

# AES-256-CBC (Cipher Block Chaining)
def encrypt_aes_cbc(plaintext: bytes, key: bytes, iv: bytes = None) -> tuple:
    """Chiffre avec AES-256-CBC"""
    if iv is None:
        iv = os.urandom(16)  # IV doit être 16 bytes pour AES
    
    cipher = Cipher(
        algorithms.AES(key),  # key doit être 16, 24 ou 32 bytes
        modes.CBC(iv)
    )
    encryptor = cipher.encryptor()
    
    # Padding PKCS7
    from cryptography.hazmat.primitives import padding as sym_padding
    padder = sym_padding.PKCS7(128).padder()
    padded = padder.update(plaintext) + padder.finalize()
    
    ciphertext = encryptor.update(padded) + encryptor.finalize()
    return ciphertext, iv

def decrypt_aes_cbc(ciphertext: bytes, key: bytes, iv: bytes) -> bytes:
    """Déchiffre AES-256-CBC"""
    cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
    decryptor = cipher.decryptor()
    padded = decryptor.update(ciphertext) + decryptor.finalize()
    
    # Unpadding
    from cryptography.hazmat.primitives import padding as sym_padding
    unpadder = sym_padding.PKCS7(128).unpadder()
    plaintext = unpadder.update(padded) + unpadder.finalize()
    return plaintext

# Usage
key = os.urandom(32)  # AES-256
plaintext = b"Secret message to encrypt"
ciphertext, iv = encrypt_aes_cbc(plaintext, key)
decrypted = decrypt_aes_cbc(ciphertext, key, iv)

# === AES-GCM (Galois/Counter Mode) - RECOMMANDÉ ===
# Authentifié: détecte modifications
def encrypt_aes_gcm(plaintext: bytes, key: bytes, associated_data: bytes = None):
    """Chiffre avec AES-256-GCM (authentifié)"""
    nonce = os.urandom(12)  # 96-bit nonce pour GCM
    
    cipher = Cipher(
        algorithms.AES(key),
        modes.GCM(nonce)
    )
    encryptor = cipher.encryptor()
    
    # Données associées (non chiffrées mais authentifiées)
    if associated_data:
        encryptor.authenticate_additional_data(associated_data)
    
    ciphertext = encryptor.update(plaintext) + encryptor.finalize()
    
    return {
        'ciphertext': ciphertext,
        'nonce': nonce,
        'tag': encryptor.tag  # Tag d'authentification
    }

def decrypt_aes_gcm(ciphertext: bytes, key: bytes, nonce: bytes, 
                    tag: bytes, associated_data: bytes = None) -> bytes:
    """Déchiffre AES-256-GCM"""
    cipher = Cipher(
        algorithms.AES(key),
        modes.GCM(nonce, tag)
    )
    decryptor = cipher.decryptor()
    
    if associated_data:
        decryptor.authenticate_additional_data(associated_data)
    
    plaintext = decryptor.update(ciphertext) + decryptor.finalize()
    return plaintext

# Usage
key = os.urandom(32)
plaintext = b"Secret data"
associated_data = b"user_id:12345"  # Metadata

result = encrypt_aes_gcm(plaintext, key, associated_data)
decrypted = decrypt_aes_gcm(
    result['ciphertext'], 
    key, 
    result['nonce'], 
    result['tag'],
    associated_data
)

# === Autres modes AES ===
# CTR (Counter) - Streaming, parallélisable
modes.CTR(nonce)

# CFB (Cipher Feedback)
modes.CFB(iv)

# OFB (Output Feedback)
modes.OFB(iv)

# ECB (Electronic Codebook) - NE PAS UTILISER
modes.ECB()  # Dangereux: patterns visibles

# === ChaCha20 (alternative moderne à AES) ===
from cryptography.hazmat.primitives.ciphers import algorithms, modes

def encrypt_chacha20(plaintext: bytes, key: bytes):
    """Chiffre avec ChaCha20"""
    nonce = os.urandom(16)  # 128-bit nonce
    
    cipher = Cipher(
        algorithms.ChaCha20(key, nonce),
        mode=None  # ChaCha20 n'a pas de mode
    )
    encryptor = cipher.encryptor()
    ciphertext = encryptor.update(plaintext) + encryptor.finalize()
    
    return ciphertext, nonce

# Usage
key = os.urandom(32)  # 256-bit
ciphertext, nonce = encrypt_chacha20(b"Message", key)

# === ChaCha20-Poly1305 (authentifié) - RECOMMANDÉ ===
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305

def encrypt_chacha20_poly1305(plaintext: bytes, key: bytes, 
                               associated_data: bytes = None):
    """Chiffre avec ChaCha20-Poly1305 (authentifié)"""
    nonce = os.urandom(12)  # 96-bit
    
    chacha = ChaCha20Poly1305(key)
    ciphertext = chacha.encrypt(nonce, plaintext, associated_data)
    
    return ciphertext, nonce

def decrypt_chacha20_poly1305(ciphertext: bytes, key: bytes, 
                               nonce: bytes, associated_data: bytes = None):
    """Déchiffre ChaCha20-Poly1305"""
    chacha = ChaCha20Poly1305(key)
    plaintext = chacha.decrypt(nonce, ciphertext, associated_data)
    return plaintext

# Usage
key = ChaCha20Poly1305.generate_key()
ciphertext, nonce = encrypt_chacha20_poly1305(b"Secret", key)
plaintext = decrypt_chacha20_poly1305(ciphertext, key, nonce)

# === AESGCM (interface simplifiée) ===
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

# Créer instance
aesgcm = AESGCM(os.urandom(32))  # 256-bit key

# Chiffrer
nonce = os.urandom(12)
ciphertext = aesgcm.encrypt(nonce, b"plaintext", b"associated_data")

# Déchiffrer
plaintext = aesgcm.decrypt(nonce, ciphertext, b"associated_data")

# Générer clé
key = AESGCM.generate_key(bit_length=256)  # 128, 192, ou 256

# === Classe utilitaire complète ===
class AESCipher:
    """Chiffrement AES-GCM simplifié"""
    
    def __init__(self, key: bytes = None):
        self.key = key or AESGCM.generate_key(bit_length=256)
        self.aesgcm = AESGCM(self.key)
    
    def encrypt(self, plaintext: bytes, associated_data: bytes = None) -> dict:
        """Chiffre et retourne dict avec nonce et ciphertext"""
        nonce = os.urandom(12)
        ciphertext = self.aesgcm.encrypt(nonce, plaintext, associated_data)
        return {
            'nonce': base64.b64encode(nonce).decode(),
            'ciphertext': base64.b64encode(ciphertext).decode()
        }
    
    def decrypt(self, encrypted_data: dict, associated_data: bytes = None) -> bytes:
        """Déchiffre depuis dict"""
        nonce = base64.b64decode(encrypted_data['nonce'])
        ciphertext = base64.b64decode(encrypted_data['ciphertext'])
        return self.aesgcm.decrypt(nonce, ciphertext, associated_data)
    
    def save_key(self, filename: str):
        """Sauvegarde la clé"""
        with open(filename, 'wb') as f:
            f.write(self.key)
    
    @classmethod
    def load_key(cls, filename: str):
        """Charge une clé existante"""
        with open(filename, 'rb') as f:
            key = f.read()
        return cls(key)


[OK] CHIFFREMENT ASYMÉTRIQUE (RSA, ECC)

# === RSA - Génération de clés ===
from cryptography.hazmat.primitives.asymmetric import rsa, padding

# Générer paire de clés
private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048,  # 2048, 3072, ou 4096
)
public_key = private_key.public_key()

# Clé 4096-bit (plus sécurisé, plus lent)
private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=4096,
)

# === Sérialisation RSA ===
# Sauvegarder clé privée (PEM, chiffrée)
from cryptography.hazmat.primitives import serialization

pem = private_key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.PKCS8,
    encryption_algorithm=serialization.BestAvailableEncryption(b'password')
)

with open('private_key.pem', 'wb') as f:
    f.write(pem)

# Sauvegarder sans encryption (dangereux!)
pem = private_key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.PKCS8,
    encryption_algorithm=serialization.NoEncryption()
)

# Sauvegarder clé publique
pem = public_key.public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo
)

with open('public_key.pem', 'wb') as f:
    f.write(pem)

# Charger clé privée
with open('private_key.pem', 'rb') as f:
    private_key = serialization.load_pem_private_key(
        f.read(),
        password=b'password'
    )

# Charger clé publique
with open('public_key.pem', 'rb') as f:
    public_key = serialization.load_pem_public_key(f.read())

# === Chiffrement RSA ===
# Chiffrer (avec clé publique)
message = b"Secret message"
ciphertext = public_key.encrypt(
    message,
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None
    )
)

# Déchiffrer (avec clé privée)
plaintext = private_key.decrypt(
    ciphertext,
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None
    )
)

# Autres paddings
# PKCS1v15 (ancien, moins sécurisé)
padding.PKCS1v15()

# === Signature RSA ===
# Signer (avec clé privée)
message = b"Important message"
signature = private_key.sign(
    message,
    padding.PSS(
        mgf=padding.MGF1(hashes.SHA256()),
        salt_length=padding.PSS.MAX_LENGTH
    ),
    hashes.SHA256()
)

# Vérifier signature (avec clé publique)
try:
    public_key.verify(
        signature,
        message,
        padding.PSS(
            mgf=padding.MGF1(hashes.SHA256()),
            salt_length=padding.PSS.MAX_LENGTH
        ),
        hashes.SHA256()
    )
    print("Signature valide!")
except Exception:
    print("Signature invalide!")

# Padding PKCS1v15 pour signature
signature = private_key.sign(
    message,
    padding.PKCS1v15(),
    hashes.SHA256()
)

# === Classe RSA complète ===
class RSAKeyPair:
    def __init__(self, key_size=2048):
        self.private_key = rsa.generate_private_key(
            public_exponent=65537,
            key_size=key_size
        )
        self.public_key = self.private_key.public_key()
    
    def encrypt(self, plaintext: bytes) -> bytes:
        """Chiffre avec clé publique"""
        return self.public_key.encrypt(
            plaintext,
            padding.OAEP(
                mgf=padding.MGF1(algorithm=hashes.SHA256()),
                algorithm=hashes.SHA256(),
                label=None
            )
        )
    
    def decrypt(self, ciphertext: bytes) -> bytes:
        """Déchiffre avec clé privée"""
        return self.private_key.decrypt(
            ciphertext,
            padding.OAEP(
                mgf=padding.MGF1(algorithm=hashes.SHA256()),
                algorithm=hashes.SHA256(),
                label=None
            )
        )
    
    def sign(self, message: bytes) -> bytes:
        """Signe avec clé privée"""
        return self.private_key.sign(
            message,
            padding.PSS(
                mgf=padding.MGF1(hashes.SHA256()),
                salt_length=padding.PSS.MAX_LENGTH
            ),
            hashes.SHA256()
        )
    
    def verify(self, message: bytes, signature: bytes) -> bool:
        """Vérifie signature avec clé publique"""
        try:
            self.public_key.verify(
                signature,
                message,
                padding.PSS(
                    mgf=padding.MGF1(hashes.SHA256()),
                    salt_length=padding.PSS.MAX_LENGTH
                ),
                hashes.SHA256()
            )
            return True
        except Exception:
            return False
    
    def save_private_key(self, filename: str, password: bytes = None):
        """Sauvegarde clé privée"""
        encryption = (
            serialization.BestAvailableEncryption(password)
            if password else serialization.NoEncryption()
        )
        pem = self.private_key.private_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PrivateFormat.PKCS8,
            encryption_algorithm=encryption
        )
        with open(filename, 'wb') as f:
            f.write(pem)
    
    def save_public_key(self, filename: str):
        """Sauvegarde clé publique"""
        pem = self.public_key.public_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PublicFormat.SubjectPublicKeyInfo
        )
        with open(filename, 'wb') as f:
            f.write(pem)
    
    @classmethod
    def load_private_key(cls, filename: str, password: bytes = None):
        """Charge clé privée"""
        with open(filename, 'rb') as f:
            private_key = serialization.load_pem_private_key(
                f.read(), password=password
            )
        instance = cls.__new__(cls)
        instance.private_key = private_key
        instance.public_key = private_key.public_key()
        return instance

# === ECC (Elliptic Curve Cryptography) ===
from cryptography.hazmat.primitives.asymmetric import ec

# Générer clé ECC
private_key = ec.generate_private_key(ec.SECP256R1())  # NIST P-256
public_key = private_key.public_key()

# Courbes disponibles
ec.SECP192R1()      # NIST P-192
ec.SECP224R1()      # NIST P-224
ec.SECP256R1()      # NIST P-256 (recommandé)
ec.SECP384R1()      # NIST P-384
ec.SECP521R1()      # NIST P-521
ec.SECP256K1()      # Bitcoin
ec.BrainpoolP256R1()
ec.BrainpoolP384R1()
ec.BrainpoolP512R1()

# Signer avec ECC (ECDSA)
signature = private_key.sign(
    b"Message to sign",
    ec.ECDSA(hashes.SHA256())
)

# Vérifier signature ECC
try:
    public_key.verify(
        signature,
        b"Message to sign",
        ec.ECDSA(hashes.SHA256())
    )
    print("Signature ECC valide!")
except Exception:
    print("Signature ECC invalide!")

# Sérialisation ECC
# Clé privée
pem = private_key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.PKCS8,
    encryption_algorithm=serialization.BestAvailableEncryption(b'password')
)

# Clé publique
pem = public_key.public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo
)

# === ECDH (Elliptic Curve Diffie-Hellman) - Échange de clés ===
# Partie A génère sa paire de clés
private_key_a = ec.generate_private_key(ec.SECP256R1())
public_key_a = private_key_a.public_key()

# Partie B génère sa paire de clés
private_key_b = ec.generate_private_key(ec.SECP256R1())
public_key_b = private_key_b.public_key()

# A calcule le secret partagé
shared_key_a = private_key_a.exchange(ec.ECDH(), public_key_b)

# B calcule le secret partagé (identique!)
shared_key_b = private_key_b.exchange(ec.ECDH(), public_key_b)

# Dériver clé de chiffrement depuis secret partagé
from cryptography.hazmat.primitives.kdf.hkdf import HKDF

derived_key = HKDF(
    algorithm=hashes.SHA256(),
    length=32,
    salt=None,
    info=b'handshake data',
).derive(shared_key_a)

# === DSA (Digital Signature Algorithm) ===
from cryptography.hazmat.primitives.asymmetric import dsa

# Générer clé DSA
private_key = dsa.generate_private_key(key_size=2048)
public_key = private_key.public_key()

# Signer
signature = private_key.sign(b"Message", hashes.SHA256())

# Vérifier
try:
    public_key.verify(signature, b"Message", hashes.SHA256())
    print("Signature DSA valide!")
except Exception:
    print("Signature DSA invalide!")

# === Ed25519 (EdDSA) - RECOMMANDÉ pour signatures ===
from cryptography.hazmat.primitives.asymmetric import ed25519

# Générer clé (plus rapide et plus sûr que RSA/DSA)
private_key = ed25519.Ed25519PrivateKey.generate()
public_key = private_key.public_key()

# Signer
signature = private_key.sign(b"Message")

# Vérifier
try:
    public_key.verify(signature, b"Message")
    print("Signature Ed25519 valide!")
except Exception:
    print("Signature Ed25519 invalide!")

# Sérialisation Ed25519
private_bytes = private_key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.PKCS8,
    encryption_algorithm=serialization.NoEncryption()
)

public_bytes = public_key.public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo
)

# Raw bytes (32 bytes)
private_raw = private_key.private_bytes(
    encoding=serialization.Encoding.Raw,
    format=serialization.PrivateFormat.Raw,
    encryption_algorithm=serialization.NoEncryption()
)

public_raw = public_key.public_bytes(
    encoding=serialization.Encoding.Raw,
    format=serialization.PublicFormat.Raw
)

# Charger depuis raw bytes
private_key = ed25519.Ed25519PrivateKey.from_private_bytes(private_raw)
public_key = ed25519.Ed25519PublicKey.from_public_bytes(public_raw)

# === X25519 (ECDH avec Curve25519) ===
from cryptography.hazmat.primitives.asymmetric import x25519

# Générer clés pour échange
private_key_a = x25519.X25519PrivateKey.generate()
public_key_a = private_key_a.public_key()

private_key_b = x25519.X25519PrivateKey.generate()
public_key_b = private_key_b.public_key()

# Calculer secret partagé
shared_secret = private_key_a.exchange(public_key_b)

# Dériver clé de chiffrement
derived_key = HKDF(
    algorithm=hashes.SHA256(),
    length=32,
    salt=None,
    info=b'key exchange',
).derive(shared_secret)


[OK] CERTIFICATS X.509

# === Créer un certificat auto-signé ===
from cryptography.x509 import (
    CertificateBuilder, Name, NameAttribute,
    BasicConstraints, SubjectAlternativeName, DNSName
)
from cryptography.x509.oid import NameOID, ExtensionOID
from datetime import datetime, timedelta

def create_self_signed_cert(hostname="localhost"):
    """Crée un certificat auto-signé"""
    # Générer clé privée
    private_key = rsa.generate_private_key(
        public_exponent=65537,
        key_size=2048,
    )
    
    # Informations du sujet
    subject = issuer = Name([
        NameAttribute(NameOID.COUNTRY_NAME, "US"),
        NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "California"),
        NameAttribute(NameOID.LOCALITY_NAME, "San Francisco"),
        NameAttribute(NameOID.ORGANIZATION_NAME, "My Company"),
        NameAttribute(NameOID.COMMON_NAME, hostname),
    ])
    
    # Construire certificat
    cert = (
        CertificateBuilder()
        .subject_name(subject)
        .issuer_name(issuer)
        .public_key(private_key.public_key())
        .serial_number(x509.random_serial_number())
        .not_valid_before(datetime.utcnow())
        .not_valid_after(datetime.utcnow() + timedelta(days=365))
        .add_extension(
            SubjectAlternativeName([DNSName(hostname)]),
            critical=False,
        )
        .sign(private_key, hashes.SHA256())
    )
    
    return cert, private_key

# Usage
cert, private_key = create_self_signed_cert("example.com")

# Sauvegarder certificat
cert_pem = cert.public_bytes(serialization.Encoding.PEM)
with open('certificate.pem', 'wb') as f:
    f.write(cert_pem)

# Sauvegarder clé privée
key_pem = private_key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.PKCS8,
    encryption_algorithm=serialization.NoEncryption()
)
with open('private_key.pem', 'wb') as f:
    f.write(key_pem)

# === Charger certificat ===
from cryptography import x509

with open('certificate.pem', 'rb') as f:
    cert = x509.load_pem_x509_certificate(f.read())

# Lire informations du certificat
print("Subject:", cert.subject)
print("Issuer:", cert.issuer)
print("Serial:", cert.serial_number)
print("Not Before:", cert.not_valid_before)
print("Not After:", cert.not_valid_after)
print("Version:", cert.version)

# Extraire Common Name
for attribute in cert.subject:
    if attribute.oid == NameOID.COMMON_NAME:
        print("CN:", attribute.value)

# === Créer CSR (Certificate Signing Request) ===
from cryptography.x509 import CertificateSigningRequestBuilder

def create_csr(hostname="example.com"):
    """Crée une demande de signature de certificat"""
    private_key = rsa.generate_private_key(
        public_exponent=65537,
        key_size=2048,
    )
    
    # Construire CSR
    csr = (
        CertificateSigningRequestBuilder()
        .subject_name(Name([
            NameAttribute(NameOID.COUNTRY_NAME, "US"),
            NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "CA"),
            NameAttribute(NameOID.LOCALITY_NAME, "San Francisco"),
            NameAttribute(NameOID.ORGANIZATION_NAME, "My Company"),
            NameAttribute(NameOID.COMMON_NAME, hostname),
        ]))
        .add_extension(
            SubjectAlternativeName([
                DNSName(hostname),
                DNSName(f"www.{hostname}"),
            ]),
            critical=False,
        )
        .sign(private_key, hashes.SHA256())
    )
    
    return csr, private_key

# Usage
csr, private_key = create_csr("example.com")

# Sauvegarder CSR
csr_pem = csr.public_bytes(serialization.Encoding.PEM)
with open('request.csr', 'wb') as f:
    f.write(csr_pem)

# === Signer un CSR (créer CA) ===
def sign_csr(csr, ca_cert, ca_key, days=365):
    """Signe un CSR avec une CA"""
    cert = (
        CertificateBuilder()
        .subject_name(csr.subject)
        .issuer_name(ca_cert.subject)
        .public_key(csr.public_key())
        .serial_number(x509.random_serial_number())
        .not_valid_before(datetime.utcnow())
        .not_valid_after(datetime.utcnow() + timedelta(days=days))
        .add_extension(
            BasicConstraints(ca=False, path_length=None),
            critical=True,
        )
    )
    
    # Copier extensions du CSR
    for extension in csr.extensions:
        cert = cert.add_extension(extension.value, extension.critical)
    
    return cert.sign(ca_key, hashes.SHA256())

# === Vérifier certificat ===
def verify_certificate_signature(cert, issuer_public_key):
    """Vérifie la signature d'un certificat"""
    try:
        issuer_public_key.verify(
            cert.signature,
            cert.tbs_certificate_bytes,
            padding.PKCS1v15(),
            cert.signature_hash_algorithm,
        )
        return True
    except Exception:
        return False

# === Extensions de certificat ===
from cryptography.x509 import (
    KeyUsage, ExtendedKeyUsage, AuthorityKeyIdentifier,
    SubjectKeyIdentifier, CRLDistributionPoints
)
from cryptography.x509.oid import ExtendedKeyUsageOID

# Key Usage
cert_builder.add_extension(
    KeyUsage(
        digital_signature=True,
        key_encipherment=True,
        content_commitment=False,
        data_encipherment=False,
        key_agreement=False,
        key_cert_sign=False,
        crl_sign=False,
        encipher_only=False,
        decipher_only=False,
    ),
    critical=True,
)

# Extended Key Usage
cert_builder.add_extension(
    ExtendedKeyUsage([
        ExtendedKeyUsageOID.SERVER_AUTH,
        ExtendedKeyUsageOID.CLIENT_AUTH,
    ]),
    critical=False,
)

# Subject Alternative Names (multiple domaines)
cert_builder.add_extension(
    SubjectAlternativeName([
        DNSName("example.com"),
        DNSName("www.example.com"),
        DNSName("mail.example.com"),
    ]),
    critical=False,
)


[OK] ENCODAGE & UTILITAIRES

# === Base64 ===
import base64

# Encoder
data = b"Binary data"
encoded = base64.b64encode(data)
print(encoded)  # b'QmluYXJ5IGRhdGE='

# Décoder
decoded = base64.b64decode(encoded)

# URL-safe Base64
encoded = base64.urlsafe_b64encode(data)
decoded = base64.urlsafe_b64decode(encoded)

# === Hex ===
# Bytes to hex
data = b"\x01\x02\x03\x04"
hex_str = data.hex()
print(hex_str)  # '01020304'

# Hex to bytes
data = bytes.fromhex('01020304')

# === Constant-time comparison ===
from cryptography.hazmat.primitives import constant_time

# Comparer de manière sûre (évite timing attacks)
is_equal = constant_time.bytes_eq(b"secret1", b"secret2")

# === Padding ===
from cryptography.hazmat.primitives import padding

# PKCS7 Padding (pour block ciphers)
padder = padding.PKCS7(128).padder()  # 128-bit blocks
padded_data = padder.update(b"data") + padder.finalize()

# Unpadding
unpadder = padding.PKCS7(128).unpadder()
data = unpadder.update(padded_data) + unpadder.finalize()

# ANSIX923 Padding
padder = padding.ANSIX923(128).padder()
padded = padder.update(b"data") + padder.finalize()

# === Générateur de nombres aléatoires sécurisé ===
# os.urandom - recommandé
random_bytes = os.urandom(32)  # 32 bytes aléatoires

# secrets module (Python 3.6+)
import secrets
token = secrets.token_bytes(32)
token_hex = secrets.token_hex(32)  # 64 caractères hex
token_url = secrets.token_urlsafe(32)  # URL-safe

# === Nonces et Salt ===
# Toujours générer aléatoirement
nonce = os.urandom(12)  # GCM: 12 bytes
iv = os.urandom(16)     # CBC: 16 bytes
salt = os.urandom(16)   # KDF: 16+ bytes


[OK] EXEMPLES PRATIQUES COMPLETS

# === 1. Système de chiffrement de fichiers ===
class FileEncryptor:
    """Chiffre/déchiffre des fichiers avec AES-GCM"""
    
    def __init__(self, password: str):
        # Dériver clé depuis password
        salt = os.urandom(16)
        kdf = PBKDF2HMAC(
            algorithm=hashes.SHA256(),
            length=32,
            salt=salt,
            iterations=480000,
        )
        self.key = kdf.derive(password.encode())
        self.salt = salt
        self.aesgcm = AESGCM(self.key)
    
    def encrypt_file(self, input_file: str, output_file: str):
        """Chiffre un fichier"""
        with open(input_file, 'rb') as f:
            plaintext = f.read()
        
        nonce = os.urandom(12)
        ciphertext = self.aesgcm.encrypt(nonce, plaintext, None)
        
        # Format: salt(16) + nonce(12) + ciphertext
        with open(output_file, 'wb') as f:
            f.write(self.salt + nonce + ciphertext)
    
    def decrypt_file(self, input_file: str, output_file: str, password: str):
        """Déchiffre un fichier"""
        with open(input_file, 'rb') as f:
            data = f.read()
        
        # Extraire composants
        salt = data[:16]
        nonce = data[16:28]
        ciphertext = data[28:]
        
        # Recalculer clé
        kdf = PBKDF2HMAC(
            algorithm=hashes.SHA256(),
            length=32,
            salt=salt,
            iterations=480000,
        )
        key = kdf.derive(password.encode())
        aesgcm = AESGCM(key)
        
        # Déchiffrer
        plaintext = aesgcm.decrypt(nonce, ciphertext, None)
        
        with open(output_file, 'wb') as f:
            f.write(plaintext)

# Usage
encryptor = FileEncryptor("my_password")
encryptor.encrypt_file("document.pdf", "document.pdf.encrypted")
encryptor.decrypt_file("document.pdf.encrypted", "document_decrypted.pdf", "my_password")

# === 2. Système de signature de messages ===
class MessageSigner:
    """Signe et vérifie des messages"""
    
    def __init__(self):
        self.private_key = ed25519.Ed25519PrivateKey.generate()
        self.public_key = self.private_key.public_key()
    
    def sign_message(self, message: str) -> dict:
        """Signe un message et retourne dict"""
        signature = self.private_key.sign(message.encode())
        return {
            'message': message,
            'signature': base64.b64encode(signature).decode(),
            'public_key': base64.b64encode(
                self.public_key.public_bytes(
                    encoding=serialization.Encoding.Raw,
                    format=serialization.PublicFormat.Raw
                )
            ).decode()
        }
    
    @staticmethod
    def verify_message(signed_data: dict) -> bool:
        """Vérifie un message signé"""
        try:
            public_key = ed25519.Ed25519PublicKey.from_public_bytes(
                base64.b64decode(signed_data['public_key'])
            )
            signature = base64.b64decode(signed_data['signature'])
            message = signed_data['message'].encode()
            
            public_key.verify(signature, message)
            return True
        except Exception:
            return False

# Usage
signer = MessageSigner()
signed = signer.sign_message("Message important")
is_valid = MessageSigner.verify_message(signed)

# === 3. Échange de clés sécurisé (Diffie-Hellman) ===
class SecureKeyExchange:
    """Implémente échange de clés ECDH"""
    
    def __init__(self):
        self.private_key = x25519.X25519PrivateKey.generate()
        self.public_key = self.private_key.public_key()
    
    def get_public_key_bytes(self) -> bytes:
        """Retourne clé publique à partager"""
        return self.public_key.public_bytes(
            encoding=serialization.Encoding.Raw,
            format=serialization.PublicFormat.Raw
        )
    
    def derive_shared_key(self, peer_public_key_bytes: bytes) -> bytes:
        """Calcule clé partagée depuis clé publique du pair"""
        peer_public_key = x25519.X25519PublicKey.from_public_bytes(
            peer_public_key_bytes
        )
        shared_secret = self.private_key.exchange(peer_public_key)
        
        # Dériver clé de chiffrement
        derived_key = HKDF(
            algorithm=hashes.SHA256(),
            length=32,
            salt=None,
            info=b'secure communication',
        ).derive(shared_secret)
        
        return derived_key

# Usage (simulation Alice et Bob)
alice = SecureKeyExchange()
bob = SecureKeyExchange()

# Échanger clés publiques
alice_public = alice.get_public_key_bytes()
bob_public = bob.get_public_key_bytes()

# Dériver clés partagées (identiques!)
alice_shared = alice.derive_shared_key(bob_public)
bob_shared = bob.derive_shared_key(alice_public)

assert alice_shared == bob_shared

# Utiliser pour chiffrer
cipher = AESGCM(alice_shared)
nonce = os.urandom(12)
ciphertext = cipher.encrypt(nonce, b"Secret message", None)
plaintext = cipher.decrypt(nonce, ciphertext, None)

# === 4. Token JWT-like sécurisé ===
import json
import time

class SecureToken:
    """Crée et vérifie des tokens signés"""
    
    def __init__(self, secret_key: bytes = None):
        if secret_key is None:
            secret_key = os.urandom(32)
        self.secret_key = secret_key
    
    def create_token(self, payload: dict, expires_in: int = 3600) -> str:
        """Crée un token avec payload et expiration"""
        # Ajouter timestamp
        payload['exp'] = int(time.time()) + expires_in
        payload['iat'] = int(time.time())
        
        # Encoder payload
        payload_json = json.dumps(payload).encode()
        payload_b64 = base64.urlsafe_b64encode(payload_json).decode()
        
        # Créer signature HMAC
        h = hmac.HMAC(self.secret_key, hashes.SHA256())
        h.update(payload_b64.encode())
        signature = base64.urlsafe_b64encode(h.finalize()).decode()
        
        # Token format: payload.signature
        return f"{payload_b64}.{signature}"
    
    def verify_token(self, token: str) -> dict:
        """Vérifie et décode un token"""
        try:
            payload_b64, signature_b64 = token.split('.')
            
            # Vérifier signature
            h = hmac.HMAC(self.secret_key, hashes.SHA256())
            h.update(payload_b64.encode())
            expected_signature = base64.urlsafe_b64encode(h.finalize()).decode()
            
            if not constant_time.bytes_eq(
                signature_b64.encode(), 
                expected_signature.encode()
            ):
                raise ValueError("Invalid signature")
            
            # Décoder payload
            payload_json = base64.urlsafe_b64decode(payload_b64)
            payload = json.loads(payload_json)
            
            # Vérifier expiration
            if payload.get('exp', 0) < time.time():
                raise ValueError("Token expired")
            
            return payload
        except Exception as e:
            raise ValueError(f"Invalid token: {e}")

# Usage
token_manager = SecureToken()
token = token_manager.create_token({
    'user_id': 123,
    'role': 'admin'
}, expires_in=3600)

payload = token_manager.verify_token(token)
print(payload)  # {'user_id': 123, 'role': 'admin', ...}

# === 5. Stockage sécurisé de secrets ===
class SecretManager:
    """Gère stockage chiffré de secrets"""
    
    def __init__(self, master_password: str, storage_file: str = 'secrets.enc'):
        self.storage_file = storage_file
        self.key = self._derive_key(master_password)
        self.cipher = Fernet(self.key)
        self.secrets = self._load_secrets()
    
    def _derive_key(self, password: str) -> bytes:
        """Dérive clé Fernet depuis password"""
        # Salt fixe stocké ou généré une fois
        salt_file = f"{self.storage_file}.salt"
        if os.path.exists(salt_file):
            with open(salt_file, 'rb') as f:
                salt = f.read()
        else:
            salt = os.urandom(16)
            with open(salt_file, 'wb') as f:
                f.write(salt)
        
        kdf = PBKDF2HMAC(
            algorithm=hashes.SHA256(),
            length=32,
            salt=salt,
            iterations=480000,
        )
        key = kdf.derive(password.encode())
        return base64.urlsafe_b64encode(key)
    
    def _load_secrets(self) -> dict:
        """Charge secrets chiffrés"""
        if not os.path.exists(self.storage_file):
            return {}
        
        with open(self.storage_file, 'rb') as f:
            encrypted = f.read()
        
        if not encrypted:
            return {}
        
        decrypted = self.cipher.decrypt(encrypted)
        return json.loads(decrypted)
    
    def _save_secrets(self):
        """Sauvegarde secrets chiffrés"""
        data = json.dumps(self.secrets).encode()
        encrypted = self.cipher.encrypt(data)
        
        with open(self.storage_file, 'wb') as f:
            f.write(encrypted)
    
    def set_secret(self, name: str, value: str):
        """Définit un secret"""
        self.secrets[name] = value
        self._save_secrets()
    
    def get_secret(self, name: str) -> str:
        """Récupère un secret"""
        return self.secrets.get(name)
    
    def delete_secret(self, name: str):
        """Supprime un secret"""
        if name in self.secrets:
            del self.secrets[name]
            self._save_secrets()
    
    def list_secrets(self) -> list:
        """Liste noms des secrets"""
        return list(self.secrets.keys())

# Usage
manager = SecretManager("master_password")
manager.set_secret("api_key", "sk-1234567890")
manager.set_secret("db_password", "super_secret")

api_key = manager.get_secret("api_key")
all_secrets = manager.list_secrets()


[OK] SÉCURITÉ & BONNES PRATIQUES

# === Génération de clés ===
# [OK] Toujours utiliser os.urandom() ou secrets
# [OK] Taille minimale: 128 bits (16 bytes)
# [OK] Recommandé: 256 bits (32 bytes)

# Bon
key = os.urandom(32)
key = secrets.token_bytes(32)

# Mauvais
import random
key = bytes([random.randint(0, 255) for _ in range(32)])  # PAS sécurisé!

# === Stockage de clés ===
# [OK] Ne JAMAIS hardcoder les clés
# [OK] Utiliser variables d'environnement ou key management systems
# [OK] Chiffrer les clés au repos
# [OK] Utiliser permissions fichiers restrictives

# Bon
key = os.environ.get('ENCRYPTION_KEY')
# chmod 600 secret.key

# Mauvais
key = b'hardcoded_key_123'  # NE JAMAIS FAIRE!

# === Mots de passe ===
# [OK] Toujours utiliser KDF (PBKDF2, Scrypt, Argon2)
# [OK] Utiliser salt unique par password
# [OK] Itérations élevées (480000+ pour PBKDF2)
# [OK] Ne jamais stocker passwords en clair

# Bon
key, salt = derive_key_pbkdf2(password)

# Mauvais
hash_obj = hashes.Hash(hashes.SHA256())
hash_obj.update(password.encode())
# Hash simple n'est PAS suffisant!

# === Nonces et IVs ===
# [OK] Toujours aléatoires et uniques
# [OK] Ne JAMAIS réutiliser avec la même clé
# [OK] Stocker avec le ciphertext

# Bon
nonce = os.urandom(12)  # Nouveau à chaque chiffrement

# Mauvais
nonce = b'\x00' * 12  # NE JAMAIS réutiliser!

# === Padding Oracle ===
# [OK] Utiliser modes authentifiés (GCM, Poly1305)
# [OK] Éviter CBC sans authentication

# Bon (authentifié)
cipher = Cipher(algorithms.AES(key), modes.GCM(nonce))

# Risqué (non authentifié)
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))

# === Timing Attacks ===
# [OK] Utiliser constant_time.bytes_eq() pour comparaisons

# Bon
from cryptography.hazmat.primitives import constant_time
is_equal = constant_time.bytes_eq(hash1, hash2)

# Mauvais
is_equal = (hash1 == hash2)  # Vulnérable aux timing attacks

# === Choix d'algorithmes ===
# RECOMMANDÉ (2024+):
# - Chiffrement symétrique: AES-256-GCM, ChaCha20-Poly1305
# - Chiffrement asymétrique: RSA-2048+, ECC-256+
# - Signatures: Ed25519, RSA-PSS
# - Hash: SHA-256, SHA-3, BLAKE2
# - KDF: PBKDF2 (480k+ iter), Scrypt, Argon2

# DÉPRÉCIÉ (éviter):
# - MD5, SHA1 (cassés)
# - DES, 3DES (faibles)
# - RSA-1024 (trop court)
# - ECB mode (patterns visibles)

# === Validation d'entrées ===
# [OK] Toujours valider longueurs
# [OK] Vérifier types (bytes vs str)
# [OK] Gérer exceptions proprement

def safe_decrypt(ciphertext: bytes, key: bytes) -> bytes:
    """Déchiffrement avec validation"""
    if not isinstance(ciphertext, bytes):
        raise TypeError("Ciphertext must be bytes")
    
    if len(key) != 32:
        raise ValueError("Key must be 32 bytes")
    
    if len(ciphertext) < 28:  # nonce(12) + tag(16)
        raise ValueError("Ciphertext too short")
    
    try:
        # Déchiffrement
        return decrypt_data(ciphertext, key)
    except Exception as e:
        raise ValueError(f"Decryption failed: {e}")


[OK] GESTION DES ERREURS

from cryptography.exceptions import (
    InvalidSignature,
    InvalidTag,
    AlreadyFinalized,
    NotYetFinalized,
    InvalidKey,
    UnsupportedAlgorithm
)

# === Gestion d'erreurs de signature ===
try:
    public_key.verify(signature, message, padding.PSS(...), hashes.SHA256())
    print("Signature valide")
except InvalidSignature:
    print("Signature invalide!")
except Exception as e:
    print(f"Erreur: {e}")

# === Gestion d'erreurs de déchiffrement ===
try:
    plaintext = cipher.decrypt(ciphertext)
except InvalidTag:
    print("Authentication failed - données modifiées!")
except Exception as e:
    print(f"Erreur de déchiffrement: {e}")

# === Gestion d'erreurs de hash ===
try:
    digest = hashes.Hash(hashes.SHA256())
    digest.update(b"data")
    hash1 = digest.finalize()
    # digest.update(b"more")  # Lève AlreadyFinalized
except AlreadyFinalized:
    print("Hash déjà finalisé!")

# === Wrapper sécurisé ===
def safe_operation(func):
    """Décorateur pour opérations crypto"""
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except InvalidSignature:
            return None, "Invalid signature"
        except InvalidTag:
            return None, "Invalid authentication tag"
        except InvalidKey:
            return None, "Invalid key"
        except Exception as e:
            return None, f"Error: {str(e)}"
    return wrapper

@safe_operation
def verify_signed_data(data, signature, public_key):
    public_key.verify(signature, data, ...)
    return True, "Valid"


[OK] PERFORMANCE & OPTIMISATION

# === Réutiliser objets crypto ===
# Bon (réutilise cipher)
aesgcm = AESGCM(key)
for message in messages:
    nonce = os.urandom(12)
    encrypted = aesgcm.encrypt(nonce, message, None)

# Moins bon (recrée à chaque fois)
for message in messages:
    aesgcm = AESGCM(key)  # Coûteux
    encrypted = aesgcm.encrypt(os.urandom(12), message, None)

# === Hash de gros fichiers ===
def hash_large_file(filename, chunk_size=8192):
    """Hash fichier par chunks (efficace en mémoire)"""
    digest = hashes.Hash(hashes.SHA256())
    
    with open(filename, 'rb') as f:
        while chunk := f.read(chunk_size):
            digest.update(chunk)
    
    return digest.finalize().hex()

# === Chiffrement streaming ===
def encrypt_stream(input_file, output_file, key):
    """Chiffre fichier en streaming"""
    nonce = os.urandom(12)
    aesgcm = AESGCM(key)
    
    # Écrire nonce en premier
    with open(output_file, 'wb') as out:
        out.write(nonce)
        
        with open(input_file, 'rb') as inp:
            while chunk := inp.read(1024 * 1024):  # 1MB chunks
                encrypted_chunk = aesgcm.encrypt(nonce, chunk, None)
                out.write(encrypted_chunk)
                # Note: en prod, utiliser mode streaming approprié

# === Parallélisation ===
from concurrent.futures import ThreadPoolExecutor

def hash_files_parallel(filenames):
    """Hash plusieurs fichiers en parallèle"""
    with ThreadPoolExecutor() as executor:
        results = executor.map(hash_large_file, filenames)
    return list(results)

# === Benchmark ===
import time

def benchmark_algorithm(func, *args, iterations=1000):
    """Benchmark une opération crypto"""
    start = time.time()
    for _ in range(iterations):
        func(*args)
    elapsed = time.time() - start
    
    print(f"{func.__name__}: {elapsed:.3f}s pour {iterations} ops")
    print(f"Moyenne: {elapsed/iterations*1000:.3f}ms par op")

# Usage
key = os.urandom(32)
data = b"x" * 1024  # 1KB

benchmark_algorithm(lambda: AESGCM(key).encrypt(os.urandom(12), data, None))


[OK] INTÉGRATION & CAS D'USAGE

# === 1. API REST sécurisée ===
class SecureAPI:
    """Authentification API avec tokens signés"""
    
    def __init__(self, secret_key: bytes):
        self.secret_key = secret_key
    
    def generate_api_token(self, user_id: int, permissions: list) -> str:
        """Génère token API"""
        payload = {
            'user_id': user_id,
            'permissions': permissions,
            'exp': int(time.time()) + 86400,  # 24h
            'jti': secrets.token_hex(16),  # Token ID unique
        }
        
        payload_json = json.dumps(payload).encode()
        
        # Signer avec HMAC
        h = hmac.HMAC(self.secret_key, hashes.SHA256())
        h.update(payload_json)
        signature = h.finalize()
        
        # Combiner
        token_data = payload_json + b'.' + signature
        return base64.urlsafe_b64encode(token_data).decode()
    
    def verify_api_token(self, token: str) -> dict:
        """Vérifie et extrait payload du token"""
        token_data = base64.urlsafe_b64decode(token)
        payload_json, signature = token_data.rsplit(b'.', 1)
        
        # Vérifier signature
        h = hmac.HMAC(self.secret_key, hashes.SHA256())
        h.update(payload_json)
        h.verify(signature)
        
        # Extraire payload
        payload = json.loads(payload_json)
        
        # Vérifier expiration
        if payload['exp'] < time.time():
            raise ValueError("Token expired")
        
        return payload

# === 2. Chiffrement de base de données ===
class EncryptedDBField:
    """Chiffre les champs sensibles en DB"""
    
    def __init__(self, encryption_key: bytes):
        self.cipher = Fernet(encryption_key)
    
    def encrypt_field(self, value: str) -> str:
        """Chiffre une valeur pour stockage"""
        if value is None:
            return None
        encrypted = self.cipher.encrypt(value.encode())
        return base64.b64encode(encrypted).decode()
    
    def decrypt_field(self, encrypted_value: str) -> str:
        """Déchiffre une valeur depuis DB"""
        if encrypted_value is None:
            return None
        encrypted = base64.b64decode(encrypted_value)
        decrypted = self.cipher.decrypt(encrypted)
        return decrypted.decode()

# Usage avec SQLAlchemy exemple
class User:
    def __init__(self, encryptor):
        self.encryptor = encryptor
        self._email = None
        self._ssn = None
    
    @property
    def email(self):
        return self.encryptor.decrypt_field(self._email)
    
    @email.setter
    def email(self, value):
        self._email = self.encryptor.encrypt_field(value)

# === 3. Signature de documents ===
class DocumentSigner:
    """Signe des documents avec horodatage"""
    
    def __init__(self):
        self.private_key = ed25519.Ed25519PrivateKey.generate()
        self.public_key = self.private_key.public_key()
    
    def sign_document(self, document_path: str) -> dict:
        """Signe un document et retourne métadonnées"""
        # Hash du document
        doc_hash = hash_large_file(document_path)
        
        # Créer payload avec timestamp
        payload = {
            'file': os.path.basename(document_path),
            'hash': doc_hash,
            'timestamp': datetime.utcnow().isoformat(),
            'algorithm': 'Ed25519'
        }
        
        payload_json = json.dumps(payload).encode()
        
        # Signer
        signature = self.private_key.sign(payload_json)
        
        return {
            'payload': payload,
            'signature': base64.b64encode(signature).decode(),
            'public_key': base64.b64encode(
                self.public_key.public_bytes(
                    encoding=serialization.Encoding.Raw,
                    format=serialization.PublicFormat.Raw
                )
            ).decode()
        }
    
    @staticmethod
    def verify_document(document_path: str, signature_data: dict) -> bool:
        """Vérifie la signature d'un document"""
        # Recalculer hash
        doc_hash = hash_large_file(document_path)
        
        payload = signature_data['payload']
        if doc_hash != payload['hash']:
            return False
        
        # Vérifier signature
        public_key = ed25519.Ed25519PublicKey.from_public_bytes(
            base64.b64decode(signature_data['public_key'])
        )
        signature = base64.b64decode(signature_data['signature'])
        payload_json = json.dumps(payload).encode()
        
        try:
            public_key.verify(signature, payload_json)
            return True
        except Exception:
            return False

# === 4. Authentification à deux facteurs (TOTP) ===
# Note: utiliser pyotp pour TOTP complet, ceci est éducatif
import struct
import hmac as stdlib_hmac

def generate_totp_secret() -> str:
    """Génère secret pour TOTP"""
    return base64.b32encode(os.urandom(20)).decode()

def generate_totp(secret: str, interval: int = 30) -> str:
    """Génère code TOTP 6 chiffres"""
    # Timestamp
    counter = int(time.time() // interval)
    
    # HMAC-SHA1
    key = base64.b32decode(secret)
    msg = struct.pack('>Q', counter)
    h = stdlib_hmac.new(key, msg, 'sha1').digest()
    
    # Extraire code
    offset = h[-1] & 0x0F
    code = struct.unpack('>I', h[offset:offset+4])[0] & 0x7FFFFFFF
    
    return str(code % 1000000).zfill(6)

def verify_totp(secret: str, code: str, window: int = 1) -> bool:
    """Vérifie code TOTP (avec fenêtre de tolérance)"""
    for i in range(-window, window + 1):
        interval = int(time.time() // 30) + i
        expected = generate_totp(secret, 30)
        if code == expected:
            return True
    return False

# === 5. Sealed Box (chiffrement anonyme) ===
class SealedBox:
    """Chiffrement où seul le destinataire peut déchiffrer"""
    
    def __init__(self):
        self.private_key = x25519.X25519PrivateKey.generate()
        self.public_key = self.private_key.public_key()
    
    def seal(self, plaintext: bytes, recipient_public_key: bytes) -> bytes:
        """Chiffre pour un destinataire (anonyme)"""
        # Générer clé éphémère
        ephemeral_key = x25519.X25519PrivateKey.generate()
        ephemeral_public = ephemeral_key.public_key()
        
        # Calculer secret partagé
        recipient_key = x25519.X25519PublicKey.from_public_bytes(
            recipient_public_key
        )
        shared_secret = ephemeral_key.exchange(recipient_key)
        
        # Dériver clé de chiffrement
        encryption_key = HKDF(
            algorithm=hashes.SHA256(),
            length=32,
            salt=None,
            info=b'sealed box',
        ).derive(shared_secret)
        
        # Chiffrer
        aesgcm = AESGCM(encryption_key)
        nonce = os.urandom(12)
        ciphertext = aesgcm.encrypt(nonce, plaintext, None)
        
        # Format: ephemeral_public(32) + nonce(12) + ciphertext
        ephemeral_bytes = ephemeral_public.public_bytes(
            encoding=serialization.Encoding.Raw,
            format=serialization.PublicFormat.Raw
        )
        return ephemeral_bytes + nonce + ciphertext
    
    def unseal(self, sealed_data: bytes) -> bytes:
        """Déchiffre sealed box"""
        # Extraire composants
        ephemeral_public_bytes = sealed_data[:32]
        nonce = sealed_data[32:44]
        ciphertext = sealed_data[44:]
        
        # Recalculer secret partagé
        ephemeral_public = x25519.X25519PublicKey.from_public_bytes(
            ephemeral_public_bytes
        )
        shared_secret = self.private_key.exchange(ephemeral_public)
        
        # Dériver clé
        encryption_key = HKDF(
            algorithm=hashes.SHA256(),
            length=32,
            salt=None,
            info=b'sealed box',
        ).derive(shared_secret)
        
        # Déchiffrer
        aesgcm = AESGCM(encryption_key)
        plaintext = aesgcm.decrypt(nonce, ciphertext, None)
        return plaintext

# Usage
recipient = SealedBox()
recipient_public = recipient.public_key.public_bytes(
    encoding=serialization.Encoding.Raw,
    format=serialization.PublicFormat.Raw
)

sender = SealedBox()
sealed = sender.seal(b"Anonymous message", recipient_public)
plaintext = recipient.unseal(sealed)


[OK] TESTS & DEBUGGING

# === Tests unitaires ===
import unittest

class TestCrypto(unittest.TestCase):
    
    def test_encryption_decryption(self):
        """Test chiffrement/déchiffrement"""
        key = AESGCM.generate_key(bit_length=256)
        aesgcm = AESGCM(key)
        
        plaintext = b"Test message"
        nonce = os.urandom(12)
        
        ciphertext = aesgcm.encrypt(nonce, plaintext, None)
        decrypted = aesgcm.decrypt(nonce, ciphertext, None)
        
        self.assertEqual(plaintext, decrypted)
    
    def test_signature_verification(self):
        """Test signature/vérification"""
        private_key = ed25519.Ed25519PrivateKey.generate()
        public_key = private_key.public_key()
        
        message = b"Message to sign"
        signature = private_key.sign(message)
        
        # Doit passer
        try:
            public_key.verify(signature, message)
            verified = True
        except Exception:
            verified = False
        
        self.assertTrue(verified)
        
        # Doit échouer avec mauvais message
        with self.assertRaises(InvalidSignature):
            public_key.verify(signature, b"Wrong message")
    
    def test_key_derivation(self):
        """Test dérivation de clé"""
        password = "test_password"
        key1, salt = derive_key_pbkdf2(password)
        
        # Même password + salt = même clé
        kdf = PBKDF2HMAC(
            algorithm=hashes.SHA256(),
            length=32,
            salt=salt,
            iterations=480000,
        )
        key2 = kdf.derive(password.encode())
        
        self.assertEqual(key1, key2)

# === Debugging ===
def debug_crypto_operation(operation_name, func, *args):
    """Debug helper pour opérations crypto"""
    print(f"\n=== {operation_name} ===")
    try:
        result = func(*args)
        print(f"[OK] Success")
        print(f"Result type: {type(result)}")
        if isinstance(result, bytes):
            print(f"Result length: {len(result)} bytes")
            print(f"Result (hex): {result[:32].hex()}...")
        return result
    except Exception as e:
        print(f"[X] Error: {type(e).__name__}: {e}")
        import traceback
        traceback.print_exc()
        return None

# Usage
key = os.urandom(32)
plaintext = b"Test data"

def encrypt_test():
    aesgcm = AESGCM(key)
    return aesgcm.encrypt(os.urandom(12), plaintext, None)

ciphertext = debug_crypto_operation("AES-GCM Encryption", encrypt_test)


[OK] COMPATIBILITÉ & INTEROPÉRABILITÉ

# === Exporter pour OpenSSL ===
def export_for_openssl(private_key):
    """Exporte clé au format OpenSSL"""
    # Format PEM traditionnel
    pem = private_key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.TraditionalOpenSSL,
        encryption_algorithm=serialization.NoEncryption()
    )
    return pem

# === Charger depuis OpenSSL ===
def load_openssl_key(pem_data: bytes, password: bytes = None):
    """Charge clé depuis format OpenSSL"""
    try:
        return serialization.load_pem_private_key(pem_data, password)
    except Exception:
        # Essayer format SSH
        return serialization.load_ssh_private_key(pem_data, password)

# === Formats de clés publiques ===
# SubjectPublicKeyInfo (standard)
public_pem = public_key.public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo
)

# OpenSSH format
public_ssh = public_key.public_bytes(
    encoding=serialization.Encoding.OpenSSH,
    format=serialization.PublicFormat.OpenSSH
)

# === Interop avec autres langages ===
# JavaScript/Node.js: utiliser même formats (PEM, base64)
# Java: KeyFactory avec PKCS8EncodedKeySpec
# Go: crypto/x509 ParsePKCS8PrivateKey

# Exemple: Préparer pour JavaScript
def prepare_for_javascript(data: bytes) -> str:
    """Encode données pour JavaScript"""
    return base64.b64encode(data).decode()

# === PEM helpers ===
def extract_pem_type(pem_data: bytes) -> str:
    """Identifie type de PEM"""
    if b'BEGIN PRIVATE KEY' in pem_data:
        return 'PRIVATE_KEY'
    elif b'BEGIN ENCRYPTED PRIVATE KEY' in pem_data:
        return 'ENCRYPTED_PRIVATE_KEY'
    elif b'BEGIN PUBLIC KEY' in pem_data:
        return 'PUBLIC_KEY'
    elif b'BEGIN CERTIFICATE' in pem_data:
        return 'CERTIFICATE'
    elif b'BEGIN RSA PRIVATE KEY' in pem_data:
        return 'RSA_PRIVATE_KEY'
    else:
        return 'UNKNOWN'


[OK] RESSOURCES & RÉFÉRENCES

# === Documentation officielle ===
# https://cryptography.io/en/latest/

# === Tutoriels recommandés ===
# - Cryptography Recipes: https://cryptography.io/en/latest/hazmat/primitives/
# - Fernet Documentation: https://cryptography.io/en/latest/fernet/
# - X.509 Certificates: https://cryptography.io/en/latest/x509/

# === Standards & RFCs ===
# - RFC 5869: HMAC-based Extract-and-Expand KDF (HKDF)
# - RFC 8439: ChaCha20 and Poly1305
# - RFC 3394: AES Key Wrap
# - RFC 5280: X.509 Public Key Infrastructure
# - RFC 7517: JSON Web Key (JWK)
# - RFC 8017: PKCS #1 RSA Cryptography

# === Recommandations OWASP ===
# - Password Storage: PBKDF2 480,000+ iterations
# - Key Sizes: AES-256, RSA-2048+, ECC-256+
# - Hash: SHA-256 minimum

# === Outils complémentaires ===
# - PyNaCl: Interface plus simple (libsodium)
# - cryptography + pyca: Ecosystem complet
# - secrets: Génération tokens/passwords
# - hashlib: Hash standards Python

# === Commandes utiles ===
# Générer clé RSA avec OpenSSL
# openssl genrsa -out private.pem 2048
# openssl rsa -in private.pem -pubout -out public.pem

# Générer certificat auto-signé
# openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 365

# Vérifier certificat
# openssl x509 -in cert.pem -text -noout

# Tester connexion SSL/TLS
# openssl s_client -connect example.com:443


[OK] CHANGELOG & VERSIONS

# cryptography 3.x -> 40.x
# - Ajout: ChaCha20Poly1305
# - Ajout: Ed25519, X25519
# - Déprécié: DSA key generation
# - Amélioré: Performance AES-GCM

# Python 3.6+: secrets module
# Python 3.9+: Improved typing hints
# Python 3.11+: Performance improvements


[OK] CHECKLIST SÉCURITÉ

# Avant de déployer en production:
# [WHITE_SQUARE] Clés générées avec os.urandom() ou secrets
# [WHITE_SQUARE] Clés stockées sécurisement (env vars, KMS)
# [WHITE_SQUARE] Passwords hashed avec KDF (PBKDF2, Scrypt)
# [WHITE_SQUARE] Nonces/IVs uniques pour chaque chiffrement
# [WHITE_SQUARE] Utilisation de modes authentifiés (GCM, Poly1305)
# [WHITE_SQUARE] Tailles de clés appropriées (256-bit min)
# [WHITE_SQUARE] Validation des entrées utilisateur
# [WHITE_SQUARE] Gestion d'erreurs appropriée
# [WHITE_SQUARE] Logging sans exposer secrets
# [WHITE_SQUARE] Tests de sécurité effectués
# [WHITE_SQUARE] Code review par expert crypto
# [WHITE_SQUARE] Dépendances à jour (pip list --outdated)
# [WHITE_SQUARE] Rotation de clés planifiée
# [WHITE_SQUARE] Backup des clés sécurisé
# [WHITE_SQUARE] Documentation maintenue

# Questions à se poser:
# - Qu'arrive-t-il si la clé est compromise?
# - Les données anciennes peuvent-elles être déchiffrées?
# - Comment gérer la rotation de clés?
# - Que faire en cas de perte de clé?
# - Les logs contiennent-ils des secrets?


# FIN DU CHEATSHEET CRYPTOGRAPHY