Below, a message is wrapped in three encryption layers — one for each relay — and then forwarded hop by hop. Click Wrap to build the onion, then Next Hop to peel one layer at each relay.
<p class="hint">A message is wrapped in <b>3 encryption layers</b> — one per relay. Click <b>Wrap</b> to build the onion, then <b>Next Hop</b> to peel one layer at each relay. Watch what each node sees.</p>
<div id="circuit">
<div class="node" id="node-you">
<div class="node-label">You</div>
<div class="node-ip">192.168.1.42</div>
</div>
<div class="arrow" id="arrow-0">→</div>
<div class="node relay" id="node-0">
<div class="node-label">Guard</div>
<div class="node-ip">Relay A</div>
</div>
<div class="arrow" id="arrow-1">→</div>
<div class="node relay" id="node-1">
<div class="node-label">Middle</div>
<div class="node-ip">Relay B</div>
</div>
<div class="arrow" id="arrow-2">→</div>
<div class="node relay" id="node-2">
<div class="node-label">Exit</div>
<div class="node-ip">Relay C</div>
</div>
<div class="arrow" id="arrow-3">→</div>
<div class="node" id="node-dest">
<div class="node-label">Server</div>
<div class="node-ip">example.com</div>
</div>
</div>
<div id="packet-wrap">
<div id="packet"></div>
</div>
<div class="log-box">
<div id="log-title">What each relay sees:</div>
<div id="log"></div>
</div>
<div class="btns">
<button id="btn-wrap" type="button">Wrap (build onion)</button>
<button id="btn-hop" type="button" disabled>Next Hop →</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
#circuit { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; margin-bottom: .6rem; }
.node { display: flex; flex-direction: column; align-items: center; justify-content: center;
width: 68px; min-height: 54px; border-radius: 10px; border: 2px solid #bcc5d0;
background: #eef2f6; padding: 4px; transition: background .3s, border-color .3s; }
.node.relay { background: #dde8f4; border-color: #94b4d6; }
.node.active { background: #1d3557; border-color: #1d3557; color: #fff; }
.node.done { background: #c3e6cb; border-color: #2c8c4e; }
.node-label { font-weight: 700; font-size: .8rem; }
.node-ip { font-size: .72rem; opacity: .75; }
.arrow { font-size: 1.3rem; color: #8899aa; padding: 0 2px; }
#packet-wrap { min-height: 64px; display: flex; align-items: center; justify-content: center; margin: .4rem 0; }
#packet { font-size: .78rem; font-family: ui-monospace, monospace; text-align: center;
border-radius: 8px; padding: 6px 10px; max-width: 100%; word-break: break-all;
background: #f0f4f8; border: 1px solid #c5d0dc; min-height: 48px;
display: flex; align-items: center; justify-content: center; color: #1d3557; }
.log-box { background: #f8f9fb; border: 1px solid #d5dde5; border-radius: 8px;
padding: 8px 12px; margin-bottom: .6rem; min-height: 60px; }
#log-title { font-weight: 700; font-size: .82rem; margin-bottom: 4px; color: #445; }
.log-entry { font-size: .81rem; margin: 2px 0; line-height: 1.4; }
.log-entry .who { font-weight: 700; color: #1d3557; }
.log-entry .sees { color: #444; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .4rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .4; cursor: default; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Onion routing demo — layer-by-layer encryption / decryption simulation.
// Each relay holds one "key" (a short label) and peels exactly its own layer.
// The demo is visual/educational; the "encryption" is representational, not real crypto.
const MESSAGE = 'Hello, example.com!';
const RELAYS = [
{ id: 'node-0', label: 'Guard (A)', key: 'KEY_A', knows: ['your IP', 'next: Relay B'], sees: 'Your IP + encrypted blob for B' },
{ id: 'node-1', label: 'Middle (B)', key: 'KEY_B', knows: ['prev: Relay A', 'next: Relay C'], sees: 'Encrypted blob from A + encrypted blob for C' },
{ id: 'node-2', label: 'Exit (C)', key: 'KEY_C', knows: ['prev: Relay B', 'destination: example.com'], sees: 'Plaintext message + destination' }
];
// Build a visual representation of the layered packet.
// layers[0] is innermost (plaintext), layers[2] is outermost.
function buildLayers(step) {
// step: 0=fully wrapped, 1=outer peeled (A done), 2=two peeled, 3=all peeled
const layers = [
{ color: '#c3e6cb', border: '#2c8c4e', text: '[ ' + MESSAGE + ' ]' },
{ color: '#dde8f4', border: '#4a90c4', text: 'ENC_C: [ ... ]' },
{ color: '#f0ddf4', border: '#9b59b6', text: 'ENC_B: [ ... ]' },
{ color: '#fde8c8', border: '#e07d20', text: 'ENC_A: [ ... ]' }
];
// remaining layers = 3-step outermost layers, from inside out
const remaining = layers.slice(0, Math.max(1, 4 - step));
// Display as nested labels
let s = '';
for (let i = remaining.length - 1; i >= 0; i--) {
const l = remaining[i];
s += '<span style="display:inline-block;border:1px solid ' + l.border +
';background:' + l.color + ';border-radius:5px;padding:2px 6px;margin:1px;">'
+ l.text + '</span>';
if (i > 0) s += ' ← inside';
}
return s;
}
let step = 0; // 0=initial, 1=wrapped, 2=guard done, 3=middle done, 4=exit/delivered
function highlight(id) {
document.querySelectorAll('.node').forEach(n => n.classList.remove('active'));
if (id) document.getElementById(id).classList.add('active');
}
function markDone(id) {
const el = document.getElementById(id);
el.classList.remove('active');
el.classList.add('done');
}
function setPacket(html) { document.getElementById('packet').innerHTML = html; }
function addLog(who, sees) {
const div = document.createElement('div');
div.className = 'log-entry';
div.innerHTML = '<span class="who">' + who + '</span>: <span class="sees">' + sees + '</span>';
document.getElementById('log').appendChild(div);
}
function clearLog() { document.getElementById('log').innerHTML = ''; }
document.getElementById('btn-wrap').addEventListener('click', function() {
if (step !== 0) return;
step = 1;
clearLog();
highlight('node-you');
setPacket(fmt('Wrapping in 3 layers…<br>%buildLayers%', { buildLayers: buildLayers(0) }));
addLog('You', 'Build onion: wrap plaintext in ENC_C, then ENC_B, then ENC_A');
this.disabled = true;
document.getElementById('btn-hop').disabled = false;
setTimeout(() => {
markDone('node-you');
setPacket(buildLayers(0));
}, 600);
});
document.getElementById('btn-hop').addEventListener('click', function() {
if (step === 1) {
// Guard peels outer (ENC_A)
step = 2;
highlight('node-0');
const r = RELAYS[0];
setPacket(fmt('Guard peels ENC_A…<br>%buildLayers%', { buildLayers: buildLayers(1) }));
addLog(r.label, fmt('%sees% | forwards to Relay B', { sees: r.sees }));
setTimeout(() => { markDone('node-0'); setPacket(buildLayers(1)); }, 600);
} else if (step === 2) {
// Middle peels ENC_B
step = 3;
highlight('node-1');
const r = RELAYS[1];
setPacket(fmt('Middle peels ENC_B…<br>%buildLayers%', { buildLayers: buildLayers(2) }));
addLog(r.label, fmt('%sees% | forwards to Relay C', { sees: r.sees }));
setTimeout(() => { markDone('node-1'); setPacket(buildLayers(2)); }, 600);
} else if (step === 3) {
// Exit peels ENC_C → plaintext
step = 4;
highlight('node-2');
const r = RELAYS[2];
setPacket(fmt('Exit peels ENC_C → plaintext!<br>%buildLayers%', { buildLayers: buildLayers(3) }));
addLog(r.label, fmt('%sees% | forwards to example.com', { sees: r.sees }));
setTimeout(() => {
markDone('node-2');
highlight('node-dest');
setPacket(fmt('✓ Delivered: <b>%MESSAGE%</b><br><small style="color:#555">example.com receives the message. It only sees Relay C’s IP — not yours.</small>', { MESSAGE: MESSAGE }));
addLog('Server', 'Receives "Hello, example.com!" from Relay C’s IP — your identity hidden');
document.getElementById('btn-hop').disabled = true;
setTimeout(() => markDone('node-dest'), 600);
}, 600);
}
});
document.getElementById('btn-reset').addEventListener('click', function() {
step = 0;
document.querySelectorAll('.node').forEach(n => { n.classList.remove('active', 'done'); });
setPacket('Click <b>Wrap</b> to begin.');
clearLog();
document.getElementById('btn-wrap').disabled = false;
document.getElementById('btn-hop').disabled = true;
});
// Initial state
setPacket('Click <b>Wrap</b> to begin.');
<p class="hint">A message is wrapped in <b>3 encryption layers</b> — one per relay. Click <b>Wrap</b> to build the onion, then <b>Next Hop</b> to peel one layer at each relay. Watch what each node sees.</p>
<div id="circuit">
<div class="node" id="node-you">
<div class="node-label">You</div>
<div class="node-ip">192.168.1.42</div>
</div>
<div class="arrow" id="arrow-0">→</div>
<div class="node relay" id="node-0">
<div class="node-label">Guard</div>
<div class="node-ip">Relay A</div>
</div>
<div class="arrow" id="arrow-1">→</div>
<div class="node relay" id="node-1">
<div class="node-label">Middle</div>
<div class="node-ip">Relay B</div>
</div>
<div class="arrow" id="arrow-2">→</div>
<div class="node relay" id="node-2">
<div class="node-label">Exit</div>
<div class="node-ip">Relay C</div>
</div>
<div class="arrow" id="arrow-3">→</div>
<div class="node" id="node-dest">
<div class="node-label">Server</div>
<div class="node-ip">example.com</div>
</div>
</div>
<div id="packet-wrap">
<div id="packet"></div>
</div>
<div class="log-box">
<div id="log-title">What each relay sees:</div>
<div id="log"></div>
</div>
<div class="btns">
<button id="btn-wrap" type="button">Wrap (build onion)</button>
<button id="btn-hop" type="button" disabled>Next Hop →</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
#circuit { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; margin-bottom: .6rem; }
.node { display: flex; flex-direction: column; align-items: center; justify-content: center;
width: 68px; min-height: 54px; border-radius: 10px; border: 2px solid #bcc5d0;
background: #eef2f6; padding: 4px; transition: background .3s, border-color .3s; }
.node.relay { background: #dde8f4; border-color: #94b4d6; }
.node.active { background: #1d3557; border-color: #1d3557; color: #fff; }
.node.done { background: #c3e6cb; border-color: #2c8c4e; }
.node-label { font-weight: 700; font-size: .8rem; }
.node-ip { font-size: .72rem; opacity: .75; }
.arrow { font-size: 1.3rem; color: #8899aa; padding: 0 2px; }
#packet-wrap { min-height: 64px; display: flex; align-items: center; justify-content: center; margin: .4rem 0; }
#packet { font-size: .78rem; font-family: ui-monospace, monospace; text-align: center;
border-radius: 8px; padding: 6px 10px; max-width: 100%; word-break: break-all;
background: #f0f4f8; border: 1px solid #c5d0dc; min-height: 48px;
display: flex; align-items: center; justify-content: center; color: #1d3557; }
.log-box { background: #f8f9fb; border: 1px solid #d5dde5; border-radius: 8px;
padding: 8px 12px; margin-bottom: .6rem; min-height: 60px; }
#log-title { font-weight: 700; font-size: .82rem; margin-bottom: 4px; color: #445; }
.log-entry { font-size: .81rem; margin: 2px 0; line-height: 1.4; }
.log-entry .who { font-weight: 700; color: #1d3557; }
.log-entry .sees { color: #444; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .4rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .4; cursor: default; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Onion routing demo — layer-by-layer encryption / decryption simulation.
// Each relay holds one "key" (a short label) and peels exactly its own layer.
// The demo is visual/educational; the "encryption" is representational, not real crypto.
const MESSAGE = 'Hello, example.com!';
const RELAYS = [
{ id: 'node-0', label: 'Guard (A)', key: 'KEY_A', knows: ['your IP', 'next: Relay B'], sees: 'Your IP + encrypted blob for B' },
{ id: 'node-1', label: 'Middle (B)', key: 'KEY_B', knows: ['prev: Relay A', 'next: Relay C'], sees: 'Encrypted blob from A + encrypted blob for C' },
{ id: 'node-2', label: 'Exit (C)', key: 'KEY_C', knows: ['prev: Relay B', 'destination: example.com'], sees: 'Plaintext message + destination' }
];
// Build a visual representation of the layered packet.
// layers[0] is innermost (plaintext), layers[2] is outermost.
function buildLayers(step) {
// step: 0=fully wrapped, 1=outer peeled (A done), 2=two peeled, 3=all peeled
const layers = [
{ color: '#c3e6cb', border: '#2c8c4e', text: '[ ' + MESSAGE + ' ]' },
{ color: '#dde8f4', border: '#4a90c4', text: 'ENC_C: [ ... ]' },
{ color: '#f0ddf4', border: '#9b59b6', text: 'ENC_B: [ ... ]' },
{ color: '#fde8c8', border: '#e07d20', text: 'ENC_A: [ ... ]' }
];
// remaining layers = 3-step outermost layers, from inside out
const remaining = layers.slice(0, Math.max(1, 4 - step));
// Display as nested labels
let s = '';
for (let i = remaining.length - 1; i >= 0; i--) {
const l = remaining[i];
s += '<span style="display:inline-block;border:1px solid ' + l.border +
';background:' + l.color + ';border-radius:5px;padding:2px 6px;margin:1px;">'
+ l.text + '</span>';
if (i > 0) s += ' ← inside';
}
return s;
}
let step = 0; // 0=initial, 1=wrapped, 2=guard done, 3=middle done, 4=exit/delivered
function highlight(id) {
document.querySelectorAll('.node').forEach(n => n.classList.remove('active'));
if (id) document.getElementById(id).classList.add('active');
}
function markDone(id) {
const el = document.getElementById(id);
el.classList.remove('active');
el.classList.add('done');
}
function setPacket(html) { document.getElementById('packet').innerHTML = html; }
function addLog(who, sees) {
const div = document.createElement('div');
div.className = 'log-entry';
div.innerHTML = '<span class="who">' + who + '</span>: <span class="sees">' + sees + '</span>';
document.getElementById('log').appendChild(div);
}
function clearLog() { document.getElementById('log').innerHTML = ''; }
document.getElementById('btn-wrap').addEventListener('click', function() {
if (step !== 0) return;
step = 1;
clearLog();
highlight('node-you');
setPacket(fmt('Wrapping in 3 layers…<br>%buildLayers%', { buildLayers: buildLayers(0) }));
addLog('You', 'Build onion: wrap plaintext in ENC_C, then ENC_B, then ENC_A');
this.disabled = true;
document.getElementById('btn-hop').disabled = false;
setTimeout(() => {
markDone('node-you');
setPacket(buildLayers(0));
}, 600);
});
document.getElementById('btn-hop').addEventListener('click', function() {
if (step === 1) {
// Guard peels outer (ENC_A)
step = 2;
highlight('node-0');
const r = RELAYS[0];
setPacket(fmt('Guard peels ENC_A…<br>%buildLayers%', { buildLayers: buildLayers(1) }));
addLog(r.label, fmt('%sees% | forwards to Relay B', { sees: r.sees }));
setTimeout(() => { markDone('node-0'); setPacket(buildLayers(1)); }, 600);
} else if (step === 2) {
// Middle peels ENC_B
step = 3;
highlight('node-1');
const r = RELAYS[1];
setPacket(fmt('Middle peels ENC_B…<br>%buildLayers%', { buildLayers: buildLayers(2) }));
addLog(r.label, fmt('%sees% | forwards to Relay C', { sees: r.sees }));
setTimeout(() => { markDone('node-1'); setPacket(buildLayers(2)); }, 600);
} else if (step === 3) {
// Exit peels ENC_C → plaintext
step = 4;
highlight('node-2');
const r = RELAYS[2];
setPacket(fmt('Exit peels ENC_C → plaintext!<br>%buildLayers%', { buildLayers: buildLayers(3) }));
addLog(r.label, fmt('%sees% | forwards to example.com', { sees: r.sees }));
setTimeout(() => {
markDone('node-2');
highlight('node-dest');
setPacket(fmt('✓ Delivered: <b>%MESSAGE%</b><br><small style="color:#555">example.com receives the message. It only sees Relay C’s IP — not yours.</small>', { MESSAGE: MESSAGE }));
addLog('Server', 'Receives "Hello, example.com!" from Relay C’s IP — your identity hidden');
document.getElementById('btn-hop').disabled = true;
setTimeout(() => markDone('node-dest'), 600);
}, 600);
}
});
document.getElementById('btn-reset').addEventListener('click', function() {
step = 0;
document.querySelectorAll('.node').forEach(n => { n.classList.remove('active', 'done'); });
setPacket('Click <b>Wrap</b> to begin.');
clearLog();
document.getElementById('btn-wrap').disabled = false;
document.getElementById('btn-hop').disabled = true;
});
// Initial state
setPacket('Click <b>Wrap</b> to begin.');
Notice what each relay sees: only the layer it is allowed to peel, and only the address of the next hop. The entry relay (Guard) knows your real IP but not the destination. The exit relay knows the destination but not who you are. The middle relay knows neither. Only by compromising all three relays simultaneously — and correlating traffic timing — could an adversary link sender to destination.
Comments
Loading comments...