<!-- FPE demo: encrypt/decrypt within a custom alphabet using a toy Feistel network -->
<div class="fpe-wrap">
<div class="field-row">
<label for="alphabet">Alphabet (radix)</label>
<input id="alphabet" type="text" value="0123456789" maxlength="64" spellcheck="false" placeholder="e.g. 0123456789" title="Characters to use in plaintext and ciphertext" aria-label="Alphabet for encryption">
</div>
<div class="field-row">
<label for="key-input">Key (numeric)</label>
<input id="key-input" type="number" value="42" min="1" max="9999" title="Numeric key for the Feistel round function" aria-label="Numeric key">
</div>
<div class="field-row">
<label for="plaintext">Plaintext</label>
<input id="plaintext" type="text" value="4532015112830366" spellcheck="false" placeholder="Enter text using the alphabet above" title="Text to encrypt — must only use characters from the alphabet" aria-label="Plaintext input">
</div>
<div class="btns">
<button id="btn-enc" type="button">Encrypt</button>
<button id="btn-dec" type="button">Decrypt</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div class="output-grid">
<div class="out-box">
<div class="out-label">Ciphertext</div>
<div class="out-value" id="ct-value">—</div>
</div>
<div class="out-box">
<div class="out-label">Decrypted</div>
<div class="out-value" id="dec-value">—</div>
</div>
</div>
<div class="status" id="status"></div>
<div class="explainer" id="explainer"></div>
</div>
/* FPE demo styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .4rem; }
.fpe-wrap { max-width: 480px; }
.field-row { display: flex; flex-direction: column; margin-bottom: .7rem; }
label { font-size: .8rem; font-weight: 600; color: #555; margin-bottom: .25rem; text-transform: uppercase; letter-spacing: .04em; }
input[type=text], input[type=number] {
font: 500 15px ui-monospace, monospace; padding: .4rem .6rem;
border: 1px solid #c0c8d2; border-radius: 8px; outline: none;
transition: border-color .15s; background: #f8f9fa; }
input:focus { border-color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .9rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.output-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; margin-bottom: .7rem; }
.out-box { background: #e8eef3; border: 1px solid #cdd9e3; border-radius: 8px; padding: .6rem .8rem; }
.out-label { font-size: .75rem; font-weight: 600; color: #5a7088; text-transform: uppercase; letter-spacing: .04em; margin-bottom: .3rem; }
.out-value { font: 700 15px ui-monospace, monospace; color: #1d3557; word-break: break-all; min-height: 1.4em; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.3em; margin-bottom: .4rem; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.explainer { font-size: .82rem; color: #555; line-height: 1.5; background: #f0f4f8; border-radius: 8px; padding: .6rem .8rem; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Toy Format-Preserving Encryption demo using a Feistel network.
// The Feistel structure guarantees a bijection (permutation) over the domain r^m,
// so ciphertext length and alphabet always match plaintext.
// Read and deduplicate the alphabet from the input.
function getAlphabet() {
const raw = document.getElementById('alphabet').value;
// Remove duplicate characters so the radix is exactly alpha.length.
return [...new Set(raw)].join('');
}
// Interpret a string as a base-r number (left digit is most significant).
function strToNum(s, alpha) {
const r = alpha.length;
let n = 0n;
for (const ch of s) {
n = n * BigInt(r) + BigInt(alpha.indexOf(ch));
}
return n;
}
// Convert a big integer back to a fixed-length base-r string.
function numToStr(n, len, alpha) {
const r = BigInt(alpha.length);
const out = [];
for (let i = 0; i < len; i++) {
out.push(alpha[Number(n % r)]);
n /= r;
}
return out.reverse().join('');
}
// Toy round function: a deterministic hash of (value, round, key) reduced mod m.
// In real FF1/FF3 the round function is AES in CBC or ECB mode.
function roundFn(val, round, key, mod) {
// Multiply-xor-shift mixing (Murmur3-inspired), no crypto security claimed.
let h = (val * 2654435761n + BigInt(round) * 1234567891n + BigInt(key)) % 4294967296n;
h = (h ^ (h >> 16n)) * 2246822519n % 4294967296n;
h = (h ^ (h >> 13n)) * 3266489917n % 4294967296n;
h = h ^ (h >> 16n);
return h % mod;
}
const ROUNDS = 8;
// Encrypt: 8-round Feistel over two halves A (length u) and B (length v=m-u).
function feistelEncrypt(s, alpha, key) {
const r = BigInt(alpha.length);
const m = s.length;
const u = Math.floor(m / 2);
const v = m - u;
let modA = r ** BigInt(u);
let modB = r ** BigInt(v);
let A = strToNum(s.slice(0, u), alpha);
let B = strToNum(s.slice(u), alpha);
for (let i = 0; i < ROUNDS; i++) {
// Add F(B) mod domain_A to A, then swap A and B.
const F = roundFn(B, i, key, modA);
const newA = (A + F) % modA;
A = B;
B = newA;
// Swap the moduli so each half alternates between domain_u and domain_v.
const tmp = modA; modA = modB; modB = tmp;
}
// After an even number of rounds A is still the left half; odd flips them.
if (ROUNDS % 2 === 0) {
return numToStr(A, u, alpha) + numToStr(B, v, alpha);
}
return numToStr(A, v, alpha) + numToStr(B, u, alpha);
}
// Decrypt: reverse the Feistel rounds by running them backwards.
function feistelDecrypt(s, alpha, key) {
const r = BigInt(alpha.length);
const m = s.length;
const u = Math.floor(m / 2);
const v = m - u;
let modA = r ** BigInt(u);
let modB = r ** BigInt(v);
// Split ciphertext respecting the parity-dependent half ordering.
let A, B;
if (ROUNDS % 2 === 0) {
A = strToNum(s.slice(0, u), alpha);
B = strToNum(s.slice(u), alpha);
} else {
A = strToNum(s.slice(0, v), alpha);
B = strToNum(s.slice(v), alpha);
}
// If ROUNDS is odd the moduli are swapped at the end of encrypt.
if (ROUNDS % 2 !== 0) { const tmp = modA; modA = modB; modB = tmp; }
// Undo each round in reverse: swap mods back, invert A = B, B = (A-F) mod modB.
for (let i = ROUNDS - 1; i >= 0; i--) {
// Undo the mod swap that happened at the end of this forward round.
const tmp = modA; modA = modB; modB = tmp;
// After restoring modA, invert: old_B=A (current), old_A=(B-F(A,modA))%modA.
const F = roundFn(A, i, key, modA);
const prevOldA = ((B - F) % modA + modA) % modA;
const prevOldB = A;
A = prevOldA;
B = prevOldB;
}
return numToStr(A, u, alpha) + numToStr(B, v, alpha);
}
function validate(text, alpha) {
for (const ch of text) {
if (!alpha.includes(ch)) return ch;
}
return null;
}
function setStatus(msg, cls) {
const s = document.getElementById('status');
s.textContent = msg;
s.className = 'status ' + (cls || '');
}
function setExplainer(msg) {
document.getElementById('explainer').innerHTML = msg;
}
function domainStr(r, m) {
try {
const v = Math.pow(r, m);
if (!isFinite(v)) return r + '^' + m;
return v.toLocaleString();
} catch(e) { return r + '^' + m; }
}
function doEncrypt() {
const alpha = getAlphabet();
const key = parseInt(document.getElementById('key-input').value) || 1;
const pt = document.getElementById('plaintext').value;
document.getElementById('dec-value').textContent = '—';
if (alpha.length < 2) { setStatus('Alphabet must have at least 2 characters.', 'bad'); return; }
if (pt.length < 2) { setStatus('Plaintext must be at least 2 characters.', 'bad'); return; }
const bad = validate(pt, alpha);
if (bad !== null) {
setStatus(fmt('Character '%bad%' is not in the alphabet.', { bad: bad }), 'bad');
return;
}
const ct = feistelEncrypt(pt, alpha, key);
document.getElementById('ct-value').textContent = ct;
setStatus('Encrypted — same alphabet, same length.', 'ok');
setExplainer(fmt('Radix r = %length%, length m = %length2%. Domain size r^m = %domainStr% possible strings — the Feistel permutes all of them.', { length: alpha.length, length2: pt.length, domainStr: domainStr(alpha.length, pt.length) }));
}
function doDecrypt() {
const alpha = getAlphabet();
const key = parseInt(document.getElementById('key-input').value) || 1;
const ct = document.getElementById('ct-value').textContent;
if (ct === '—' || ct === '') { setStatus('Encrypt first, then decrypt.', 'bad'); return; }
const pt2 = feistelDecrypt(ct, alpha, key);
document.getElementById('dec-value').textContent = pt2;
const original = document.getElementById('plaintext').value;
if (pt2 === original) {
setStatus('Decrypted — matches original plaintext perfectly.', 'ok');
} else {
setStatus('Mismatch — key or alphabet changed?', 'bad');
}
}
function doReset() {
document.getElementById('alphabet').value = '0123456789';
document.getElementById('key-input').value = '42';
document.getElementById('plaintext').value = '4532015112830366';
document.getElementById('ct-value').textContent = '—';
document.getElementById('dec-value').textContent = '—';
setStatus('', '');
setExplainer('');
}
document.getElementById('btn-enc').addEventListener('click', doEncrypt);
document.getElementById('btn-dec').addEventListener('click', doDecrypt);
document.getElementById('btn-reset').addEventListener('click', doReset);
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Toy Format-Preserving Encryption demo using a Feistel network.
// The Feistel structure guarantees a bijection (permutation) over the domain r^m,
// so ciphertext length and alphabet always match plaintext.
// Read and deduplicate the alphabet from the input.
function getAlphabet() {
const raw = document.getElementById('alphabet').value;
// Remove duplicate characters so the radix is exactly alpha.length.
return [...new Set(raw)].join('');
}
// Interpret a string as a base-r number (left digit is most significant).
function strToNum(s, alpha) {
const r = alpha.length;
let n = 0n;
for (const ch of s) {
n = n * BigInt(r) + BigInt(alpha.indexOf(ch));
}
return n;
}
// Convert a big integer back to a fixed-length base-r string.
function numToStr(n, len, alpha) {
const r = BigInt(alpha.length);
const out = [];
for (let i = 0; i < len; i++) {
out.push(alpha[Number(n % r)]);
n /= r;
}
return out.reverse().join('');
}
// Toy round function: a deterministic hash of (value, round, key) reduced mod m.
// In real FF1/FF3 the round function is AES in CBC or ECB mode.
function roundFn(val, round, key, mod) {
// Multiply-xor-shift mixing (Murmur3-inspired), no crypto security claimed.
let h = (val * 2654435761n + BigInt(round) * 1234567891n + BigInt(key)) % 4294967296n;
h = (h ^ (h >> 16n)) * 2246822519n % 4294967296n;
h = (h ^ (h >> 13n)) * 3266489917n % 4294967296n;
h = h ^ (h >> 16n);
return h % mod;
}
const ROUNDS = 8;
// Encrypt: 8-round Feistel over two halves A (length u) and B (length v=m-u).
function feistelEncrypt(s, alpha, key) {
const r = BigInt(alpha.length);
const m = s.length;
const u = Math.floor(m / 2);
const v = m - u;
let modA = r ** BigInt(u);
let modB = r ** BigInt(v);
let A = strToNum(s.slice(0, u), alpha);
let B = strToNum(s.slice(u), alpha);
for (let i = 0; i < ROUNDS; i++) {
// Add F(B) mod domain_A to A, then swap A and B.
const F = roundFn(B, i, key, modA);
const newA = (A + F) % modA;
A = B;
B = newA;
// Swap the moduli so each half alternates between domain_u and domain_v.
const tmp = modA; modA = modB; modB = tmp;
}
// After an even number of rounds A is still the left half; odd flips them.
if (ROUNDS % 2 === 0) {
return numToStr(A, u, alpha) + numToStr(B, v, alpha);
}
return numToStr(A, v, alpha) + numToStr(B, u, alpha);
}
// Decrypt: reverse the Feistel rounds by running them backwards.
function feistelDecrypt(s, alpha, key) {
const r = BigInt(alpha.length);
const m = s.length;
const u = Math.floor(m / 2);
const v = m - u;
let modA = r ** BigInt(u);
let modB = r ** BigInt(v);
// Split ciphertext respecting the parity-dependent half ordering.
let A, B;
if (ROUNDS % 2 === 0) {
A = strToNum(s.slice(0, u), alpha);
B = strToNum(s.slice(u), alpha);
} else {
A = strToNum(s.slice(0, v), alpha);
B = strToNum(s.slice(v), alpha);
}
// If ROUNDS is odd the moduli are swapped at the end of encrypt.
if (ROUNDS % 2 !== 0) { const tmp = modA; modA = modB; modB = tmp; }
// Undo each round in reverse: swap mods back, invert A = B, B = (A-F) mod modB.
for (let i = ROUNDS - 1; i >= 0; i--) {
// Undo the mod swap that happened at the end of this forward round.
const tmp = modA; modA = modB; modB = tmp;
// After restoring modA, invert: old_B=A (current), old_A=(B-F(A,modA))%modA.
const F = roundFn(A, i, key, modA);
const prevOldA = ((B - F) % modA + modA) % modA;
const prevOldB = A;
A = prevOldA;
B = prevOldB;
}
return numToStr(A, u, alpha) + numToStr(B, v, alpha);
}
function validate(text, alpha) {
for (const ch of text) {
if (!alpha.includes(ch)) return ch;
}
return null;
}
function setStatus(msg, cls) {
const s = document.getElementById('status');
s.textContent = msg;
s.className = 'status ' + (cls || '');
}
function setExplainer(msg) {
document.getElementById('explainer').innerHTML = msg;
}
function domainStr(r, m) {
try {
const v = Math.pow(r, m);
if (!isFinite(v)) return r + '^' + m;
return v.toLocaleString();
} catch(e) { return r + '^' + m; }
}
function doEncrypt() {
const alpha = getAlphabet();
const key = parseInt(document.getElementById('key-input').value) || 1;
const pt = document.getElementById('plaintext').value;
document.getElementById('dec-value').textContent = '—';
if (alpha.length < 2) { setStatus('Alphabet must have at least 2 characters.', 'bad'); return; }
if (pt.length < 2) { setStatus('Plaintext must be at least 2 characters.', 'bad'); return; }
const bad = validate(pt, alpha);
if (bad !== null) {
setStatus(fmt('Character '%bad%' is not in the alphabet.', { bad: bad }), 'bad');
return;
}
const ct = feistelEncrypt(pt, alpha, key);
document.getElementById('ct-value').textContent = ct;
setStatus('Encrypted — same alphabet, same length.', 'ok');
setExplainer(fmt('Radix r = %length%, length m = %length2%. Domain size r^m = %domainStr% possible strings — the Feistel permutes all of them.', { length: alpha.length, length2: pt.length, domainStr: domainStr(alpha.length, pt.length) }));
}
function doDecrypt() {
const alpha = getAlphabet();
const key = parseInt(document.getElementById('key-input').value) || 1;
const ct = document.getElementById('ct-value').textContent;
if (ct === '—' || ct === '') { setStatus('Encrypt first, then decrypt.', 'bad'); return; }
const pt2 = feistelDecrypt(ct, alpha, key);
document.getElementById('dec-value').textContent = pt2;
const original = document.getElementById('plaintext').value;
if (pt2 === original) {
setStatus('Decrypted — matches original plaintext perfectly.', 'ok');
} else {
setStatus('Mismatch — key or alphabet changed?', 'bad');
}
}
function doReset() {
document.getElementById('alphabet').value = '0123456789';
document.getElementById('key-input').value = '42';
document.getElementById('plaintext').value = '4532015112830366';
document.getElementById('ct-value').textContent = '—';
document.getElementById('dec-value').textContent = '—';
setStatus('', '');
setExplainer('');
}
document.getElementById('btn-enc').addEventListener('click', doEncrypt);
document.getElementById('btn-dec').addEventListener('click', doDecrypt);
document.getElementById('btn-reset').addEventListener('click', doReset);
Comments
Loading comments...