SecureS2S
End-to-end encrypted service-to-service communication using RSA-2048 + AES-256-GCM hybrid cryptography over plain TCP sockets.

Hybrid Cryptographic Protocol (RSA + AES)
Pure asymmetric encryption is computationally expensive for bulk data (RSA-2048 encrypts ~250 bytes per operation). Pure symmetric encryption cannot solve the key distribution problem — two peers with no prior shared secret cannot agree on a key without an out-of-band channel.
Implemented a hybrid protocol where RSA-2048 is used exclusively for a one-time session key exchange, and AES-256-GCM handles all subsequent message traffic.
public_key_bytes = send_socket.recv(4096)
server_public_key = crypto.load_public_key(public_key_bytes)
session_key = crypto.generate_aes_key()
encrypted_session_key = crypto.encrypt_session_key(server_public_key, session_key)
send_socket.sendall(encrypted_session_key)conn.sendall(public_key_bytes)
encrypted_session_key = conn.recv(4096)
session_key = crypto.decrypt_session_key(private_key, encrypted_session_key)The RSA key pair is ephemeral — generated fresh on each run. While the current implementation does not preserve the private key across restarts, the architecture cleanly separates the asymmetric key-exchange layer from the symmetric encryption layer, making it straightforward to swap in ECDHE in the future. A pure-RSA approach for all messages would waste CPU and cap message size at the key modulus.
Inherits the performance characteristics of AES-GCM (hardware-accelerated on modern x86 via AES-NI) for all application data, while solving the initial key distribution securely.
RSA-OAEP with SHA-256
Textbook RSA (raw encryption without padding) is deterministic and malleable — an attacker can encrypt guesses and compare ciphertexts, or manipulate ciphertexts to produce predictable plaintext changes.
Applied OAEP (Optimal Asymmetric Encryption Padding) with SHA-256 as both the hash and MGF1 hash function.
def encrypt_session_key(public_key, session_key):
encrypted_key = public_key.encrypt(
session_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
return encrypted_keyOAEP introduces randomness and redundancy so that the same plaintext encrypts to different ciphertexts each time, and decryption fails (with high probability) if the ciphertext was tampered with. The alternative considered was PKCS#1 v1.5 padding, which is simpler but has known padding-oracle attack surfaces (Bleichenbacher, Manger). OAEP eliminates these classes of attacks at the cost of ~10 extra bytes per encryption.
Eliminates padding-oracle attacks. Follows NIST-recommended configurations for a 128-bit security level, establishing a secure initial wrapper for the symmetric session key.
AES-256-GCM Authenticated Encryption with Per-Message Nonces
Encryption without integrity checking (e.g., AES-CBC + HMAC separately) complicates the implementation and can introduce subtle bugs like padding-oracle attacks or tag-verification timing leaks.
Used AES-GCM, an authenticated encryption (AEAD) mode that combines confidentiality and integrity in a single primitive. Each message generates a fresh 96-bit nonce via os.urandom(12) and appends it to the ciphertext.
def aes_gcm_encrypt(session_key, plaintext: bytes):
aesgcm = AESGCM(session_key)
nonce = os.urandom(12)
ciphertext = aesgcm.encrypt(nonce, plaintext, None)
return nonce + ciphertextdef aes_gcm_decrypt(session_key, payload: bytes):
aesgcm = AESGCM(session_key)
nonce = payload[:12]
ciphertext = payload[12:]
plaintext = aesgcm.decrypt(nonce, ciphertext, None)
return plaintextNIST SP 800-38D recommends a 96-bit nonce for AES-GCM as the optimal trade-off between security and message-size limits. Using kernel-level entropy (os.urandom) instead of a counter means nonce reuse is probabilistically infeasible — you would need roughly 2^48 messages for a 50% collision probability. The alternative (AES-CBC + HMAC-SHA256 in encrypt-then-MAC order) would require careful coordination of two keys and two primitives. GCM eliminates this coordination risk by design.
Confidentiality and integrity guaranteed without coordination risk. Nonce reuse attacks are mathematically prevented. Any tampered ciphertext is rejected automatically during decryption.
Full-Duplex Communication with Thread Gating
TCP sockets are inherently symmetrical but Python's input() blocks the calling thread. If the send and receive loops share a single thread, the user cannot receive messages while typing, or cannot send while waiting for a message.
Split the communication into two daemon threads per server — one dedicated to receiving, one to sending. The key establishment is coordinated with a threading.Event so that messages are not sent before the session key is available.
t_recv = threading.Thread(target=receive_handler, daemon=True)
t_send = threading.Thread(target=send_handler, daemon=True)
t_recv.start()
t_send.start()
t_recv.join()def send_handler():
global send_socket_global
key_established.wait()
# Proceed to connect or send only after the key is securedA single-threaded select/poll-based event loop would work but adds complexity (selectors module, non-blocking I/O, manual buffering) with no benefit for a two-peer interactive chat. Threading keeps each logical path (send, receive) as a simple blocking loop, making the code readable and correct by construction.
True full-duplex communication without complex event loops or asynchronous I/O overhead. Daemons ensure the application exits cleanly when the main thread shuts down.
Automatic Delivery Acknowledgements (__ACK__ Protocol)
TCP guarantees delivery at the transport layer but gives no application-level signal that the remote peer has read and decrypted a message. A successful sendall() only means the kernel accepted the data — not that it was processed successfully.
Built a lightweight application-level ACK protocol. When a peer receives a non-ACK message, it replies with the literal __ACK__ encrypted under the session key. The sender displays "(Arrived)" when it decrypts an __ACK__.
safe_print(f"[Server B]: {msg_text}")
ack_payload = crypto.aes_gcm_encrypt(session_key, b"__ACK__")
send_socket_global.sendall(ack_payload)if msg_text == "__ACK__":
safe_print(" (Arrived)")The __ACK__ protocol verifies that the entire cryptographic pipeline — decrypt, decode, display — completed successfully. A corrupted ciphertext, a wrong key, or a deserialization error would all prevent the ACK from being sent, so the sender knows immediately that delivery failed. The __ACK__ messages themselves are encrypted, so a passive network observer cannot distinguish ACKs from data messages by pattern alone.
Application-level certainty of message delivery and successful decryption, strictly more informative than a TCP transport ACK.
Thread-Safe Console Output
When two threads write to stdout simultaneously (one printing an incoming message, one refreshing the input() prompt), the output overlaps and garbles the display.
Implemented a safe_print() function that acquires a threading.Lock, clears the current input line with a carriage return and spaces, writes the message, then re-draws the prompt.
print_lock = threading.Lock()
def safe_print(msg):
with print_lock:
sys.stdout.write('\r' + ' ' * 80 + '\r')
print(msg)
sys.stdout.write(PROMPT)
sys.stdout.flush()The print_lock serializes all terminal output so that message printing and prompt drawing never interleave. The carriage return + spaces technique overwrites any partial user input that was being typed — this is the standard solution for single-line terminal interfaces where you do not want a full curses dependency.
Clean, non-garbled console UI despite asynchronous incoming messages and active user typing.
Key Technical Achievements
Hybrid Cryptography
Successfully bridged the key distribution problem using RSA-2048, while retaining the high-performance bulk encryption speed of AES-256-GCM for subsequent messages.
Robust Padding Defenses
Defending against oracle attacks (Bleichenbacher, Manger) by replacing legacy PKCS#1 v1.5 padding with OAEP and SHA-256.
AEAD Security Guarantees
Authenticated encryption with AES-GCM and probabilistically unique nonces ensures that message tampering and replay attacks are mathematically prevented.
Verifiable Delivery
Custom encrypted application-level ACKs provide absolute certainty that a message was not just delivered, but successfully decrypted.
Clean Multithreading
Thread gating and safe console I/O enable true full-duplex operation without the heavy footprint of asynchronous event loops or curses libraries.