Type a message below. The demo picks a truly random key the same length as your message and XORs them together — that is the entire cipher. XOR the ciphertext with the same key and your message comes back exactly.
<p class="hint">The one-time pad XORs each bit of your message with a bit of a truly random key. Pick a key as long as the message and it is <b>provably</b> unbreakable. Shorten it or reuse it and the secrecy shatters.</p>
<label class="fld">Message <input id="msg" type="text" value="ATTACK" maxlength="8"></label>
<div class="row">
<button id="rand" type="button">Fresh random key</button>
<label class="chk"><input id="reuse" type="checkbox"> Reuse the same key on a 2nd message</label>
</div>
<div id="reuse2" class="hidden">
<label class="fld">2nd message <input id="msg2" type="text" value="RETREAT" maxlength="8"></label>
</div>
<pre id="out" class="out"></pre>
<button id="break" type="button" class="ghost">What can an attacker learn?</button>
<div class="status" id="status"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.fld { display: block; font-size: .85rem; color: #1d3557; margin: .5rem 0; font-weight: 600; }
.fld input { display: block; margin-top: .25rem; font: 600 15px ui-monospace, monospace;
padding: .4rem .6rem; border: 1px solid #cdd9e3; border-radius: 8px; width: 100%;
max-width: 260px; text-transform: uppercase; }
.row { display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; margin: .5rem 0; }
.chk { font-size: .85rem; color: #444; font-weight: 500; }
.hidden { display: none; }
.out { background: #0f1f2e; color: #d6e4f0; padding: .8rem; border-radius: 8px;
font: 13px/1.6 ui-monospace, monospace; overflow-x: auto; white-space: pre; margin: .6rem 0; }
.out .k { color: #ffd166; }
.out .c { color: #ef8a8a; }
.out .p { color: #8ce0a8; }
.status { font-size: .92rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; line-height: 1.45; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; margin: .2rem .4rem .2rem 0; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// One-time pad: ciphertext = message XOR key, byte by byte. With a TRULY RANDOM
// key as long as the message, used exactly once, Shannon (1949) proved this gives
// PERFECT SECRECY: the ciphertext reveals nothing about the message — every
// plaintext of that length is equally likely. Two failures break it:
// (1) a key SHORTER than the message must repeat -> structure leaks;
// (2) REUSING a key on two messages -> XOR of ciphertexts = XOR of plaintexts,
const norm = s => (s.toUpperCase().replace(/[^A-Z]/g,'') || 'A').slice(0,8);
let key = [];
function freshKey(n){ key = []; for(let i=0;i<n;i++) key.push(Math.floor(Math.random()*256)); }
const bytes = s => [...s].map(c => c.charCodeAt(0));
const xor = (a,b) => a.map((v,i) => v ^ b[i % b.length]);
const hex = arr => arr.map(v => v.toString(16).padStart(2,'0').toUpperCase()).join(' ');
function render(){
const m = norm(document.getElementById('msg').value);
const reuse = document.getElementById('reuse').checked;
document.getElementById('reuse2').className = reuse ? '' : 'hidden';
if(key.length < m.length) freshKey(m.length);
const p = bytes(m);
const k = key.slice(0, m.length);
const c = xor(p, k);
let html =
fmt(`<span class="p">message %m%</span>
plaintext %hex%
<span class="k">key (XOR) %hex2%</span>
<span class="c">ciphertext %hex3%</span>
decrypt %fromCharCode%`, { m: m.padEnd(8), hex: hex(p), hex2: hex(k), hex3: hex(c), fromCharCode: String.fromCharCode(...xor(c,k)) });
if(reuse){
const m2 = norm(document.getElementById('msg2').value);
const p2 = bytes(m2);
const n = Math.min(p2.length, key.length);
const c2 = xor(p2.slice(0,n), key.slice(0,n));
html += "\n\n" + fmt(`<span class="c">2nd ciphertext (same key) %hex%</span>`, { hex: hex(c2) });
}
document.getElementById('out').innerHTML = html;
setStatus('','');
}
function setStatus(t,cls){ const s=document.getElementById('status'); s.innerHTML=t; s.className='status '+cls; }
function attack(){
const reuse = document.getElementById('reuse').checked;
if(!reuse){
setStatus(`✓ Single use, random key as long as the message: the attacker sees only random bytes. Every possible plaintext of this length is equally consistent — nothing leaks. That is perfect secrecy.`,'ok');
return;
}
// Reuse attack: c1 XOR c2 = p1 XOR p2 (the key cancels).
const m1 = norm(document.getElementById('msg').value);
const m2 = norm(document.getElementById('msg2').value);
const n = Math.min(m1.length, m2.length, key.length);
const p1 = bytes(m1).slice(0,n), p2 = bytes(m2).slice(0,n);
const k = key.slice(0,n);
const c1 = xor(p1,k), c2 = xor(p2,k);
const cdiff = c1.map((v,i)=> v ^ c2[i]);
const pdiff = p1.map((v,i)=> v ^ p2[i]);
const leaked = cdiff.every((v,i)=> v === pdiff[i]);
setStatus(fmt(`✗ Key reused! The attacker XORs the two ciphertexts and the key cancels out:
c1 ⊕ c2 = %hex% = p1 ⊕ p2 %v%
The random key is gone — only the messages remain, and they leak.`, { hex: hex(cdiff), v: leaked ? `(exactly!)` : '' }), 'bad');
}
['msg','msg2'].forEach(id => document.getElementById(id).addEventListener('input', render));
document.getElementById('reuse').addEventListener('change', render);
document.getElementById('rand').addEventListener('click', ()=>{ const m=norm(document.getElementById('msg').value); freshKey(Math.max(m.length, norm(document.getElementById('msg2').value).length)); render(); });
document.getElementById('break').addEventListener('click', attack);
freshKey(8);
render();
<p class="hint">The one-time pad XORs each bit of your message with a bit of a truly random key. Pick a key as long as the message and it is <b>provably</b> unbreakable. Shorten it or reuse it and the secrecy shatters.</p>
<label class="fld">Message <input id="msg" type="text" value="ATTACK" maxlength="8"></label>
<div class="row">
<button id="rand" type="button">Fresh random key</button>
<label class="chk"><input id="reuse" type="checkbox"> Reuse the same key on a 2nd message</label>
</div>
<div id="reuse2" class="hidden">
<label class="fld">2nd message <input id="msg2" type="text" value="RETREAT" maxlength="8"></label>
</div>
<pre id="out" class="out"></pre>
<button id="break" type="button" class="ghost">What can an attacker learn?</button>
<div class="status" id="status"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.fld { display: block; font-size: .85rem; color: #1d3557; margin: .5rem 0; font-weight: 600; }
.fld input { display: block; margin-top: .25rem; font: 600 15px ui-monospace, monospace;
padding: .4rem .6rem; border: 1px solid #cdd9e3; border-radius: 8px; width: 100%;
max-width: 260px; text-transform: uppercase; }
.row { display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; margin: .5rem 0; }
.chk { font-size: .85rem; color: #444; font-weight: 500; }
.hidden { display: none; }
.out { background: #0f1f2e; color: #d6e4f0; padding: .8rem; border-radius: 8px;
font: 13px/1.6 ui-monospace, monospace; overflow-x: auto; white-space: pre; margin: .6rem 0; }
.out .k { color: #ffd166; }
.out .c { color: #ef8a8a; }
.out .p { color: #8ce0a8; }
.status { font-size: .92rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; line-height: 1.45; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; margin: .2rem .4rem .2rem 0; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// One-time pad: ciphertext = message XOR key, byte by byte. With a TRULY RANDOM
// key as long as the message, used exactly once, Shannon (1949) proved this gives
// PERFECT SECRECY: the ciphertext reveals nothing about the message — every
// plaintext of that length is equally likely. Two failures break it:
// (1) a key SHORTER than the message must repeat -> structure leaks;
// (2) REUSING a key on two messages -> XOR of ciphertexts = XOR of plaintexts,
const norm = s => (s.toUpperCase().replace(/[^A-Z]/g,'') || 'A').slice(0,8);
let key = [];
function freshKey(n){ key = []; for(let i=0;i<n;i++) key.push(Math.floor(Math.random()*256)); }
const bytes = s => [...s].map(c => c.charCodeAt(0));
const xor = (a,b) => a.map((v,i) => v ^ b[i % b.length]);
const hex = arr => arr.map(v => v.toString(16).padStart(2,'0').toUpperCase()).join(' ');
function render(){
const m = norm(document.getElementById('msg').value);
const reuse = document.getElementById('reuse').checked;
document.getElementById('reuse2').className = reuse ? '' : 'hidden';
if(key.length < m.length) freshKey(m.length);
const p = bytes(m);
const k = key.slice(0, m.length);
const c = xor(p, k);
let html =
fmt(`<span class="p">message %m%</span>
plaintext %hex%
<span class="k">key (XOR) %hex2%</span>
<span class="c">ciphertext %hex3%</span>
decrypt %fromCharCode%`, { m: m.padEnd(8), hex: hex(p), hex2: hex(k), hex3: hex(c), fromCharCode: String.fromCharCode(...xor(c,k)) });
if(reuse){
const m2 = norm(document.getElementById('msg2').value);
const p2 = bytes(m2);
const n = Math.min(p2.length, key.length);
const c2 = xor(p2.slice(0,n), key.slice(0,n));
html += "\n\n" + fmt(`<span class="c">2nd ciphertext (same key) %hex%</span>`, { hex: hex(c2) });
}
document.getElementById('out').innerHTML = html;
setStatus('','');
}
function setStatus(t,cls){ const s=document.getElementById('status'); s.innerHTML=t; s.className='status '+cls; }
function attack(){
const reuse = document.getElementById('reuse').checked;
if(!reuse){
setStatus(`✓ Single use, random key as long as the message: the attacker sees only random bytes. Every possible plaintext of this length is equally consistent — nothing leaks. That is perfect secrecy.`,'ok');
return;
}
// Reuse attack: c1 XOR c2 = p1 XOR p2 (the key cancels).
const m1 = norm(document.getElementById('msg').value);
const m2 = norm(document.getElementById('msg2').value);
const n = Math.min(m1.length, m2.length, key.length);
const p1 = bytes(m1).slice(0,n), p2 = bytes(m2).slice(0,n);
const k = key.slice(0,n);
const c1 = xor(p1,k), c2 = xor(p2,k);
const cdiff = c1.map((v,i)=> v ^ c2[i]);
const pdiff = p1.map((v,i)=> v ^ p2[i]);
const leaked = cdiff.every((v,i)=> v === pdiff[i]);
setStatus(fmt(`✗ Key reused! The attacker XORs the two ciphertexts and the key cancels out:
c1 ⊕ c2 = %hex% = p1 ⊕ p2 %v%
The random key is gone — only the messages remain, and they leak.`, { hex: hex(cdiff), v: leaked ? `(exactly!)` : '' }), 'bad');
}
['msg','msg2'].forEach(id => document.getElementById(id).addEventListener('input', render));
document.getElementById('reuse').addEventListener('change', render);
document.getElementById('rand').addEventListener('click', ()=>{ const m=norm(document.getElementById('msg').value); freshKey(Math.max(m.length, norm(document.getElementById('msg2').value).length)); render(); });
document.getElementById('break').addEventListener('click', attack);
freshKey(8);
render();