A general-purpose encrypted communication tool inspired by the foundational
Hiding Routing Information
paper (Goldschlag, Reed & Syverson, 1996)
Onion Router is a secure desktop application that demonstrates onion routing — the anonymous-communication architecture that later evolved into Tor. It wraps messages in multiple layers of 1,024-bit RSA + AES-256-CBC encryption, routes them hop-by-hop through a simulated network, and peels one cryptographic layer at each node — ensuring that no single node ever learns both the sender and the recipient.
The application is built across three languages, each handling a distinct layer of the security stack:
| Layer | Language | Artifact | Responsibility |
|---|---|---|---|
| 🔐 Cryptographic Core | C (OpenSSL) | libonion_crypto.so |
RSA-1024, AES-256-CBC, SHA-256, CSPRNG; fixed-size onion construction & peeling |
| 🔀 Routing Engine | C++17 | libonion_engine.so |
5-node network, virtual circuit management, replay detection, trace logging |
| 🖥️ User Interface | Python 3 + GTK 3 | main.py |
Animated network visualization, message console, real-time encryption log |
⚠️ Disclaimer: This is an educational and demonstrative implementation faithful to the 1996 paper. It runs entirely on a single machine and is not a hardened anonymity system — do not use it to protect real-world traffic.
- Overview
- Features
- Architecture Deep Dive
- System Requirements
- Installation & Build
- Usage Guide
- Project Structure
- Component Integration
- Testing
- Security Notes & Limitations
- Academic Reference
- License
- Layered onion encryption — each hop adds/removes a cryptographic layer using RSA-1024 + AES-256-CBC, exactly as the paper describes
- Fixed-size 4,096-byte onions — random padding ensures that onion size never reveals the number of remaining hops
- Bidirectional virtual circuits — forward and backward symmetric key pairs enable anonymous two-way communication
- Replay detection — SHA-256 fingerprinting rejects duplicate onions, preventing replay attacks
- Expiration enforcement — onions carry timestamps and are automatically rejected after their TTL expires
- Real-time visualization — Cairo-rendered animated network view shows the onion traveling hop-by-hop with pulsing nodes
- Color-coded encryption log — every cryptographic operation (
BUILD,PEEL,FORWARD,CRYPT_FWD/BWD,DELIVER,REPLAY) is logged in real time - Node key inspection — examine the RSA-1024 public key of any node in the network
- Dark security-console aesthetic — Adwaita-dark theme with terminal-green accents
Every onion is a fixed 4,096-byte blob, so its size never leaks how many hops remain (the paper's constant-size requirement). Each layer is structured as:
┌──────────────────────────────────────────────────────────────────────┐
│ ONION LAYER (4096 bytes) │
├─────────────────────────┬────────────────────────────────────────────┤
│ 128-byte RSA-1024 │ 3968-byte AES-256-CBC region │
│ encrypted block │ │
│ │ │ ┌─────────┬──────────┬──────────────┐ │
│ │ │ │ Header │ Inner │ Random │ │
│ ▼ │ │ (route │ Onion │ Padding │ │
│ Wraps a 48-byte │ │ info) │ (next │ (constant │ │
│ session secret: │ │ │ layer) │ size) │ │
│ ┌──────────────────┐ │ └─────────┴──────────┴──────────────┘ │
│ │ 32-byte AES key │ │ │
│ │ 16-byte IV │ │ │
│ └──────────────────┘ │ │
└─────────────────────────┴────────────────────────────────────────────┘
The header carries everything a node needs and nothing it does not:
| Field | Description |
|---|---|
exp_time |
Expiration timestamp — onions are rejected after this time |
next_hop |
ID of the next routing node (or NULL if this is the final destination) |
Ff, Kf |
Forward stream cryptographic function and symmetric key |
Fb, Kb |
Backward stream cryptographic function and symmetric key |
When a routing node receives an onion, it performs these operations in sequence:
Encrypted Onion Routing Node X Smaller Onion
┌─────┐ ┌─────┐
│█████│ ──► 1. RSA-decrypt outer block │████ │
│█████│ 2. AES-decrypt body with │████ │
│█████│ recovered session key │████ │
│█████│ 3. Check expiration time │ │ + random padding
│█████│ 4. Reject replays (SHA-256 DB) │ │ → back to 4096B
│█████│ 5. Store circuit state └─────┘
└─────┘ 6. Forward re-padded onion ──────► to next hop
A node learns only its immediate predecessor and successor — never the full route, the total number of hops, or its position in the chain.
Once the onion has traversed the route and each node has stored its circuit state, the hops form a virtual circuit:
┌───────────┐ forward keys ┌───────┐ forward keys ┌───────────┐
│ Initiator │ ═══════════════════► │ Node │ ═══════════════════► │ Responder │
│ Proxy │ ◄═══════════════════ │ (×N) │ ◄═══════════════════ │ Proxy │
└───────────┘ backward keys └───────┘ backward keys └───────────┘
- Forward direction: data is pre-crypted by the initiator's proxy (applying inverses of all forward keys, innermost first) so that each hop peels exactly one layer
- Backward direction: the responder's proxy crypts the reply once, and each intermediate node adds another layer; the initiator strips them all
The demo builds a fixed five-node topology (as labelled in the paper's Figure 1):
X ─────── U
╱ ╲ ╱ ╲
W ╲ ╱ ╲
╲ ╲ ╱ ╲
╲ Y ─────── Z
╲ ╱
▼
(W, Z = Proxy/Routing Nodes)
(X, Y, U = Routing Nodes)
| Node | Role | Description |
|---|---|---|
| W | Proxy/Routing | Initiator's entry point — constructs onions, manages the forward/backward stream |
| X | Routing | Intermediate relay node |
| Y | Routing | Intermediate relay node |
| Z | Proxy/Routing | Responder's exit point — delivers plaintext to the destination |
| U | Routing | Intermediate relay node |
| Component | Requirement |
|---|---|
| OS | Linux (Ubuntu 20.04+ recommended) |
| C Compiler | GCC with C11 support |
| C++ Compiler | G++ with C++17 support |
| Build System | CMake ≥ 3.15 (fallback compiler invocation in run.sh) |
| Crypto Library | OpenSSL development headers (libssl-dev) |
| GUI Toolkit | GTK 3 with PyGObject (python3-gi, python3-gi-cairo) |
| Python | Python 3.8+ |
sudo apt-get update && sudo apt-get install -y \
build-essential cmake libssl-dev \
python3-gi python3-gi-cairo gir1.2-gtk-3.0 \
libgirepository1.0-dev pkg-config💡 Python interpreter note: The GUI needs a Python that can
import gi(PyGObject). On many systems, the distro interpreter/usr/bin/python3has this, whilepyenv/condaPythons may not.run.shautomatically probes for a GTK-capable interpreter; you can force one with:ONION_PYTHON=/usr/bin/python3 ./run.sh
git clone https://gitlab.com/BASSCIOP/onion-router.git
cd onion-router
./run.shThis builds both native shared libraries and launches the GTK GUI.
# Build + launch GUI (default)
./run.sh
# Build + run C/C++ test suites only
./run.sh --test
# Skip build, launch GUI with existing libraries
./run.sh --no-buildmake # Configure (CMake) + build shared libraries and tests
make test # Build, then run C and C++ test suites
make run # Build, then launch the GUI
make clean # Remove all build artifactscmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j$(nproc)
ctest --test-dir build --output-on-failureBuild artifacts are placed in:
build/lib/—libonion_crypto.so,libonion_engine.sobuild/bin/—test_crypto,test_engine
Select a Source (proxy) and Destination from the dropdown menus. Optionally specify intermediate Hops (e.g., X,Y) and set a Circuit TTL in seconds.
Click Build Circuit to watch the initiator's proxy wrap one RSA+AES layer per hop. The network view animates the onion traveling through nodes, and the encryption log fills with color-coded events.
Type a message and click Send Through Onion. The message is delivered over the forward stream to the exit node, and an acknowledgement returns via the backward stream.
Click Replay CREATE to re-inject the original onion and observe a node detecting and rejecting the replay from its seen-onion hash table — demonstrating the paper's anti-replay mechanism.
Click Destroy to tear down the virtual circuit, propagating DESTROY commands through all participating nodes.
Click the Node Keys button in the header bar to view the RSA-1024 public key (PEM format) of any node in the network.
| Tag | Color | Meaning |
|---|---|---|
BUILD |
🟢 Green | Proxy wraps a layer (RSA + AES) |
PEEL |
🔵 Cyan | Node removes its encryption layer |
FORWARD |
🟡 Yellow | Onion re-padded and relayed to next hop |
CIRCUIT |
🟣 Purple | Circuit state stored in node's table |
CRYPT_FWD/BWD |
🔷 Teal | Stream (de)cipher applied per hop |
DELIVER |
🟢 Bright Green | Payload delivered / recovered |
REPLAY |
🔴 Red | Replayed onion detected and rejected |
EXPIRE |
🟠 Orange | Expired onion dropped |
ERROR |
🔴 Red | Operation failed |
onion_router_app/
│
├── 📁 src/
│ ├── 📁 crypto/ # ── C Cryptographic Core ──
│ │ ├── onion_crypto.h # RSA-1024, AES-256-CBC, SHA-256, CSPRNG
│ │ ├── onion_crypto.c # (272 lines) key gen, encrypt, decrypt, hash
│ │ ├── onion_builder.h # Onion build/peel interface + constants
│ │ └── onion_builder.c # (298 lines) layered onion construction
│ │
│ ├── 📁 engine/ # ── C++17 Routing Engine ──
│ │ ├── routing_node.hpp/.cpp # Node: peel onion, verify expiry, replay-check
│ │ ├── circuit_manager.hpp/.cpp # Circuit table: ID → {connections, keys}
│ │ └── onion_router.hpp/.cpp # 5-node network + extern "C" ABI for ctypes
│ │
│ └── 📁 gui/ # ── Python / GTK 3 Front-End ──
│ ├── main.py # Application entry point (Gtk.Application)
│ ├── app_window.py # Main window, dark theme, event orchestration
│ ├── network_view.py # Cairo-rendered animated network graph
│ ├── message_panel.py # Message compose/send + color-coded log
│ └── crypto_utils.py # ctypes bindings to both .so libraries
│
├── 📁 tests/
│ ├── test_crypto.c # C unit tests for crypto primitives
│ ├── test_engine.cpp # C++ unit tests for routing engine
│ └── test_gui_smoke.py # Headless (xvfb) GUI integration test
│
├── 📁 docs/
│ └── screenshot.png # Application screenshot
│
├── CMakeLists.txt # CMake build configuration
├── Makefile # Convenience wrapper around CMake
├── run.sh # Build + launch script with auto-detection
├── requirements.txt # Python dependencies (PyGObject, pycairo)
├── LICENSE # MIT License
└── README.md # This file
Codebase: ~3,800 lines across 18 source files (806 C, 1,158 C++, 1,596 Python, 254 test code).
┌─────────────────────────────────────────────────────────────┐
│ Python / GTK 3 GUI │
│ main.py → app_window.py → network_view.py │
│ message_panel.py │
│ │ │
│ ctypes (FFI) │
│ │ │
│ ▼ │
│ crypto_utils.OnionEngine │
│ (Python wrapper for the C ABI) │
└────────────────────────┬────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────┐
│ libonion_engine.so (C++17) │
│ │
│ OnionRouter ── RoutingNode ── CircuitManager │
│ │ │
│ │ extern "C" ABI: orouter_create(), orouter_send() │
│ │ orouter_reply(), orouter_destroy(), etc. │
│ │ │
│ │ Every operation emits a JSON trace for the GUI │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────┐ │
│ │ libonion_crypto.so (C) │ │
│ │ │ │
│ │ RSA-1024 │ AES-256-CBC │ SHA-256 │ │
│ │ CSPRNG │ Onion Build │ Padding │ │
│ └────────────────────────────────────────┘ │
│ (OpenSSL 3.x EVP API) │
└────────────────────────────────────────────────────────────┘
The engine exposes a small extern "C" ABI (orouter_*) that the Python OnionEngine class wraps. Every operation produces a JSON trace — a list of steps ({step, type, node, circuit_id, layer, bytes, detail}) — which the GUI replays to animate the network and populate the encryption log.
The project includes three test suites:
| Test | Type | What It Validates |
|---|---|---|
test_crypto |
C unit test | RSA-1024 keygen/encrypt/decrypt, AES-256-CBC round-trip, SHA-256 digests, onion build/peel integrity, constant-size padding |
test_engine |
C++ unit test | Node creation, circuit table management, full onion routing through 5-node network, replay detection, expiration enforcement |
test_gui_smoke |
Python integration | Headless GTK window instantiation, engine loading, circuit build, message send, replay rejection, destroy — all via xvfb-run |
Run all tests:
# Via run.sh
./run.sh --test
# Via Make
make test
# Via CTest
ctest --test-dir build --output-on-failure| Aspect | Status |
|---|---|
| Key management | Keys are generated fresh in-process each run; no persistent key distribution or PKI |
| RSA-1024 | Used to match the era of the 1996 paper; not adequate for real-world confidentiality today (minimum RSA-2048 recommended) |
| Network simulation | Everything runs in a single process on one host — there is no real network transport |
| Traffic analysis | No traffic padding, no dummy messages, no latency smoothing — a global passive adversary would trivially correlate traffic |
| Forward secrecy | Not implemented — compromise of a node's long-term RSA key exposes past sessions |
| Side channels | No timing attack mitigations or constant-time comparisons beyond what OpenSSL provides |
This is a teaching tool, not a production anonymity network. For real-world anonymous communication, use Tor.
D. M. Goldschlag, M. G. Reed, P. F. Syverson.
"Hiding Routing Information"
Workshop on Information Hiding, Cambridge, UK, May 1996.
In: R. Anderson (Ed.), Information Hiding, LNCS 1174, pp. 137–150, Springer-Verlag.
📄 Full Paper (PDF)
This paper introduced the concept of Onion Routing — the layered encryption approach that directly led to the development of Tor in 2002 and remains the foundation of anonymous communication on the Internet today.
MIT License
Copyright (c) 2026 ROBERT C BASS®
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
See LICENSE for the full text.
Built with 🧅 by ROBERT C BASS® — Inspired by the pioneers of anonymous communication
