In Part 1, we explored the “why” behind end-to-end encryption (E2EE). In Part 2, you learned to encrypt with AES—fast and safe. Part 3 brought in asymmetric encryption and digital signatures with RSA, solving the key exchange problem. Today, we combine everything: let’s build a basic, but real, Python chat demo that delivers truly private, authenticated communication.
Now, let’s bring these concepts together in a real-world project: a minimal web-based end-to-end encryption (E2EE) chat app in Python.
This isn’t just theory—you’ll get complete, working code that you can run locally and see end-to-end encryption (E2EE) in action through your browser.
Page Contents
What We’re Building
A secure chat web app where:
- Users register and get their own RSA key pairs
- Messages are encrypted with fresh AES keys for each message
- AES keys are protected using RSA encryption
- Digital signatures verify message authenticity
- The server never sees your actual messages—only encrypted data
This demonstrates the same principles used by Signal, WhatsApp, and other secure messaging platforms.
Project Setup
First, create a new directory for your project:
mkdir e2ee_chat
cd e2ee_chat
Install the required dependencies:
pip install flask cryptography
Create the following directory structure:
e2ee_chat/
├── app.py
├── templates/
│ ├── base.html
│ ├── register.html
│ ├── login.html
│ └── chat.html
├── static/
│ └── style.css
└── messages.json
The Complete Flask Application
Here’s the full app.py file with all the E2EE logic:
# app.py
from flask import Flask, render_template, request, session, jsonify, redirect, url_for
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
import json
import base64
from datetime import datetime
app = Flask(__name__)
app.secret_key = "your-super-secret-key-change-this-in-production"
# In-memory storage (use a real database in production)
users_db = {}
messages_db = []
def generate_rsa_keypair():
"""Generate a new RSA key pair"""
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048
)
public_key = private_key.public_key()
return private_key, public_key
def serialize_key(key, private=False):
"""Convert key to PEM format"""
if private:
return key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption()
)
else:
return key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
def load_public_key(pem_data):
"""Load public key from PEM data"""
return serialization.load_pem_public_key(pem_data)
def load_private_key(pem_data):
"""Load private key from PEM data"""
return serialization.load_pem_private_key(pem_data, password=None)
def encrypt_message_for_recipient(sender_private_key, recipient_public_key, plaintext):
"""
Encrypt a message using hybrid encryption:
1. Generate random AES key
2. Encrypt message with AES
3. Encrypt AES key with recipient's public key
4. Sign the encrypted message with sender's private key
"""
# Generate AES key and encrypt message
aes_key = AESGCM.generate_key(bit_length=256)
nonce = os.urandom(12)
aesgcm = AESGCM(aes_key)
ciphertext = aesgcm.encrypt(nonce, plaintext.encode(), None)
# Encrypt AES key with recipient's public key
encrypted_aes_key = recipient_public_key.encrypt(
aes_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
# Sign the ciphertext with sender's private key
signature = sender_private_key.sign(
ciphertext,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
return {
"nonce": base64.b64encode(nonce).decode(),
"ciphertext": base64.b64encode(ciphertext).decode(),
"encrypted_aes_key": base64.b64encode(encrypted_aes_key).decode(),
"signature": base64.b64encode(signature).decode()
}
def decrypt_message_from_sender(recipient_private_key, sender_public_key, encrypted_data):
"""
Decrypt a message and verify signature
"""
try:
# Decode from base64
nonce = base64.b64decode(encrypted_data["nonce"])
ciphertext = base64.b64decode(encrypted_data["ciphertext"])
encrypted_aes_key = base64.b64decode(encrypted_data["encrypted_aes_key"])
signature = base64.b64decode(encrypted_data["signature"])
# Decrypt AES key with recipient's private key
aes_key = recipient_private_key.decrypt(
encrypted_aes_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
# Verify signature
try:
sender_public_key.verify(
signature,
ciphertext,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
signature_valid = True
except:
signature_valid = False
# Decrypt message with AES key
aesgcm = AESGCM(aes_key)
plaintext = aesgcm.decrypt(nonce, ciphertext, None).decode()
return {
"success": True,
"message": plaintext,
"signature_valid": signature_valid
}
except Exception as e:
return {
"success": False,
"error": str(e)
}
@app.route('/')
def index():
if 'username' in session:
return redirect(url_for('chat'))
return redirect(url_for('login'))
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form['username']
if username in users_db:
return render_template('register.html', error="Username already exists")
# Generate key pair
private_key, public_key = generate_rsa_keypair()
# Store user data
users_db[username] = {
"public_key": serialize_key(public_key).decode(),
"private_key": serialize_key(private_key, private=True).decode()
}
session['username'] = username
return redirect(url_for('chat'))
return render_template('register.html')
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
if username not in users_db:
return render_template('login.html', error="Username not found")
session['username'] = username
return redirect(url_for('chat'))
return render_template('login.html')
@app.route('/chat')
def chat():
if 'username' not in session:
return redirect(url_for('login'))
username = session['username']
other_users = [u for u in users_db.keys() if u != username]
return render_template('chat.html',
username=username,
other_users=other_users,
public_key=users_db[username]['public_key'])
@app.route('/send_message', methods=['POST'])
def send_message():
if 'username' not in session:
return jsonify({"success": False, "error": "Not logged in"})
data = request.json
sender = session['username']
recipient = data['recipient']
message = data['message']
if recipient not in users_db:
return jsonify({"success": False, "error": "Recipient not found"})
# Load keys
sender_private_key = load_private_key(users_db[sender]['private_key'].encode())
recipient_public_key = load_public_key(users_db[recipient]['public_key'].encode())
# Encrypt message
encrypted_data = encrypt_message_for_recipient(
sender_private_key,
recipient_public_key,
message
)
# Store message
message_entry = {
"id": len(messages_db),
"sender": sender,
"recipient": recipient,
"timestamp": datetime.now().isoformat(),
"encrypted_data": encrypted_data
}
messages_db.append(message_entry)
return jsonify({"success": True, "message_id": message_entry["id"]})
@app.route('/get_messages')
def get_messages():
if 'username' not in session:
return jsonify({"success": False, "error": "Not logged in"})
username = session['username']
user_messages = []
# Get private key for decryption
recipient_private_key = load_private_key(users_db[username]['private_key'].encode())
for msg in messages_db:
if msg['recipient'] == username:
# Load sender's public key
sender_public_key = load_public_key(users_db[msg['sender']]['public_key'].encode())
# Decrypt message
decrypted = decrypt_message_from_sender(
recipient_private_key,
sender_public_key,
msg['encrypted_data']
)
if decrypted['success']:
user_messages.append({
"id": msg["id"],
"sender": msg["sender"],
"message": decrypted["message"],
"timestamp": msg["timestamp"],
"signature_valid": decrypted["signature_valid"]
})
return jsonify({"success": True, "messages": user_messages})
@app.route('/logout')
def logout():
session.pop('username', None)
return redirect(url_for('login'))
if __name__ == '__main__':
app.run(debug=True)
HTML Templates
Create these templates in the templates/ directory:
Base Template (base.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}E2E Encrypted Chat{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<div class="container">
<header>
<h1>🔒 End-to-End Encrypted Chat</h1>
</header>
<main>
{% block content %}{% endblock %}
</main>
</div>
</body>
</html>
Registration Page (register.html)
{% extends "base.html" %}
{% block title %}Register - E2E Chat{% endblock %}
{% block content %}
<div class="auth-form">
<h2>Create Account</h2>
<p>Your RSA key pair will be generated automatically for secure messaging.</p>
{% if error %}
<div class="error">{{ error }}</div>
{% endif %}
<form method="POST">
<div class="form-group">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
</div>
<button type="submit">Register & Generate Keys</button>
</form>
<p class="auth-link">
Already have an account? <a href="{{ url_for('login') }}">Login here</a>
</p>
</div>
{% endblock %}
Login Page (login.html)
{% extends "base.html" %}
{% block title %}Login - E2E Chat{% endblock %}
{% block content %}
<div class="auth-form">
<h2>Login</h2>
{% if error %}
<div class="error">{{ error }}</div>
{% endif %}
<form method="POST">
<div class="form-group">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
</div>
<button type="submit">Login</button>
</form>
<p class="auth-link">
Don't have an account? <a href="{{ url_for('register') }}">Register here</a>
</p>
</div>
{% endblock %}
Chat Interface (chat.html)
{% extends "base.html" %}
{% block title %}Chat - E2E Chat{% endblock %}
{% block content %}
<div class="chat-container">
<div class="chat-header">
<div class="user-info">
<span>Welcome, <strong>{{ username }}</strong></span>
<a href="{{ url_for('logout') }}" class="logout-btn">Logout</a>
</div>
<div class="key-info">
<details>
<summary>🔑 Your Public Key (share this with others)</summary>
<textarea readonly class="public-key">{{ public_key }}</textarea>
</details>
</div>
</div>
<div class="chat-main">
<div class="sidebar">
<h3>Send Message To:</h3>
<div class="user-list">
{% for user in other_users %}
<button class="user-btn" onclick="selectRecipient('{{ user }}')">
{{ user }}
</button>
{% endfor %}
</div>
{% if not other_users %}
<p class="no-users">No other users registered yet.</p>
{% endif %}
</div>
<div class="chat-area">
<div class="message-area">
<div id="messages" class="messages"></div>
<div class="loading" id="loading" style="display: none;">
Decrypting messages...
</div>
</div>
<div class="message-input-area">
<div class="recipient-info" id="recipient-info" style="display: none;">
<span>Sending to: <strong id="selected-recipient"></strong></span>
</div>
<div class="input-group">
<input type="text"
id="message-input"
placeholder="Select a user and type your encrypted message..."
disabled>
<button id="send-btn" onclick="sendMessage()" disabled>
🔒 Send Encrypted
</button>
</div>
</div>
</div>
</div>
</div>
<script>
let selectedRecipient = null;
let messageHistory = [];
function selectRecipient(username) {
selectedRecipient = username;
document.getElementById('selected-recipient').textContent = username;
document.getElementById('recipient-info').style.display = 'block';
document.getElementById('message-input').disabled = false;
document.getElementById('send-btn').disabled = false;
document.getElementById('message-input').placeholder = `Type your message to ${username}...`;
// Update user button states
document.querySelectorAll('.user-btn').forEach(btn => {
btn.classList.remove('selected');
});
event.target.classList.add('selected');
// Clear messages when switching users
document.getElementById('messages').innerHTML = '';
loadMessages();
}
async function sendMessage() {
const messageInput = document.getElementById('message-input');
const message = messageInput.value.trim();
if (!message || !selectedRecipient) return;
try {
const response = await fetch('/send_message', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
recipient: selectedRecipient,
message: message
})
});
const result = await response.json();
if (result.success) {
messageInput.value = '';
addMessageToUI('You', message, new Date().toLocaleTimeString(), true, true);
} else {
alert('Failed to send message: ' + result.error);
}
} catch (error) {
alert('Error sending message: ' + error.message);
}
}
async function loadMessages() {
const loading = document.getElementById('loading');
loading.style.display = 'block';
try {
const response = await fetch('/get_messages');
const result = await response.json();
if (result.success) {
result.messages.forEach(msg => {
addMessageToUI(
msg.sender,
msg.message,
new Date(msg.timestamp).toLocaleTimeString(),
msg.signature_valid,
false
);
});
}
} catch (error) {
console.error('Error loading messages:', error);
} finally {
loading.style.display = 'none';
}
}
function addMessageToUI(sender, message, timestamp, signatureValid, isOwn) {
const messagesDiv = document.getElementById('messages');
const messageDiv = document.createElement('div');
messageDiv.className = `message ${isOwn ? 'own-message' : 'other-message'}`;
const statusIcon = signatureValid ? '✅' : '⚠️';
const statusText = signatureValid ? 'Verified' : 'Unverified';
messageDiv.innerHTML = `
<div class="message-header">
<span class="sender">${sender}</span>
<span class="timestamp">${timestamp}</span>
<span class="signature-status" title="Digital signature ${statusText.toLowerCase()}">
${statusIcon} ${statusText}
</span>
</div>
<div class="message-content">${message}</div>
`;
messagesDiv.appendChild(messageDiv);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
// Handle Enter key in message input
document.getElementById('message-input').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
sendMessage();
}
});
// Auto-refresh messages every 3 seconds
setInterval(loadMessages, 3000);
// Load messages on page load
window.addEventListener('load', loadMessages);
</script>
{% endblock %}
CSS Styling
Create static/style.css for a clean, professional look:
/* static/style.css */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
min-height: 100vh;
box-shadow: 0 0 20px rgba(0,0,0,0.1);
}
header {
background: #2c3e50;
color: white;
padding: 1rem;
text-align: center;
}
header h1 {
font-size: 1.5rem;
font-weight: 600;
}
/* Auth Forms */
.auth-form {
max-width: 400px;
margin: 2rem auto;
padding: 2rem;
background: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.auth-form h2 {
text-align: center;
margin-bottom: 1rem;
color: #2c3e50;
}
.auth-form p {
text-align: center;
color: #666;
margin-bottom: 1.5rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #555;
}
.form-group input {
width: 100%;
padding: 0.8rem;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 1rem;
transition: border-color 0.3s;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
}
.auth-form button {
width: 100%;
padding: 0.8rem;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
font-size: 1rem;
cursor: pointer;
transition: background 0.3s;
}
.auth-form button:hover {
background: #5a6fd8;
}
.auth-link {
text-align: center;
margin-top: 1rem;
}
.auth-link a {
color: #667eea;
text-decoration: none;
}
.error {
background: #fee;
color: #c33;
padding: 0.8rem;
border-radius: 5px;
margin-bottom: 1rem;
border: 1px solid #fcc;
}
/* Chat Interface */
.chat-container {
display: flex;
flex-direction: column;
height: calc(100vh - 80px);
}
.chat-header {
background: #f8f9fa;
padding: 1rem;
border-bottom: 1px solid #e9ecef;
}
.user-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.logout-btn {
background: #dc3545;
color: white;
padding: 0.5rem 1rem;
text-decoration: none;
border-radius: 5px;
font-size: 0.9rem;
}
.key-info details {
margin-top: 0.5rem;
}
.key-info summary {
cursor: pointer;
color: #667eea;
font-weight: 500;
}
.public-key {
width: 100%;
height: 80px;
margin-top: 0.5rem;
font-family: monospace;
font-size: 0.8rem;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 5px;
background: #f8f9fa;
resize: none;
}
.chat-main {
display: flex;
flex: 1;
overflow: hidden;
}
.sidebar {
width: 250px;
background: #f8f9fa;
border-right: 1px solid #e9ecef;
padding: 1rem;
}
.sidebar h3 {
margin-bottom: 1rem;
color: #495057;
}
.user-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.user-btn {
padding: 0.8rem;
background: white;
border: 1px solid #ddd;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s;
text-align: left;
}
.user-btn:hover {
background: #e9ecef;
}
.user-btn.selected {
background: #667eea;
color: white;
border-color: #667eea;
}
.no-users {
color: #666;
font-style: italic;
}
.chat-area {
flex: 1;
display: flex;
flex-direction: column;
}
.message-area {
flex: 1;
padding: 1rem;
overflow-y: auto;
background: #f8f9fa;
}
.messages {
display: flex;
flex-direction: column;
gap: 1rem;
}
.message {
max-width: 70%;
padding: 1rem;
border-radius: 10px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.own-message {
align-self: flex-end;
background: #667eea;
color: white;
}
.other-message {
align-self: flex-start;
background: white;
border: 1px solid #e9ecef;
}
.message-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
font-size: 0.8rem;
}
.sender {
font-weight: 600;
}
.timestamp {
opacity: 0.7;
}
.signature-status {
font-size: 0.7rem;
padding: 0.2rem 0.5rem;
border-radius: 10px;
background: rgba(255,255,255,0.2);
}
.message-content {
line-height: 1.4;
}
.message-input-area {
background: white;
border-top: 1px solid #e9ecef;
padding: 1rem;
}
.recipient-info {
margin-bottom: 0.5rem;
color: #666;
font-size: 0.9rem;
}
.input-group {
display: flex;
gap: 0.5rem;
}
.input-group input {
flex: 1;
padding: 0.8rem;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 1rem;
}
.input-group input:focus {
outline: none;
border-color: #667eea;
}
.input-group button {
padding: 0.8rem 1.5rem;
background: #28a745;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-weight: 500;
transition: background 0.3s;
}
.input-group button:hover:not(:disabled) {
background: #218838;
}
.input-group button:disabled {
background: #ccc;
cursor: not-allowed;
}
.loading {
text-align: center;
color: #666;
font-style: italic;
padding: 1rem;
}
/* Responsive */
@media (max-width: 768px) {
.chat-main {
flex-direction: column;
}
.sidebar {
width: 100%;
max-height: 200px;
overflow-y: auto;
}
.message {
max-width: 90%;
}
}
Running the Application
- Save all the files in their respective directories
- Navigate to your project folder
- Run the Flask app:
python app.py
- Open your browser to
http://localhost:5000 - Register two different users (you can use two browser windows)
- Start sending encrypted messages between them!
What Happens Under the Hood
When you send a message, here’s the crypto magic happening:
- AES Key Generation: A random 256-bit AES key is created for this message only
- Message Encryption: Your message is encrypted with AES-GCM (authenticated encryption)
- Key Exchange: The AES key is encrypted with the recipient’s RSA public key
- Digital Signature: The encrypted message is signed with your RSA private key
- Secure Transport: Only the encrypted data is sent to the server—never your actual message
- Decryption: The recipient decrypts the AES key with their private key, then decrypts your message
- Verification: The digital signature proves the message really came from you and wasn’t tampered with
Security Features Demonstrated
- Forward Secrecy: Each message uses a different AES key
- Authentication: Digital signatures prevent impersonation
- Integrity: Any tampering with messages is detected
- Server Blindness: The server never sees your actual messages
- Non-repudiation: Senders can’t deny sending verified messages
Production Considerations
This demo shows the core concepts, but for production use, you’d need to add:
- Proper key storage: Private keys should never be stored on servers
- Key verification: Users should verify each other’s public keys out-of-band
- Transport security: Use HTTPS in production
- Database storage: Replace in-memory storage with a real database
- User authentication: Add proper login with passwords or OAuth
- Rate limiting: Prevent spam and abuse
- Message persistence: Handle message history properly
- Error handling: More robust error handling for network issues
Understanding the Code
The heart of the encryption is in the encrypt_message_for_recipient() function, which implements the same hybrid encryption used by real messaging apps:
# 1. Generate fresh AES key (symmetric encryption for speed)
aes_key = AESGCM.generate_key(bit_length=256)
# 2. Encrypt message with AES
ciphertext = aesgcm.encrypt(nonce, plaintext.encode(), None)
# 3. Encrypt AES key with recipient's public key (for secure key exchange)
encrypted_aes_key = recipient_public_key.encrypt(aes_key, padding.OAEP(...))
# 4. Sign the encrypted message (for authenticity)
signature = sender_private_key.sign(ciphertext, padding.PSS(...), hashes.SHA256())
This combines the speed of symmetric encryption with the security of asymmetric cryptography—exactly how WhatsApp, Signal, and other secure messengers work.
Try It Yourself
- Register two users and send messages between them
- Try tampering with the encrypted data to see signature verification fail
- Look at the browser’s Network tab to see that only encrypted data travels
- Examine the public keys to understand the key exchange process
What’s Next?
In Part 5 (the final part), we’ll cover production-ready security practices: secure key storage, handling key compromise, preventing common attacks, and integrating with hardware security modules.
This web app demonstrates that end-to-end encryption isn’t magic—it’s just good cryptography applied systematically. With the code above, you have a working foundation that uses the same principles as billion-user messaging platforms.
Have questions or want to see specific features added? Drop a comment below! I’d love to see how you extend this demo or where you’d use E2EE in your own projects.
Frequently Asked Questions: End-to-End Encrypted Chat Web App
Here are the most common questions readers ask about building the E2EE chat web app from Part 4:
1. Is this chat app actually secure enough for real use?
This demo demonstrates the core E2EE principles used by Signal and WhatsApp, but it’s designed for learning, not production. For real use, you’d need to add HTTPS, proper user authentication, secure key storage (never on the server), key verification mechanisms, and protection against replay attacks. The cryptographic implementation itself is sound, but the surrounding infrastructure needs hardening.
2. Why do you generate a new AES key for each message instead of reusing one?
This provides forward secrecy – if someone compromises an AES key, they can only decrypt that one message, not your entire conversation history. Real messaging apps go even further with techniques like the Signal Protocol’s “double ratchet” to continuously rotate keys.
3. Can I modify this to work with multiple users in a group chat?
Yes! For group chat, you’d encrypt each message multiple times – once for each recipient’s public key. So if you have 5 people in a group, you’d create 5 copies of the encrypted AES key (one encrypted with each person’s public key) but only one encrypted message. The server would then deliver the appropriate encrypted key to each recipient.
4. What happens if someone steals my private key?
They can decrypt all future messages sent to you and impersonate you by signing messages. This is why production apps store private keys in secure enclaves or hardware modules, and why key rotation and recovery mechanisms are crucial. In a real app, you’d want to detect compromise and revoke/rotate keys immediately.
5. Why does the signature verification sometimes show as “unverified”?
This usually happens if the message data gets corrupted during transmission or storage, or if someone tampers with the encrypted message. The signature verification failing is actually a security feature – it’s telling you the message isn’t authentic or has been modified.
6. Can I use this code in my commercial project?
The cryptographic concepts are standard, but for commercial use, you should have a security expert review the implementation. Also consider using established libraries like PyNaCl or the Signal Protocol library rather than building from scratch. Never deploy homegrown crypto in production without professional security auditing.
7. How do I handle users on different devices or browsers?
Currently, each registration creates new keys, so the same user on different devices would have different identities. Production apps solve this with key synchronization across devices (like WhatsApp’s multi-device feature) or by using deterministic key generation from a master secret that can be shared securely between a user’s devices.
8. What if the server gets hacked? Can they read my messages?
No! That’s the whole point of end-to-end encryption. The server only stores encrypted data that it cannot decrypt. However, hackers could potentially see metadata (who talks to whom, when), inject malicious code, or perform man-in-the-middle attacks if they compromise the server completely.
9. Why use RSA instead of elliptic curve cryptography (ECC)?
RSA is easier to understand for educational purposes, but you’re right that ECC is better for production – it’s faster and uses smaller keys for equivalent security. You can replace the RSA code with ECC using Python’s cryptography library’s ec module. Modern apps like Signal use Curve25519 for key exchange.
10. How would I add this to a React/Vue frontend instead of plain HTML?
You’d need to implement the cryptography in JavaScript using the Web Crypto API instead of Python. The server would remain the same (just relaying encrypted data), but all encryption/decryption would happen in the browser. Libraries like crypto-js or the native Web Crypto API can handle AES-GCM and RSA operations client-side.
11. Can I make this work without users having to share public keys manually?
Yes! You could add a “user directory” where public keys are automatically shared when users register. However, this introduces a trust problem – how do you verify that the public key actually belongs to the right person? Production apps solve this with key fingerprints, QR codes for verification, or trusted key servers with transparency logs.
12. What’s the maximum message size I can encrypt with this setup?
RSA can only encrypt small amounts of data (typically ~200 bytes for 2048-bit keys), but since we use hybrid encryption (RSA for the AES key, AES for the message), your message size is only limited by practical considerations like browser memory and network transfer limits. AES can handle gigabytes of data efficiently.
13. How do I prevent replay attacks where someone resends old encrypted messages?
Add timestamps and sequence numbers to your messages, and track which messages you’ve already received. In the signature, include not just the message content but also a timestamp and message ID. Recipients should reject messages that are too old or have been seen before.
14. Can I extend this to encrypt files or images, not just text?
Absolutely! The AES encryption works on any binary data. You’d modify the encrypt_message_for_recipient() function to handle file uploads, read the file as bytes instead of encoding text, and then encrypt those bytes. On the receiving end, you’d decrypt back to bytes and allow the user to download the file.
15. How does this compare to using HTTPS? Isn’t that already encrypted?
HTTPS encrypts data between your browser and the server, but the server can still read your messages in plain text. With E2EE, even if someone compromises the server, intercepts your network traffic, or subpoenas the hosting company, they still can’t read your actual messages. E2EE protects against threats that HTTPS cannot.
Bonus: Quick Troubleshooting Tips
- “Decryption failed” errors: Usually means key mismatch or corrupted data
- Signature always shows unverified: Check that you’re using the correct sender’s public key
- Messages not appearing: Check browser console for JavaScript errors
- Server crashes: Make sure all required packages are installed:
pip install flask cryptography

