Draw a shape below (or use the preset) and encrypt it in ECB mode. Notice how the structure survives encryption. Then switch to CTR mode and see the difference.
<div class="controls">
<div class="btn-group">
<button id="btn-tux" type="button" class="active">Load Tux shape</button>
<button id="btn-clear" type="button" class="ghost">Clear canvas</button>
</div>
<div class="mode-row">
<span class="label">Encryption mode:</span>
<label class="radio"><input type="radio" name="mode" value="ecb" checked> ECB</label>
<label class="radio"><input type="radio" name="mode" value="ctr"> CTR</label>
</div>
<button id="btn-encrypt" type="button">Encrypt</button>
<button id="btn-decrypt" type="button" class="ghost" disabled>Decrypt</button>
</div>
<div class="canvases">
<div class="canvas-wrap">
<div class="canvas-label">Plaintext (draw here)</div>
<canvas id="plain" width="160" height="160"></canvas>
</div>
<div class="arrow">โ</div>
<div class="canvas-wrap">
<div class="canvas-label">Ciphertext</div>
<canvas id="cipher" width="160" height="160"></canvas>
</div>
</div>
<div id="status" class="status"></div>
<div class="legend">
Block size: 4ร4 pixels | Grid: 40 blocks (10ร10 logical blocks shown on 160ร160 px)
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin-bottom: .75rem; }
.btn-group { display: flex; gap: .4rem; }
.mode-row { display: flex; align-items: center; gap: .5rem; font-size: .88rem; }
.label { font-weight: 600; color: #444; }
.radio { display: flex; align-items: center; gap: .25rem; cursor: pointer; font-size: .88rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1.5px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button.active { background: #e63946; border-color: #c92f3c; }
button:disabled { opacity: .45; cursor: default; }
.canvases { display: flex; align-items: center; gap: .75rem; margin: .5rem 0; }
.canvas-wrap { display: flex; flex-direction: column; align-items: center; gap: .3rem; }
.canvas-label { font-size: .78rem; font-weight: 700; color: #555; letter-spacing: .04em; text-transform: uppercase; }
canvas { border: 1.5px solid #b0bcc8; border-radius: 4px; cursor: crosshair; image-rendering: pixelated; display: block; }
.arrow { font-size: 1.6rem; color: #888; }
.status { font-size: .88rem; font-weight: 600; min-height: 1.3em; margin: .4rem 0; color: #1a6e35; }
.status.warn { color: #c92f3c; }
.legend { font-size: .76rem; color: #888; margin-top: .3rem; }
// ECB vs CTR visual demo โ encrypt a 10ร10 grid of 4-pixel "blocks" on a 160ร160 canvas.
// ECB: identical pixel blocks โ identical encrypted blocks (pattern visible).
// CTR: block XOR'd with keystream derived from counter โ output looks random.
// This is a SIMPLIFIED toy cipher (not AES) for visual illustration โ the security
// properties demonstrated (ECB pattern leak, CTR randomization) are identical to AES.
const BLOCK = 4; // pixels per block side
const GRID = 10; // blocks per row/col (10*4 = 160px per axis)
const TOTAL = GRID * GRID; // 100 blocks
const plainCanvas = document.getElementById('plain');
const cipherCanvas = document.getElementById('cipher');
const pCtx = plainCanvas.getContext('2d');
const cCtx = cipherCanvas.getContext('2d');
const status = document.getElementById('status');
// โโ Toy cipher: 3-byte XOR+rotate permutation (illustrative only) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Key is fixed; we only care about the STRUCTURAL property of each mode.
const KEY = [0x5A, 0xA3, 0x7C]; // arbitrary 3-byte "key"
function encryptBlock(bytes) {
// Encrypt a single 3-byte (RGB) block โ used for ECB.
// Pure XOR: self-inverse (decrypt === encrypt), and deterministic
// regardless of block position (that IS the ECB property: same block,
// same key โ same ciphertext no matter where it appears).
return bytes.map((b, i) => b ^ KEY[i % 3] ^ 0xC9);
}
function ctrKeystream(blockIndex) {
// CTR keystream for one block: deterministic function of counter + key.
// Simulates "Encrypt(Key, Counter)" per AES-CTR spec.
const ctr = blockIndex ^ 0xABCD;
return KEY.map((k, i) => {
let v = k ^ ((ctr >> (i * 4)) & 0xff) ^ (i * 0x5C);
v = ((v << 3) | (v >> 5)) & 0xff; // rotate left 3
return v ^ 0xE7;
});
}
// โโ Plain pixel storage: 100 blocks ร 3 channels (RGB) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Each block stores the AVERAGE colour of its 4ร4 area (simplified pixel block).
const plain = new Uint8Array(TOTAL * 3); // RGB per block
function getMode() {
return document.querySelector('input[name="mode"]:checked').value;
}
// โโ Tux shape preset โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Encoded as a 10ร10 grid of colours: 0=white bg, 1=black, 2=orange (beak/feet)
const TUX = [
[0,0,0,1,1,1,1,0,0,0],
[0,0,1,1,1,1,1,1,0,0],
[0,1,1,0,1,1,0,1,1,0],
[0,1,1,1,1,1,1,1,1,0],
[0,0,1,2,1,1,2,1,0,0],
[0,0,0,1,1,1,1,0,0,0],
[0,1,1,1,1,1,1,1,1,0],
[0,1,1,1,1,1,1,1,1,0],
[0,0,1,1,0,0,1,1,0,0],
[0,0,1,0,0,0,0,1,0,0],
];
const TUX_COLORS = [
[240,240,240], // white bg
[20, 20, 20], // black
[220,140, 20], // orange
];
function loadTux() {
for (let by = 0; by < GRID; by++) {
for (let bx = 0; bx < GRID; bx++) {
const idx = (by * GRID + bx) * 3;
const [r,g,b] = TUX_COLORS[TUX[by][bx]];
plain[idx] = r; plain[idx+1] = g; plain[idx+2] = b;
}
}
renderPlain();
clearCipher();
status.textContent = '';
status.className = 'status';
document.getElementById('btn-decrypt').disabled = true;
}
function renderPlain() {
pCtx.clearRect(0, 0, 160, 160);
for (let b = 0; b < TOTAL; b++) {
const bx = b % GRID, by = Math.floor(b / GRID);
const r = plain[b*3], g = plain[b*3+1], bl = plain[b*3+2];
pCtx.fillStyle = `rgb(${r},${g},${bl})`;
pCtx.fillRect(bx * BLOCK, by * BLOCK, BLOCK, BLOCK);
}
}
function clearCipher() {
cCtx.fillStyle = '#e8eef3';
cCtx.fillRect(0, 0, 160, 160);
}
function renderCipherData(data) {
cCtx.clearRect(0, 0, 160, 160);
for (let b = 0; b < TOTAL; b++) {
const bx = b % GRID, by = Math.floor(b / GRID);
const r = data[b*3], g = data[b*3+1], bl = data[b*3+2];
cCtx.fillStyle = `rgb(${r},${g},${bl})`;
cCtx.fillRect(bx * BLOCK, by * BLOCK, BLOCK, BLOCK);
}
}
// โโ Drawing on plain canvas โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let drawing = false;
let drawColor = [20, 20, 20];
function canvasXY(canvas, e) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
return [
Math.floor((clientX - rect.left) * scaleX / BLOCK),
Math.floor((clientY - rect.top) * scaleY / BLOCK),
];
}
function paintBlock(bx, by, color) {
if (bx < 0 || bx >= GRID || by < 0 || by >= GRID) return;
const idx = (by * GRID + bx) * 3;
plain[idx] = color[0]; plain[idx+1] = color[1]; plain[idx+2] = color[2];
pCtx.fillStyle = `rgb(${color[0]},${color[1]},${color[2]})`;
pCtx.fillRect(bx * BLOCK, by * BLOCK, BLOCK, BLOCK);
}
plainCanvas.addEventListener('mousedown', e => {
drawing = true;
drawColor = e.button === 2 ? [240,240,240] : [20,20,20];
const [bx,by] = canvasXY(plainCanvas, e);
paintBlock(bx, by, drawColor);
clearCipher();
document.getElementById('btn-decrypt').disabled = true;
});
plainCanvas.addEventListener('mousemove', e => {
if (!drawing) return;
const [bx,by] = canvasXY(plainCanvas, e);
paintBlock(bx, by, drawColor);
});
window.addEventListener('mouseup', () => { drawing = false; });
plainCanvas.addEventListener('contextmenu', e => e.preventDefault());
// โโ Encrypt / Decrypt โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const cipher = new Uint8Array(TOTAL * 3);
document.getElementById('btn-encrypt').addEventListener('click', () => {
const mode = getMode();
for (let b = 0; b < TOTAL; b++) {
const rgb = [plain[b*3], plain[b*3+1], plain[b*3+2]];
let enc;
if (mode === 'ecb') {
enc = encryptBlock(rgb); // same key for every block โ identical blocks stay identical!
} else {
// CTR: XOR plaintext with keystream[counter]
const ks = ctrKeystream(b);
enc = rgb.map((v, i) => v ^ ks[i % 3]);
}
cipher[b*3] = enc[0]; cipher[b*3+1] = enc[1]; cipher[b*3+2] = enc[2];
}
renderCipherData(cipher);
const modeLabel = mode === 'ecb' ? 'ECB' : 'CTR';
status.textContent = mode === 'ecb'
? 'ECB: identical blocks โ identical ciphertext. The shape is still visible.'
: 'CTR: every block gets a unique keystream. The output looks random.';
status.className = mode === 'ecb' ? 'status warn' : 'status';
document.getElementById('btn-decrypt').disabled = false;
});
document.getElementById('btn-decrypt').addEventListener('click', () => {
const mode = getMode();
for (let b = 0; b < TOTAL; b++) {
const rgb = [cipher[b*3], cipher[b*3+1], cipher[b*3+2]];
let dec;
if (mode === 'ecb') {
// ECB decrypt: same operation is its own inverse (pure XOR)
dec = encryptBlock(rgb);
} else {
const ks = ctrKeystream(b);
dec = rgb.map((v, i) => v ^ ks[i % 3]); // XOR is its own inverse
}
plain[b*3] = dec[0]; plain[b*3+1] = dec[1]; plain[b*3+2] = dec[2];
}
renderPlain();
clearCipher();
status.textContent = 'Decrypted โ original plaintext recovered.';
status.className = 'status';
document.getElementById('btn-decrypt').disabled = true;
});
document.getElementById('btn-tux').addEventListener('click', loadTux);
document.getElementById('btn-clear').addEventListener('click', () => {
plain.fill(240);
renderPlain();
clearCipher();
status.textContent = '';
status.className = 'status';
document.getElementById('btn-decrypt').disabled = true;
});
// โโ Init โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
loadTux();
// โโ Verification assertions (run in Node.js via: node verify.js) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Assert ECB: same input block โ same output regardless of position.
// Assert CTR: different blocks โ different keystreams.
// Assert round-trip: decrypt(encrypt(x)) === x.
if (typeof process !== 'undefined' && process.versions && process.versions.node) {
let pass = true;
// ECB: same plaintext block โ same ciphertext (determinism = the ECB weakness)
const ecbA = encryptBlock([128, 64, 32]);
const ecbB = encryptBlock([128, 64, 32]);
if (JSON.stringify(ecbA) !== JSON.stringify(ecbB)) { console.error('FAIL: ECB determinism'); pass = false; }
// CTR: two different counters โ different keystreams
const ks0 = ctrKeystream(0);
const ks1 = ctrKeystream(1);
if (JSON.stringify(ks0) === JSON.stringify(ks1)) { console.error('FAIL: CTR keystreams collide at 0 and 1'); pass = false; }
// CTR round-trip: decrypt(encrypt(x)) === x for 10 random-ish blocks
for (let b = 0; b < 10; b++) {
const orig = [(b * 37 + 11) & 0xff, (b * 53 + 199) & 0xff, (b * 71 + 83) & 0xff];
const ks = ctrKeystream(b);
const enc = orig.map((v, i) => v ^ ks[i % 3]);
const dec = enc.map((v, i) => v ^ ks[i % 3]);
if (JSON.stringify(dec) !== JSON.stringify(orig)) { console.error(`FAIL: CTR round-trip at block ${b}`); pass = false; }
}
// ECB round-trip (pure XOR is self-inverse)
const origEcb = [100, 200, 150];
const encEcb = encryptBlock(origEcb);
const decEcb = encryptBlock(encEcb);
if (JSON.stringify(decEcb) !== JSON.stringify(origEcb)) { console.error('FAIL: ECB round-trip'); pass = false; }
// ECB: different plaintexts โ different ciphertexts
const p1 = encryptBlock([0, 0, 0]);
const p2 = encryptBlock([255, 255, 255]);
if (JSON.stringify(p1) === JSON.stringify(p2)) { console.error('FAIL: ECB collision'); pass = false; }
if (pass) console.log('All assertions passed: ECB determinism, CTR keystream uniqueness, CTR/ECB round-trips.');
process.exit(pass ? 0 : 1);
}
<div class="controls">
<div class="btn-group">
<button id="btn-tux" type="button" class="active">Load Tux shape</button>
<button id="btn-clear" type="button" class="ghost">Clear canvas</button>
</div>
<div class="mode-row">
<span class="label">Encryption mode:</span>
<label class="radio"><input type="radio" name="mode" value="ecb" checked> ECB</label>
<label class="radio"><input type="radio" name="mode" value="ctr"> CTR</label>
</div>
<button id="btn-encrypt" type="button">Encrypt</button>
<button id="btn-decrypt" type="button" class="ghost" disabled>Decrypt</button>
</div>
<div class="canvases">
<div class="canvas-wrap">
<div class="canvas-label">Plaintext (draw here)</div>
<canvas id="plain" width="160" height="160"></canvas>
</div>
<div class="arrow">โ</div>
<div class="canvas-wrap">
<div class="canvas-label">Ciphertext</div>
<canvas id="cipher" width="160" height="160"></canvas>
</div>
</div>
<div id="status" class="status"></div>
<div class="legend">
Block size: 4ร4 pixels | Grid: 40 blocks (10ร10 logical blocks shown on 160ร160 px)
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin-bottom: .75rem; }
.btn-group { display: flex; gap: .4rem; }
.mode-row { display: flex; align-items: center; gap: .5rem; font-size: .88rem; }
.label { font-weight: 600; color: #444; }
.radio { display: flex; align-items: center; gap: .25rem; cursor: pointer; font-size: .88rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1.5px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button.active { background: #e63946; border-color: #c92f3c; }
button:disabled { opacity: .45; cursor: default; }
.canvases { display: flex; align-items: center; gap: .75rem; margin: .5rem 0; }
.canvas-wrap { display: flex; flex-direction: column; align-items: center; gap: .3rem; }
.canvas-label { font-size: .78rem; font-weight: 700; color: #555; letter-spacing: .04em; text-transform: uppercase; }
canvas { border: 1.5px solid #b0bcc8; border-radius: 4px; cursor: crosshair; image-rendering: pixelated; display: block; }
.arrow { font-size: 1.6rem; color: #888; }
.status { font-size: .88rem; font-weight: 600; min-height: 1.3em; margin: .4rem 0; color: #1a6e35; }
.status.warn { color: #c92f3c; }
.legend { font-size: .76rem; color: #888; margin-top: .3rem; }
// ECB vs CTR visual demo โ encrypt a 10ร10 grid of 4-pixel "blocks" on a 160ร160 canvas.
// ECB: identical pixel blocks โ identical encrypted blocks (pattern visible).
// CTR: block XOR'd with keystream derived from counter โ output looks random.
// This is a SIMPLIFIED toy cipher (not AES) for visual illustration โ the security
// properties demonstrated (ECB pattern leak, CTR randomization) are identical to AES.
const BLOCK = 4; // pixels per block side
const GRID = 10; // blocks per row/col (10*4 = 160px per axis)
const TOTAL = GRID * GRID; // 100 blocks
const plainCanvas = document.getElementById('plain');
const cipherCanvas = document.getElementById('cipher');
const pCtx = plainCanvas.getContext('2d');
const cCtx = cipherCanvas.getContext('2d');
const status = document.getElementById('status');
// โโ Toy cipher: 3-byte XOR+rotate permutation (illustrative only) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Key is fixed; we only care about the STRUCTURAL property of each mode.
const KEY = [0x5A, 0xA3, 0x7C]; // arbitrary 3-byte "key"
function encryptBlock(bytes) {
// Encrypt a single 3-byte (RGB) block โ used for ECB.
// Pure XOR: self-inverse (decrypt === encrypt), and deterministic
// regardless of block position (that IS the ECB property: same block,
// same key โ same ciphertext no matter where it appears).
return bytes.map((b, i) => b ^ KEY[i % 3] ^ 0xC9);
}
function ctrKeystream(blockIndex) {
// CTR keystream for one block: deterministic function of counter + key.
// Simulates "Encrypt(Key, Counter)" per AES-CTR spec.
const ctr = blockIndex ^ 0xABCD;
return KEY.map((k, i) => {
let v = k ^ ((ctr >> (i * 4)) & 0xff) ^ (i * 0x5C);
v = ((v << 3) | (v >> 5)) & 0xff; // rotate left 3
return v ^ 0xE7;
});
}
// โโ Plain pixel storage: 100 blocks ร 3 channels (RGB) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Each block stores the AVERAGE colour of its 4ร4 area (simplified pixel block).
const plain = new Uint8Array(TOTAL * 3); // RGB per block
function getMode() {
return document.querySelector('input[name="mode"]:checked').value;
}
// โโ Tux shape preset โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Encoded as a 10ร10 grid of colours: 0=white bg, 1=black, 2=orange (beak/feet)
const TUX = [
[0,0,0,1,1,1,1,0,0,0],
[0,0,1,1,1,1,1,1,0,0],
[0,1,1,0,1,1,0,1,1,0],
[0,1,1,1,1,1,1,1,1,0],
[0,0,1,2,1,1,2,1,0,0],
[0,0,0,1,1,1,1,0,0,0],
[0,1,1,1,1,1,1,1,1,0],
[0,1,1,1,1,1,1,1,1,0],
[0,0,1,1,0,0,1,1,0,0],
[0,0,1,0,0,0,0,1,0,0],
];
const TUX_COLORS = [
[240,240,240], // white bg
[20, 20, 20], // black
[220,140, 20], // orange
];
function loadTux() {
for (let by = 0; by < GRID; by++) {
for (let bx = 0; bx < GRID; bx++) {
const idx = (by * GRID + bx) * 3;
const [r,g,b] = TUX_COLORS[TUX[by][bx]];
plain[idx] = r; plain[idx+1] = g; plain[idx+2] = b;
}
}
renderPlain();
clearCipher();
status.textContent = '';
status.className = 'status';
document.getElementById('btn-decrypt').disabled = true;
}
function renderPlain() {
pCtx.clearRect(0, 0, 160, 160);
for (let b = 0; b < TOTAL; b++) {
const bx = b % GRID, by = Math.floor(b / GRID);
const r = plain[b*3], g = plain[b*3+1], bl = plain[b*3+2];
pCtx.fillStyle = `rgb(${r},${g},${bl})`;
pCtx.fillRect(bx * BLOCK, by * BLOCK, BLOCK, BLOCK);
}
}
function clearCipher() {
cCtx.fillStyle = '#e8eef3';
cCtx.fillRect(0, 0, 160, 160);
}
function renderCipherData(data) {
cCtx.clearRect(0, 0, 160, 160);
for (let b = 0; b < TOTAL; b++) {
const bx = b % GRID, by = Math.floor(b / GRID);
const r = data[b*3], g = data[b*3+1], bl = data[b*3+2];
cCtx.fillStyle = `rgb(${r},${g},${bl})`;
cCtx.fillRect(bx * BLOCK, by * BLOCK, BLOCK, BLOCK);
}
}
// โโ Drawing on plain canvas โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let drawing = false;
let drawColor = [20, 20, 20];
function canvasXY(canvas, e) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
return [
Math.floor((clientX - rect.left) * scaleX / BLOCK),
Math.floor((clientY - rect.top) * scaleY / BLOCK),
];
}
function paintBlock(bx, by, color) {
if (bx < 0 || bx >= GRID || by < 0 || by >= GRID) return;
const idx = (by * GRID + bx) * 3;
plain[idx] = color[0]; plain[idx+1] = color[1]; plain[idx+2] = color[2];
pCtx.fillStyle = `rgb(${color[0]},${color[1]},${color[2]})`;
pCtx.fillRect(bx * BLOCK, by * BLOCK, BLOCK, BLOCK);
}
plainCanvas.addEventListener('mousedown', e => {
drawing = true;
drawColor = e.button === 2 ? [240,240,240] : [20,20,20];
const [bx,by] = canvasXY(plainCanvas, e);
paintBlock(bx, by, drawColor);
clearCipher();
document.getElementById('btn-decrypt').disabled = true;
});
plainCanvas.addEventListener('mousemove', e => {
if (!drawing) return;
const [bx,by] = canvasXY(plainCanvas, e);
paintBlock(bx, by, drawColor);
});
window.addEventListener('mouseup', () => { drawing = false; });
plainCanvas.addEventListener('contextmenu', e => e.preventDefault());
// โโ Encrypt / Decrypt โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const cipher = new Uint8Array(TOTAL * 3);
document.getElementById('btn-encrypt').addEventListener('click', () => {
const mode = getMode();
for (let b = 0; b < TOTAL; b++) {
const rgb = [plain[b*3], plain[b*3+1], plain[b*3+2]];
let enc;
if (mode === 'ecb') {
enc = encryptBlock(rgb); // same key for every block โ identical blocks stay identical!
} else {
// CTR: XOR plaintext with keystream[counter]
const ks = ctrKeystream(b);
enc = rgb.map((v, i) => v ^ ks[i % 3]);
}
cipher[b*3] = enc[0]; cipher[b*3+1] = enc[1]; cipher[b*3+2] = enc[2];
}
renderCipherData(cipher);
const modeLabel = mode === 'ecb' ? 'ECB' : 'CTR';
status.textContent = mode === 'ecb'
? 'ECB: identical blocks โ identical ciphertext. The shape is still visible.'
: 'CTR: every block gets a unique keystream. The output looks random.';
status.className = mode === 'ecb' ? 'status warn' : 'status';
document.getElementById('btn-decrypt').disabled = false;
});
document.getElementById('btn-decrypt').addEventListener('click', () => {
const mode = getMode();
for (let b = 0; b < TOTAL; b++) {
const rgb = [cipher[b*3], cipher[b*3+1], cipher[b*3+2]];
let dec;
if (mode === 'ecb') {
// ECB decrypt: same operation is its own inverse (pure XOR)
dec = encryptBlock(rgb);
} else {
const ks = ctrKeystream(b);
dec = rgb.map((v, i) => v ^ ks[i % 3]); // XOR is its own inverse
}
plain[b*3] = dec[0]; plain[b*3+1] = dec[1]; plain[b*3+2] = dec[2];
}
renderPlain();
clearCipher();
status.textContent = 'Decrypted โ original plaintext recovered.';
status.className = 'status';
document.getElementById('btn-decrypt').disabled = true;
});
document.getElementById('btn-tux').addEventListener('click', loadTux);
document.getElementById('btn-clear').addEventListener('click', () => {
plain.fill(240);
renderPlain();
clearCipher();
status.textContent = '';
status.className = 'status';
document.getElementById('btn-decrypt').disabled = true;
});
// โโ Init โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
loadTux();
// โโ Verification assertions (run in Node.js via: node verify.js) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Assert ECB: same input block โ same output regardless of position.
// Assert CTR: different blocks โ different keystreams.
// Assert round-trip: decrypt(encrypt(x)) === x.
if (typeof process !== 'undefined' && process.versions && process.versions.node) {
let pass = true;
// ECB: same plaintext block โ same ciphertext (determinism = the ECB weakness)
const ecbA = encryptBlock([128, 64, 32]);
const ecbB = encryptBlock([128, 64, 32]);
if (JSON.stringify(ecbA) !== JSON.stringify(ecbB)) { console.error('FAIL: ECB determinism'); pass = false; }
// CTR: two different counters โ different keystreams
const ks0 = ctrKeystream(0);
const ks1 = ctrKeystream(1);
if (JSON.stringify(ks0) === JSON.stringify(ks1)) { console.error('FAIL: CTR keystreams collide at 0 and 1'); pass = false; }
// CTR round-trip: decrypt(encrypt(x)) === x for 10 random-ish blocks
for (let b = 0; b < 10; b++) {
const orig = [(b * 37 + 11) & 0xff, (b * 53 + 199) & 0xff, (b * 71 + 83) & 0xff];
const ks = ctrKeystream(b);
const enc = orig.map((v, i) => v ^ ks[i % 3]);
const dec = enc.map((v, i) => v ^ ks[i % 3]);
if (JSON.stringify(dec) !== JSON.stringify(orig)) { console.error(`FAIL: CTR round-trip at block ${b}`); pass = false; }
}
// ECB round-trip (pure XOR is self-inverse)
const origEcb = [100, 200, 150];
const encEcb = encryptBlock(origEcb);
const decEcb = encryptBlock(encEcb);
if (JSON.stringify(decEcb) !== JSON.stringify(origEcb)) { console.error('FAIL: ECB round-trip'); pass = false; }
// ECB: different plaintexts โ different ciphertexts
const p1 = encryptBlock([0, 0, 0]);
const p2 = encryptBlock([255, 255, 255]);
if (JSON.stringify(p1) === JSON.stringify(p2)) { console.error('FAIL: ECB collision'); pass = false; }
if (pass) console.log('All assertions passed: ECB determinism, CTR keystream uniqueness, CTR/ECB round-trips.');
process.exit(pass ? 0 : 1);
}
The lesson: a secure cipher in a broken mode is a broken cipher.
Comments
Loading comments...