"""
Lab 4.2 - AEAD demo and key-generation helper
Chapter 4: Cryptography and Encryption: Foundations
Section 4.11 - Inspect a handshake, generate keys, encrypt with AEAD

This script demonstrates the Python-side steps from the lab walkthrough:
  - Generating AES-256-GCM and ChaCha20-Poly1305 keys
  - Encrypting and decrypting with associated data (AEAD)
  - Generating an Ed25519 key pair and signing/verifying a message
  - Demonstrating that GCM nonce reuse is detectable (keystream XOR cancels)

Run inside the tls-tools container:
    python /lab/lab4_2_demo.py

The 'hazmat' import path is the library's own misuse-resistance warning:
you are handling hazardous materials. For application code, prefer
the high-level wrappers shown here; never compose raw cipher + mode by hand.
"""

import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM, ChaCha20Poly1305
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import (
    Encoding, PublicFormat, PrivateFormat, NoEncryption
)

SEP = "-" * 60


def demo_aesgcm():
    print(SEP)
    print("AES-256-GCM (AEAD) demo")
    print(SEP)

    key = AESGCM.generate_key(bit_length=256)
    print(f"Key (256-bit):  {key.hex()}")

    nonce = os.urandom(12)   # 96-bit nonce - unique per message
    print(f"Nonce (96-bit): {nonce.hex()}")

    plaintext = b"Chapter 4: strong primitives, weak surroundings."
    aad = b"session-id:abc123"   # authenticated but NOT encrypted
    print(f"Plaintext:      {plaintext}")
    print(f"AAD:            {aad}")

    aesgcm = AESGCM(key)
    ciphertext_with_tag = aesgcm.encrypt(nonce, plaintext, aad)
    print(f"Ciphertext+tag: {ciphertext_with_tag.hex()}")
    print(f"  (last 16 bytes = GCM authentication tag)")

    recovered = aesgcm.decrypt(nonce, ciphertext_with_tag, aad)
    print(f"Decrypted:      {recovered}")

    # Demonstrate that tampering is detected
    tampered = bytearray(ciphertext_with_tag)
    tampered[0] ^= 0xFF
    try:
        aesgcm.decrypt(nonce, bytes(tampered), aad)
        print("ERROR: tamper was not detected!")
    except Exception:
        print("Tamper detected: integrity check failed before any plaintext was returned.")

    print()


def demo_chacha20_poly1305():
    print(SEP)
    print("ChaCha20-Poly1305 (AEAD) demo")
    print(SEP)

    key = ChaCha20Poly1305.generate_key()
    print(f"Key (256-bit):  {key.hex()}")

    nonce = os.urandom(12)
    plaintext = b"ChaCha20 needs no hardware AES support."
    aad = b"v1"

    chacha = ChaCha20Poly1305(key)
    ct = chacha.encrypt(nonce, plaintext, aad)
    print(f"Ciphertext+tag: {ct.hex()}")

    recovered = chacha.decrypt(nonce, ct, aad)
    print(f"Decrypted:      {recovered}")
    print()


def demo_ed25519():
    print(SEP)
    print("Ed25519 digital signature demo")
    print(SEP)

    private_key = Ed25519PrivateKey.generate()
    public_key = private_key.public_key()

    pub_bytes = public_key.public_bytes(Encoding.Raw, PublicFormat.Raw)
    priv_bytes = private_key.private_bytes(Encoding.Raw, PrivateFormat.Raw, NoEncryption())
    print(f"Private key (32 bytes): {priv_bytes.hex()}")
    print(f"Public key  (32 bytes): {pub_bytes.hex()}")
    print("(A 3072-bit RSA key would be ~384 bytes - 12x larger for the same security)")

    message = b"Signed with Ed25519 - deterministic nonce, no per-sig randomness needed."
    signature = private_key.sign(message)
    print(f"Signature   (64 bytes): {signature.hex()}")

    # Verify
    try:
        public_key.verify(signature, message)
        print("Signature verified OK.")
    except Exception as e:
        print(f"Verification failed: {e}")

    # Demonstrate determinism: signing the same message twice gives the same sig
    sig2 = private_key.sign(message)
    print(f"Signing again produces the same signature: {signature == sig2}")
    print("(ECDSA would produce a different random k each time - the foot-gun)")
    print()


def demo_nonce_reuse_danger():
    print(SEP)
    print("GCM nonce-reuse demonstration (why uniqueness is mandatory)")
    print(SEP)

    key = AESGCM.generate_key(bit_length=256)
    aesgcm = AESGCM(key)

    REUSED_NONCE = b"\x00" * 12  # deliberately reused - never do this

    pt1 = b"plaintext message one - secrets inside"
    pt2 = b"plaintext message two - more secrets!"

    ct1 = aesgcm.encrypt(REUSED_NONCE, pt1, None)
    ct2 = aesgcm.encrypt(REUSED_NONCE, pt2, None)

    # Strip the 16-byte authentication tag (last 16 bytes) to get raw ciphertext
    raw1 = ct1[:-16]
    raw2 = ct2[:-16]
    min_len = min(len(raw1), len(raw2))

    # XOR the two ciphertexts: keystream cancels, leaving XOR of plaintexts
    xored = bytes(a ^ b for a, b in zip(raw1[:min_len], raw2[:min_len]))
    # XOR with known plaintext 1 to recover plaintext 2
    recovered_pt2 = bytes(a ^ b for a, b in zip(xored, pt1[:min_len]))

    print(f"Plaintext 2 recovered via nonce-reuse XOR: {recovered_pt2[:min_len]!r}")
    print("(The GCM auth key is also recoverable - enabling forgery of any message)")
    print("Fix: ensure every nonce is unique per key. Use a counter, not zero.")
    print()


if __name__ == "__main__":
    demo_aesgcm()
    demo_chacha20_poly1305()
    demo_ed25519()
    demo_nonce_reuse_danger()
    print("Lab 4.2 demo complete.")
