← Back to Docs

🔐 Hybrid Cryptography Diagrams

1️⃣ Hybrid Cryptography Overview

SENDER Step 1: Generate Session Key session_key = RANDOM(32 bytes) Step 2: Asymmetric Encryption X25519 + Sealed Box enc_session_key = Encrypt(session_key, receiver_public_key) Step 3: Symmetric Encryption ChaCha20-Poly1305 AEAD ciphertext+tag = Encrypt(message, session_key) Step 4: Send to Receiver Secure Transmission mTLS RECEIVER Step 1: Receive Encrypted Message Step 2: Asymmetric Decryption X25519 + Sealed Box session_key = Decrypt(enc_session_key, private_key) Step 3: Verify and Decrypt ChaCha20-Poly1305 AEAD 1. Verify Tag (Authentication) 2. Decrypt(ciphertext, session_key) Step 4: Original Message Recovered ✓
Asymmetric Encryption (X25519)
Symmetric Encryption (ChaCha20)
Data Transmission

2️⃣ Detailed Encryption Flow (Sender)

Input Plaintext: "Hello World" 1. Generate Session Key session_key = os.urandom(32) 256-bit random key 2. Encrypt Session Key X25519 + Sealed Box sealed_box = SealedBox(public_key) enc_session_key = sealed_box.encrypt() 3. Generate Nonce nonce = os.urandom(12) 96-bit random nonce 4. ChaCha20 Encryption cipher = ChaCha20Poly1305(session_key) result = cipher.encrypt(nonce, plaintext) ↓ result = ciphertext || tag (n bytes) || (16 bytes) 🔑 Key Usage Receiver Public Key (X25519) 32 bytes - for Session Key encryption Session Key (ChaCha20) 32 bytes - for main message encryption Nonce (ChaCha20) 12 bytes - unique for each message 📦 Final Output (JSON) message_id: "sender-abc-receiver" enc_session_key: "base64..." nonce: "base64..." ciphertext: "base64..." ← includes tag sent_time: "2025-10-15T..."
Important Note: Session Key is used only for one message and then discarded (Forward Secrecy).

3️⃣ Detailed Decryption Flow (Receiver)

📬 Receive Encrypted Message enc_session_key: "base64..." nonce: "base64..." ciphertext: "base64..." (includes tag) 1. Decrypt Session Key sealed_box = SealedBox(private_key) session_key = sealed_box.decrypt(enc_session_key) Using X25519 private key 2. Automatic Tag Separation ⚙️ Happens inside cipher.decrypt(): ciphertext = data[:-16] tag_received = data[-16:] ⚠️ You don't do anything! Library handles it 3. Verify and Decrypt cipher = ChaCha20Poly1305(session_key) plaintext = cipher.decrypt(nonce, ciphertext_with_tag) ✓ Tag verified, message is valid 🔍 Tag Verification Details 1. Compute tag_computed from ciphertext tag_computed = Poly1305(ciphertext) 2. Compare with tag_received if tag_computed == tag_received ✓ Valid Decrypt ✗ Invalid Reject
⚠️ Key Note: In version 2.0, you don't write any additional code for tag separation. The cryptography library handles everything automatically and securely.

4️⃣ Detailed Ciphertext + Tag Structure

Byte Structure of cipher.encrypt() Output Ciphertext n bytes (length of original message) Tag 16 bytes (fixed) 0 n-1 n n+15 Example: Message "HELLO" (5 bytes) A3 F2 C1 D4 E5 5 bytes ciphertext 7A 8B … 3F 2C 16 bytes tag Total Length: 21 bytes (5 + 16)
# Example in Python: plaintext = b"HELLO" # 5 bytes cipher = ChaCha20Poly1305(session_key) result = cipher.encrypt(nonce, plaintext, None) print(f"Plaintext length: {len(plaintext)}") # 5 print(f"Result length: {len(result)}") # 21 print(f"Ciphertext: {result[:-16].hex()}") # first 5 bytes print(f"Tag: {result[-16:].hex()}") # last 16 bytes

5️⃣ Complete Message Flow from Sender to Receiver

1 Public Key of Receiver from Server 2 Session Key Random 32 bytes Generate 3 Encrypt Session Key with Public Key Message with Session Key 4 Send via TLS to Server 5 Decrypt Session Key with Private Key Message with Session Key
# Complete Encryption Flow in Sender.py # 1. Retrieve receiver's public key with open(f"keys/{receiver_id}_public.key", "r") as f: public_key_b64 = f.read().strip() # 2. Create sealed box for asymmetric encryption public_key = PublicKey(b64decode(public_key_b64)) sealed_box = SealedBox(public_key) # 3. Generate random session key session_key = os.urandom(32) # 256-bit # 4. Encrypt session key with public key enc_session_key = sealed_box.encrypt(session_key) # 5. Generate random nonce nonce = os.urandom(12) # 96-bit # 6. Encrypt message with ChaCha20-Poly1305 cipher = ChaCha20Poly1305(session_key) message_bytes = message["content"].encode('utf-8') ciphertext_with_tag = cipher.encrypt(nonce, message_bytes, None) # ↑ includes ciphertext + tag (16 bytes) # 7. Construct final message encrypted_message = { "message_id": f"{sender_id}-{correlation_id}-{receiver_id}", "receiver_client_id": receiver_id, "enc_session_key": b64encode(enc_session_key).decode('utf-8'), "nonce": b64encode(nonce).decode('utf-8'), "ciphertext": b64encode(ciphertext_with_tag).decode('utf-8'), "sent_time": datetime.now(timezone.utc).isoformat() } # 8. Send to server command = f"publish {exchange_name} {routing_key} {json.dumps(encrypted_message)}\n" writer.write(command.encode('utf-8'))
# Complete Decryption Flow in Receiver.py # 1. Receive encrypted message message_data = json.loads(message_str) # 2. Decrypt session key with private key enc_session_key = b64decode(message_data["enc_session_key"]) sealed_box = SealedBox(PRIVATE_KEY) session_key = sealed_box.decrypt(enc_session_key) # 3. Extract nonce and ciphertext+tag nonce = b64decode(message_data["nonce"]) ciphertext_with_tag = b64decode(message_data["ciphertext"]) # 4. Decrypt and verify message cipher = ChaCha20Poly1305(session_key) try: plaintext_bytes = cipher.decrypt(nonce, ciphertext_with_tag, None) # ↑ Inside this function: # - Tag is separated # - Tag is verified # - If valid, decrypts plaintext = plaintext_bytes.decode('utf-8') print(f"Message received: {plaintext}") except InvalidTag: print("❌ Message tampered or corrupted!")