The demo below simulates AES-GCM authenticated encryption (using a simplified toy version safe for illustration). Type a message, encrypt it, then click any ciphertext byte to flip a bit and watch the authentication tag fail.
<!-- AEAD toy demo — simulates encrypt + MAC-over-ciphertext (Encrypt-then-MAC) -->
<p class="hint">Type a message and encrypt it. Each colored byte is part of the ciphertext — click any byte to flip a bit. Watch the <b>authentication tag</b> change color: green = intact, red = tampered. Then try to decrypt.</p>
<div class="row">
<input id="plaintext" type="text" maxlength="24" placeholder="Your message here…" autocomplete="off" spellcheck="false">
<button id="btn-encrypt" type="button">Encrypt</button>
</div>
<div id="cipher-area" class="hidden">
<div class="label-row">
<span class="label">Nonce (public)</span>
<span class="label">Ciphertext (click to flip)</span>
<span class="label">Auth tag</span>
</div>
<div class="bytes-row">
<div id="nonce-bytes" class="byte-strip nonce-strip"></div>
<div id="cipher-bytes" class="byte-strip cipher-strip"></div>
<div id="tag-bytes" class="byte-strip tag-strip"></div>
</div>
<p class="hint small">Click any ciphertext byte to flip its lowest bit — simulating a single-bit tamper attack.</p>
<div class="row">
<button id="btn-decrypt" type="button">Decrypt</button>
<button id="btn-reset-bits" type="button" class="ghost">Restore bits</button>
</div>
</div>
<div id="status" class="status"></div>
<div id="plaintext-out" class="plaintext-out hidden"></div>
/* Styles for the AEAD authenticated-encryption demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.5; }
.hint.small { font-size: .8rem; color: #666; margin: .4rem 0; }
.row { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .8rem; align-items: center; }
input { flex: 1; min-width: 160px; font: 15px system-ui, sans-serif; padding: .42rem .7rem;
border: 1px solid #adb1b8; border-radius: 8px; outline: none; }
input:focus { border-color: #1d3557; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; white-space: nowrap; }
button.ghost { background: #fff; color: #1d3557; }
.label-row { display: flex; gap: .5rem; margin-bottom: .25rem; }
.label { font-size: .75rem; font-weight: 600; color: #555; text-transform: uppercase; letter-spacing: .04em; }
.bytes-row { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .25rem; }
.byte-strip { display: flex; flex-wrap: wrap; gap: 3px; }
.byte { width: 28px; height: 28px; display: flex; align-items: center; justify-content: center;
font: 700 10px ui-monospace, monospace; border-radius: 5px; user-select: none; }
.nonce-strip .byte { background: #e8eef3; color: #1d3557; border: 1px solid #cdd9e3; }
.cipher-strip .byte { background: #c9ccd1; color: #1d3557; border: 1px solid #adb1b8; cursor: pointer; transition: background .12s; }
.cipher-strip .byte:hover { background: #b0b5bd; }
.cipher-strip .byte.flipped { background: #e63946; color: #fff; border-color: #c92f3c; }
.tag-strip .byte { background: #d0eedd; color: #0a5c33; border: 1px solid #9ed4b5; }
.tag-strip .byte.bad { background: #ffd6d6; color: #c92f3c; border-color: #f5a5a5; }
.hidden { display: none; }
.status { font-size: 1rem; font-weight: 600; min-height: 1.4em; margin: .4rem 0; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.plaintext-out { font: 600 1rem ui-monospace, monospace; padding: .5rem .8rem;
background: #f0faf4; border: 1px solid #9ed4b5; border-radius: 8px;
margin-top: .4rem; letter-spacing: .05em; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Toy AEAD demo: CTR-mode encryption (XOR with keystream) + authentication tag.
// The keystream and tag use a simple mixing hash — NOT cryptographically secure,
// but correctly demonstrates the Encrypt-then-MAC structure of real AEAD (AES-GCM).
// Fixed demo key (16 bytes = 128-bit equivalent); in real AES-GCM this would be random.
const KEY = [0x4b, 0x69, 0x70, 0x75, 0x41, 0x45, 0x41, 0x44,
0x5f, 0x74, 0x6f, 0x79, 0x5f, 0x6b, 0x65, 0x79];
// A simple non-cryptographic mixing function used as a stand-in for AES block cipher.
function simpleHash(data) {
let h0 = 0x6a09e667, h1 = 0xbb67ae85;
for (let i = 0; i < data.length; i++) {
h0 = Math.imul(h0 ^ data[i], 0x9e3779b9) >>> 0;
h1 = Math.imul(h1 ^ (h0 + i), 0x6c62272e) >>> 0;
h0 = ((h0 << 13) | (h0 >>> 19)) >>> 0;
h1 = ((h1 << 7) | (h1 >>> 25)) >>> 0;
}
return [h0, h1];
}
// Generate a keystream by hashing (key || nonce || block_counter) repeatedly.
function makeKeystream(key, nonce, length) {
const stream = [];
for (let block = 0; stream.length < length; block++) {
const blockData = [...key, ...nonce, block & 0xff, (block >> 8) & 0xff];
const [h0, h1] = simpleHash(blockData);
stream.push(h0 & 0xff, (h0 >> 8) & 0xff, (h0 >> 16) & 0xff, (h0 >> 24) & 0xff,
h1 & 0xff, (h1 >> 8) & 0xff, (h1 >> 16) & 0xff, (h1 >> 24) & 0xff);
}
return stream.slice(0, length);
}
// Compute authentication tag over key, nonce and ciphertext — the MAC step.
function computeTag(key, nonce, cipherBytes) {
const tagInput = [...key, ...nonce, ...cipherBytes, cipherBytes.length & 0xff];
const [h0, h1] = simpleHash(tagInput);
return [
h0 & 0xff, (h0 >> 8) & 0xff, (h0 >> 16) & 0xff, (h0 >> 24) & 0xff,
h1 & 0xff, (h1 >> 8) & 0xff, (h1 >> 16) & 0xff, (h1 >> 24) & 0xff
];
}
let origNonce = [], origCipher = [], origTag = [], flippedIndices = new Set();
function toHex(b) { return b.toString(16).padStart(2, '0'); }
function renderBytes(containerId, bytes, cls, clickable, flipped) {
const el = document.getElementById(containerId);
el.innerHTML = '';
bytes.forEach((b, i) => {
const div = document.createElement('div');
div.className = 'byte' + (flipped && flipped.has(i) ? ' flipped' : '');
div.textContent = toHex(b);
if (clickable) {
div.title = 'Click to flip the lowest bit of this byte';
div.addEventListener('click', () => flipBit(i));
}
el.appendChild(div);
});
}
function renderTagBytes(tag, expected) {
const el = document.getElementById('tag-bytes');
el.innerHTML = '';
const isBad = tag.some((b, i) => b !== expected[i]);
tag.forEach(b => {
const div = document.createElement('div');
div.className = 'byte' + (isBad ? ' bad' : '');
div.textContent = toHex(b);
el.appendChild(div);
});
}
function setStatus(text, cls) {
const el = document.getElementById('status');
el.textContent = text;
el.className = 'status ' + cls;
}
function encrypt() {
const msg = document.getElementById('plaintext').value.trim();
if (!msg) { setStatus('Please enter a message first.', 'bad'); return; }
// Generate a fresh random 8-byte nonce for every encryption — uniqueness is mandatory.
origNonce = Array.from({ length: 8 }, () => Math.random() * 256 | 0);
const msgBytes = Array.from(msg).map(c => c.charCodeAt(0));
const stream = makeKeystream(KEY, origNonce, msgBytes.length);
origCipher = msgBytes.map((b, i) => b ^ stream[i]);
origTag = computeTag(KEY, origNonce, origCipher);
flippedIndices.clear();
document.getElementById('cipher-area').classList.remove('hidden');
document.getElementById('plaintext-out').classList.add('hidden');
renderBytes('nonce-bytes', origNonce, 'nonce-strip', false, null);
renderBytes('cipher-bytes', origCipher, 'cipher-strip', true, flippedIndices);
renderTagBytes(origTag, origTag);
setStatus('✓ Ciphertext + tag ready. Try decrypting, or flip a byte first.', 'ok');
}
function flipBit(index) {
if (flippedIndices.has(index)) flippedIndices.delete(index);
else flippedIndices.add(index);
const currentCipher = origCipher.map((b, i) => flippedIndices.has(i) ? b ^ 0x01 : b);
const currentTag = computeTag(KEY, origNonce, currentCipher);
renderBytes('cipher-bytes', currentCipher, 'cipher-strip', true, flippedIndices);
renderTagBytes(currentTag, origTag);
const tampered = flippedIndices.size > 0;
document.getElementById('plaintext-out').classList.add('hidden');
setStatus(tampered ? '⚠ Bit flipped — the authentication tag no longer matches.' : '✓ Ciphertext + tag ready. Try decrypting, or flip a byte first.', tampered ? 'bad' : 'ok');
}
function decrypt() {
const currentCipher = origCipher.map((b, i) => flippedIndices.has(i) ? b ^ 0x01 : b);
const currentTag = computeTag(KEY, origNonce, currentCipher);
const tagOk = currentTag.every((b, i) => b === origTag[i]);
if (!tagOk) {
setStatus('✗ Tag mismatch — message rejected. (Decryption refused.)', 'bad');
document.getElementById('plaintext-out').classList.add('hidden');
return;
}
const stream = makeKeystream(KEY, origNonce, currentCipher.length);
const plainBytes = currentCipher.map((b, i) => b ^ stream[i]);
const recovered = plainBytes.map(b => String.fromCharCode(b)).join('');
setStatus('✓ Tag verified — decryption succeeded.', 'ok');
const outEl = document.getElementById('plaintext-out');
outEl.textContent = fmt('Decrypted: %recovered%', { recovered: recovered });
outEl.classList.remove('hidden');
}
function resetBits() {
flippedIndices.clear();
if (origCipher.length === 0) return;
renderBytes('cipher-bytes', origCipher, 'cipher-strip', true, flippedIndices);
renderTagBytes(origTag, origTag);
document.getElementById('plaintext-out').classList.add('hidden');
setStatus('✓ Ciphertext + tag ready. Try decrypting, or flip a byte first.', 'ok');
}
document.getElementById('btn-encrypt').addEventListener('click', encrypt);
document.getElementById('btn-decrypt').addEventListener('click', decrypt);
document.getElementById('btn-reset-bits').addEventListener('click', resetBits);
document.getElementById('plaintext').addEventListener('keydown', e => {
if (e.key === 'Enter') encrypt();
});
<!-- AEAD toy demo — simulates encrypt + MAC-over-ciphertext (Encrypt-then-MAC) -->
<p class="hint">Type a message and encrypt it. Each colored byte is part of the ciphertext — click any byte to flip a bit. Watch the <b>authentication tag</b> change color: green = intact, red = tampered. Then try to decrypt.</p>
<div class="row">
<input id="plaintext" type="text" maxlength="24" placeholder="Your message here…" autocomplete="off" spellcheck="false">
<button id="btn-encrypt" type="button">Encrypt</button>
</div>
<div id="cipher-area" class="hidden">
<div class="label-row">
<span class="label">Nonce (public)</span>
<span class="label">Ciphertext (click to flip)</span>
<span class="label">Auth tag</span>
</div>
<div class="bytes-row">
<div id="nonce-bytes" class="byte-strip nonce-strip"></div>
<div id="cipher-bytes" class="byte-strip cipher-strip"></div>
<div id="tag-bytes" class="byte-strip tag-strip"></div>
</div>
<p class="hint small">Click any ciphertext byte to flip its lowest bit — simulating a single-bit tamper attack.</p>
<div class="row">
<button id="btn-decrypt" type="button">Decrypt</button>
<button id="btn-reset-bits" type="button" class="ghost">Restore bits</button>
</div>
</div>
<div id="status" class="status"></div>
<div id="plaintext-out" class="plaintext-out hidden"></div>
/* Styles for the AEAD authenticated-encryption demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.5; }
.hint.small { font-size: .8rem; color: #666; margin: .4rem 0; }
.row { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .8rem; align-items: center; }
input { flex: 1; min-width: 160px; font: 15px system-ui, sans-serif; padding: .42rem .7rem;
border: 1px solid #adb1b8; border-radius: 8px; outline: none; }
input:focus { border-color: #1d3557; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; white-space: nowrap; }
button.ghost { background: #fff; color: #1d3557; }
.label-row { display: flex; gap: .5rem; margin-bottom: .25rem; }
.label { font-size: .75rem; font-weight: 600; color: #555; text-transform: uppercase; letter-spacing: .04em; }
.bytes-row { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .25rem; }
.byte-strip { display: flex; flex-wrap: wrap; gap: 3px; }
.byte { width: 28px; height: 28px; display: flex; align-items: center; justify-content: center;
font: 700 10px ui-monospace, monospace; border-radius: 5px; user-select: none; }
.nonce-strip .byte { background: #e8eef3; color: #1d3557; border: 1px solid #cdd9e3; }
.cipher-strip .byte { background: #c9ccd1; color: #1d3557; border: 1px solid #adb1b8; cursor: pointer; transition: background .12s; }
.cipher-strip .byte:hover { background: #b0b5bd; }
.cipher-strip .byte.flipped { background: #e63946; color: #fff; border-color: #c92f3c; }
.tag-strip .byte { background: #d0eedd; color: #0a5c33; border: 1px solid #9ed4b5; }
.tag-strip .byte.bad { background: #ffd6d6; color: #c92f3c; border-color: #f5a5a5; }
.hidden { display: none; }
.status { font-size: 1rem; font-weight: 600; min-height: 1.4em; margin: .4rem 0; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.plaintext-out { font: 600 1rem ui-monospace, monospace; padding: .5rem .8rem;
background: #f0faf4; border: 1px solid #9ed4b5; border-radius: 8px;
margin-top: .4rem; letter-spacing: .05em; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Toy AEAD demo: CTR-mode encryption (XOR with keystream) + authentication tag.
// The keystream and tag use a simple mixing hash — NOT cryptographically secure,
// but correctly demonstrates the Encrypt-then-MAC structure of real AEAD (AES-GCM).
// Fixed demo key (16 bytes = 128-bit equivalent); in real AES-GCM this would be random.
const KEY = [0x4b, 0x69, 0x70, 0x75, 0x41, 0x45, 0x41, 0x44,
0x5f, 0x74, 0x6f, 0x79, 0x5f, 0x6b, 0x65, 0x79];
// A simple non-cryptographic mixing function used as a stand-in for AES block cipher.
function simpleHash(data) {
let h0 = 0x6a09e667, h1 = 0xbb67ae85;
for (let i = 0; i < data.length; i++) {
h0 = Math.imul(h0 ^ data[i], 0x9e3779b9) >>> 0;
h1 = Math.imul(h1 ^ (h0 + i), 0x6c62272e) >>> 0;
h0 = ((h0 << 13) | (h0 >>> 19)) >>> 0;
h1 = ((h1 << 7) | (h1 >>> 25)) >>> 0;
}
return [h0, h1];
}
// Generate a keystream by hashing (key || nonce || block_counter) repeatedly.
function makeKeystream(key, nonce, length) {
const stream = [];
for (let block = 0; stream.length < length; block++) {
const blockData = [...key, ...nonce, block & 0xff, (block >> 8) & 0xff];
const [h0, h1] = simpleHash(blockData);
stream.push(h0 & 0xff, (h0 >> 8) & 0xff, (h0 >> 16) & 0xff, (h0 >> 24) & 0xff,
h1 & 0xff, (h1 >> 8) & 0xff, (h1 >> 16) & 0xff, (h1 >> 24) & 0xff);
}
return stream.slice(0, length);
}
// Compute authentication tag over key, nonce and ciphertext — the MAC step.
function computeTag(key, nonce, cipherBytes) {
const tagInput = [...key, ...nonce, ...cipherBytes, cipherBytes.length & 0xff];
const [h0, h1] = simpleHash(tagInput);
return [
h0 & 0xff, (h0 >> 8) & 0xff, (h0 >> 16) & 0xff, (h0 >> 24) & 0xff,
h1 & 0xff, (h1 >> 8) & 0xff, (h1 >> 16) & 0xff, (h1 >> 24) & 0xff
];
}
let origNonce = [], origCipher = [], origTag = [], flippedIndices = new Set();
function toHex(b) { return b.toString(16).padStart(2, '0'); }
function renderBytes(containerId, bytes, cls, clickable, flipped) {
const el = document.getElementById(containerId);
el.innerHTML = '';
bytes.forEach((b, i) => {
const div = document.createElement('div');
div.className = 'byte' + (flipped && flipped.has(i) ? ' flipped' : '');
div.textContent = toHex(b);
if (clickable) {
div.title = 'Click to flip the lowest bit of this byte';
div.addEventListener('click', () => flipBit(i));
}
el.appendChild(div);
});
}
function renderTagBytes(tag, expected) {
const el = document.getElementById('tag-bytes');
el.innerHTML = '';
const isBad = tag.some((b, i) => b !== expected[i]);
tag.forEach(b => {
const div = document.createElement('div');
div.className = 'byte' + (isBad ? ' bad' : '');
div.textContent = toHex(b);
el.appendChild(div);
});
}
function setStatus(text, cls) {
const el = document.getElementById('status');
el.textContent = text;
el.className = 'status ' + cls;
}
function encrypt() {
const msg = document.getElementById('plaintext').value.trim();
if (!msg) { setStatus('Please enter a message first.', 'bad'); return; }
// Generate a fresh random 8-byte nonce for every encryption — uniqueness is mandatory.
origNonce = Array.from({ length: 8 }, () => Math.random() * 256 | 0);
const msgBytes = Array.from(msg).map(c => c.charCodeAt(0));
const stream = makeKeystream(KEY, origNonce, msgBytes.length);
origCipher = msgBytes.map((b, i) => b ^ stream[i]);
origTag = computeTag(KEY, origNonce, origCipher);
flippedIndices.clear();
document.getElementById('cipher-area').classList.remove('hidden');
document.getElementById('plaintext-out').classList.add('hidden');
renderBytes('nonce-bytes', origNonce, 'nonce-strip', false, null);
renderBytes('cipher-bytes', origCipher, 'cipher-strip', true, flippedIndices);
renderTagBytes(origTag, origTag);
setStatus('✓ Ciphertext + tag ready. Try decrypting, or flip a byte first.', 'ok');
}
function flipBit(index) {
if (flippedIndices.has(index)) flippedIndices.delete(index);
else flippedIndices.add(index);
const currentCipher = origCipher.map((b, i) => flippedIndices.has(i) ? b ^ 0x01 : b);
const currentTag = computeTag(KEY, origNonce, currentCipher);
renderBytes('cipher-bytes', currentCipher, 'cipher-strip', true, flippedIndices);
renderTagBytes(currentTag, origTag);
const tampered = flippedIndices.size > 0;
document.getElementById('plaintext-out').classList.add('hidden');
setStatus(tampered ? '⚠ Bit flipped — the authentication tag no longer matches.' : '✓ Ciphertext + tag ready. Try decrypting, or flip a byte first.', tampered ? 'bad' : 'ok');
}
function decrypt() {
const currentCipher = origCipher.map((b, i) => flippedIndices.has(i) ? b ^ 0x01 : b);
const currentTag = computeTag(KEY, origNonce, currentCipher);
const tagOk = currentTag.every((b, i) => b === origTag[i]);
if (!tagOk) {
setStatus('✗ Tag mismatch — message rejected. (Decryption refused.)', 'bad');
document.getElementById('plaintext-out').classList.add('hidden');
return;
}
const stream = makeKeystream(KEY, origNonce, currentCipher.length);
const plainBytes = currentCipher.map((b, i) => b ^ stream[i]);
const recovered = plainBytes.map(b => String.fromCharCode(b)).join('');
setStatus('✓ Tag verified — decryption succeeded.', 'ok');
const outEl = document.getElementById('plaintext-out');
outEl.textContent = fmt('Decrypted: %recovered%', { recovered: recovered });
outEl.classList.remove('hidden');
}
function resetBits() {
flippedIndices.clear();
if (origCipher.length === 0) return;
renderBytes('cipher-bytes', origCipher, 'cipher-strip', true, flippedIndices);
renderTagBytes(origTag, origTag);
document.getElementById('plaintext-out').classList.add('hidden');
setStatus('✓ Ciphertext + tag ready. Try decrypting, or flip a byte first.', 'ok');
}
document.getElementById('btn-encrypt').addEventListener('click', encrypt);
document.getElementById('btn-decrypt').addEventListener('click', decrypt);
document.getElementById('btn-reset-bits').addEventListener('click', resetBits);
document.getElementById('plaintext').addEventListener('keydown', e => {
if (e.key === 'Enter') encrypt();
});
Notice two things. First, the ciphertext looks like random noise — confidentiality is working. Second, the moment you flip any bit the tag mismatch is detected immediately — integrity kicks in. The receiver never sees a corrupted plaintext; the decryption step is simply refused.
Comments
Loading comments...