The demo below runs a simplified 4-round Feistel cipher on an 8-bit block. Type your own message byte (0–255), hit Encrypt, and watch the two halves swap and mix through each round. Then hit Decrypt to see the same rounds run in reverse — landing back on the original value.
<div class="controls">
<label>Message byte (0–255):
<input id="msgInput" type="number" min="0" max="255" value="42" />
</label>
<div class="btns">
<button id="btnEncrypt" type="button">Encrypt</button>
<button id="btnDecrypt" type="button" disabled>Decrypt</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
</div>
<div id="roundsViz" class="rounds-viz"></div>
<div id="status" class="status"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.controls { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: .8rem; }
label { font-weight: 600; font-size: .9rem; }
input[type=number] { width: 70px; padding: .3rem .5rem; border: 1px solid #adb1b8; border-radius: 6px;
font-size: 1rem; margin-left: .4rem; }
.btns { display: flex; gap: .5rem; }
button { font: 600 13px system-ui; padding: .4rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button:disabled { opacity: .4; cursor: default; }
button.ghost { background: #fff; color: #1d3557; }
.rounds-viz { display: flex; flex-direction: column; gap: .5rem; }
.round-row { display: flex; align-items: center; gap: .5rem; }
.round-label { font: 700 12px ui-monospace, monospace; color: #555; width: 56px; flex-shrink: 0; }
.half { display: flex; align-items: center; justify-content: center; min-width: 54px; padding: .3rem .5rem;
border-radius: 6px; font: 700 13px ui-monospace, monospace; border: 1.5px solid #cdd9e3;
background: #e8eef3; color: #1d3557; }
.half.left { background: #d6eaf8; border-color: #9abdd6; }
.half.right { background: #fef9e7; border-color: #d4a820; }
.half.changed { outline: 2px solid #e63946; outline-offset: 1px; }
.arrow { color: #888; font-size: .9rem; }
.xor-info { font: 12px ui-monospace, monospace; color: #555; margin-left: .3rem; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin-top: .5rem; }
.status.ok { color: #0a7d33; }
.status.enc { color: #1d3557; }
.header-row { display: flex; align-items: center; gap: .5rem; margin-bottom: .2rem; }
.header-label { font: 700 11px system-ui; color: #888; width: 56px; }
.header-half { font: 700 11px system-ui; color: #888; min-width: 54px; text-align: center; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Simplified 4-round Feistel cipher on an 8-bit block (two 4-bit halves).
// Round keys are fixed constants for demo purposes.
// Round function F(x, k) = ((x * 3 + k) ^ (x << 1 | x >> 3)) & 0xF
// Encryption: (L, R) -> (R, L ^ F(R, K_i)) for each round
// Decryption: same rounds in reverse order with same keys, using the inverse identity.
const ROUNDS = 4;
const ROUND_KEYS = [5, 11, 3, 9]; // 4-bit keys (0-15)
function F(x, k) {
// A simple keyed mixing function on 4-bit values — not invertible in general.
return ((x * 3 + k) ^ ((x << 1 | x >> 3) & 0xF)) & 0xF;
}
function feistelEncrypt(msg) {
// msg is 8-bit; split into two 4-bit halves
let L = (msg >> 4) & 0xF;
let R = msg & 0xF;
const steps = [{ L, R, label: `Input`, xorInfo: '' }];
for (let i = 0; i < ROUNDS; i++) {
const fVal = F(R, ROUND_KEYS[i]);
const newL = R;
const newR = (L ^ fVal) & 0xF;
steps.push({ L: newL, R: newR, label: 'R' + (i + 1), xorInfo: `F=${hex(fVal)}` });
L = newL; R = newR;
}
return { ciphertext: (L << 4) | R, steps };
}
function feistelDecrypt(cipher) {
// Same rounds, reversed key order; inversion identity: L_prev = R_curr ^ F(L_curr, K_i), R_prev = L_curr
let L = (cipher >> 4) & 0xF;
let R = cipher & 0xF;
const steps = [{ L, R, label: `Input`, xorInfo: '' }];
for (let i = ROUNDS - 1; i >= 0; i--) {
const fVal = F(L, ROUND_KEYS[i]);
const newR = L;
const newL = (R ^ fVal) & 0xF;
steps.push({ L: newL, R: newR, label: 'D' + (ROUNDS - i), xorInfo: `F=${hex(fVal)}` });
L = newL; R = newR;
}
return { plaintext: (L << 4) | R, steps };
}
function hex(v) { return '0x' + v.toString(16).toUpperCase().padStart(2, '0'); }
function renderSteps(steps, title) {
const viz = document.getElementById('roundsViz');
viz.innerHTML = '';
// Header
const hdr = document.createElement('div');
hdr.className = 'header-row';
hdr.innerHTML = fmt(`<div class="header-label">%title%</div>
<div class="header-half">Left (H)</div>
<div class="header-half"></div>
<div class="header-half">Right (L)</div>`, { title: title });
viz.appendChild(hdr);
steps.forEach((s, idx) => {
const row = document.createElement('div');
row.className = 'round-row';
const prevL = idx > 0 ? steps[idx - 1].L : null;
const prevR = idx > 0 ? steps[idx - 1].R : null;
const lChanged = prevL !== null && s.L !== prevL;
const rChanged = prevR !== null && s.R !== prevR;
row.innerHTML = `
<div class="round-label">${s.label}</div>
<div class="half left${lChanged ? ' changed' : ''}">${hex(s.L)}</div>
<div class="arrow">⇌</div>
<div class="half right${rChanged ? ' changed' : ''}">${hex(s.R)}</div>
${s.xorInfo ? `<div class="xor-info">${s.xorInfo}</div>` : ''}
`;
viz.appendChild(row);
});
}
let lastCipher = null;
document.getElementById('btnEncrypt').addEventListener('click', () => {
const msg = parseInt(document.getElementById('msgInput').value, 10);
if (isNaN(msg) || msg < 0 || msg > 255) {
setStatus(`Enter a number between 0 and 255.`, '');
return;
}
const { ciphertext, steps } = feistelEncrypt(msg);
lastCipher = ciphertext;
renderSteps(steps, `Enc`);
setStatus(fmt(`Encrypted %hex% → %hex2%`, { hex: hex(msg), hex2: hex(ciphertext) }), 'enc');
document.getElementById('btnDecrypt').disabled = false;
console.log(fmt(`Feistel encrypt: %msg% → %ciphertext% (steps: %stringify%)`, { msg: msg, ciphertext: ciphertext, stringify: JSON.stringify(steps) }));
});
document.getElementById('btnDecrypt').addEventListener('click', () => {
if (lastCipher === null) return;
const { plaintext, steps } = feistelDecrypt(lastCipher);
renderSteps(steps, `Dec`);
const orig = parseInt(document.getElementById('msgInput').value, 10);
const match = plaintext === orig;
setStatus(fmt(`Decrypted %hex% → %hex2%%v%`, { hex: hex(lastCipher), hex2: hex(plaintext), v: match ? ' ✓ matches original!' : ' ✗ mismatch!' }), match ? 'ok' : '');
console.log(fmt(`Feistel decrypt: %lastCipher% → %plaintext% (match=%match%)`, { lastCipher: lastCipher, plaintext: plaintext, match: match }));
});
document.getElementById('btnReset').addEventListener('click', () => {
document.getElementById('roundsViz').innerHTML = '';
document.getElementById('status').textContent = '';
document.getElementById('btnDecrypt').disabled = true;
lastCipher = null;
});
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t;
s.className = 'status ' + cls;
}
<div class="controls">
<label>Message byte (0–255):
<input id="msgInput" type="number" min="0" max="255" value="42" />
</label>
<div class="btns">
<button id="btnEncrypt" type="button">Encrypt</button>
<button id="btnDecrypt" type="button" disabled>Decrypt</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
</div>
<div id="roundsViz" class="rounds-viz"></div>
<div id="status" class="status"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.controls { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: .8rem; }
label { font-weight: 600; font-size: .9rem; }
input[type=number] { width: 70px; padding: .3rem .5rem; border: 1px solid #adb1b8; border-radius: 6px;
font-size: 1rem; margin-left: .4rem; }
.btns { display: flex; gap: .5rem; }
button { font: 600 13px system-ui; padding: .4rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button:disabled { opacity: .4; cursor: default; }
button.ghost { background: #fff; color: #1d3557; }
.rounds-viz { display: flex; flex-direction: column; gap: .5rem; }
.round-row { display: flex; align-items: center; gap: .5rem; }
.round-label { font: 700 12px ui-monospace, monospace; color: #555; width: 56px; flex-shrink: 0; }
.half { display: flex; align-items: center; justify-content: center; min-width: 54px; padding: .3rem .5rem;
border-radius: 6px; font: 700 13px ui-monospace, monospace; border: 1.5px solid #cdd9e3;
background: #e8eef3; color: #1d3557; }
.half.left { background: #d6eaf8; border-color: #9abdd6; }
.half.right { background: #fef9e7; border-color: #d4a820; }
.half.changed { outline: 2px solid #e63946; outline-offset: 1px; }
.arrow { color: #888; font-size: .9rem; }
.xor-info { font: 12px ui-monospace, monospace; color: #555; margin-left: .3rem; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin-top: .5rem; }
.status.ok { color: #0a7d33; }
.status.enc { color: #1d3557; }
.header-row { display: flex; align-items: center; gap: .5rem; margin-bottom: .2rem; }
.header-label { font: 700 11px system-ui; color: #888; width: 56px; }
.header-half { font: 700 11px system-ui; color: #888; min-width: 54px; text-align: center; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Simplified 4-round Feistel cipher on an 8-bit block (two 4-bit halves).
// Round keys are fixed constants for demo purposes.
// Round function F(x, k) = ((x * 3 + k) ^ (x << 1 | x >> 3)) & 0xF
// Encryption: (L, R) -> (R, L ^ F(R, K_i)) for each round
// Decryption: same rounds in reverse order with same keys, using the inverse identity.
const ROUNDS = 4;
const ROUND_KEYS = [5, 11, 3, 9]; // 4-bit keys (0-15)
function F(x, k) {
// A simple keyed mixing function on 4-bit values — not invertible in general.
return ((x * 3 + k) ^ ((x << 1 | x >> 3) & 0xF)) & 0xF;
}
function feistelEncrypt(msg) {
// msg is 8-bit; split into two 4-bit halves
let L = (msg >> 4) & 0xF;
let R = msg & 0xF;
const steps = [{ L, R, label: `Input`, xorInfo: '' }];
for (let i = 0; i < ROUNDS; i++) {
const fVal = F(R, ROUND_KEYS[i]);
const newL = R;
const newR = (L ^ fVal) & 0xF;
steps.push({ L: newL, R: newR, label: 'R' + (i + 1), xorInfo: `F=${hex(fVal)}` });
L = newL; R = newR;
}
return { ciphertext: (L << 4) | R, steps };
}
function feistelDecrypt(cipher) {
// Same rounds, reversed key order; inversion identity: L_prev = R_curr ^ F(L_curr, K_i), R_prev = L_curr
let L = (cipher >> 4) & 0xF;
let R = cipher & 0xF;
const steps = [{ L, R, label: `Input`, xorInfo: '' }];
for (let i = ROUNDS - 1; i >= 0; i--) {
const fVal = F(L, ROUND_KEYS[i]);
const newR = L;
const newL = (R ^ fVal) & 0xF;
steps.push({ L: newL, R: newR, label: 'D' + (ROUNDS - i), xorInfo: `F=${hex(fVal)}` });
L = newL; R = newR;
}
return { plaintext: (L << 4) | R, steps };
}
function hex(v) { return '0x' + v.toString(16).toUpperCase().padStart(2, '0'); }
function renderSteps(steps, title) {
const viz = document.getElementById('roundsViz');
viz.innerHTML = '';
// Header
const hdr = document.createElement('div');
hdr.className = 'header-row';
hdr.innerHTML = fmt(`<div class="header-label">%title%</div>
<div class="header-half">Left (H)</div>
<div class="header-half"></div>
<div class="header-half">Right (L)</div>`, { title: title });
viz.appendChild(hdr);
steps.forEach((s, idx) => {
const row = document.createElement('div');
row.className = 'round-row';
const prevL = idx > 0 ? steps[idx - 1].L : null;
const prevR = idx > 0 ? steps[idx - 1].R : null;
const lChanged = prevL !== null && s.L !== prevL;
const rChanged = prevR !== null && s.R !== prevR;
row.innerHTML = `
<div class="round-label">${s.label}</div>
<div class="half left${lChanged ? ' changed' : ''}">${hex(s.L)}</div>
<div class="arrow">⇌</div>
<div class="half right${rChanged ? ' changed' : ''}">${hex(s.R)}</div>
${s.xorInfo ? `<div class="xor-info">${s.xorInfo}</div>` : ''}
`;
viz.appendChild(row);
});
}
let lastCipher = null;
document.getElementById('btnEncrypt').addEventListener('click', () => {
const msg = parseInt(document.getElementById('msgInput').value, 10);
if (isNaN(msg) || msg < 0 || msg > 255) {
setStatus(`Enter a number between 0 and 255.`, '');
return;
}
const { ciphertext, steps } = feistelEncrypt(msg);
lastCipher = ciphertext;
renderSteps(steps, `Enc`);
setStatus(fmt(`Encrypted %hex% → %hex2%`, { hex: hex(msg), hex2: hex(ciphertext) }), 'enc');
document.getElementById('btnDecrypt').disabled = false;
console.log(fmt(`Feistel encrypt: %msg% → %ciphertext% (steps: %stringify%)`, { msg: msg, ciphertext: ciphertext, stringify: JSON.stringify(steps) }));
});
document.getElementById('btnDecrypt').addEventListener('click', () => {
if (lastCipher === null) return;
const { plaintext, steps } = feistelDecrypt(lastCipher);
renderSteps(steps, `Dec`);
const orig = parseInt(document.getElementById('msgInput').value, 10);
const match = plaintext === orig;
setStatus(fmt(`Decrypted %hex% → %hex2%%v%`, { hex: hex(lastCipher), hex2: hex(plaintext), v: match ? ' ✓ matches original!' : ' ✗ mismatch!' }), match ? 'ok' : '');
console.log(fmt(`Feistel decrypt: %lastCipher% → %plaintext% (match=%match%)`, { lastCipher: lastCipher, plaintext: plaintext, match: match }));
});
document.getElementById('btnReset').addEventListener('click', () => {
document.getElementById('roundsViz').innerHTML = '';
document.getElementById('status').textContent = '';
document.getElementById('btnDecrypt').disabled = true;
lastCipher = null;
});
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t;
s.className = 'status ' + cls;
}
Comments
Loading comments...