Skip to content

Commit 2ce0f30

Browse files
feat: standardize theme toggle and README education structure
1 parent 0072aea commit 2ce0f30

5 files changed

Lines changed: 61 additions & 111 deletions

File tree

README.md

Lines changed: 24 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,74 +4,44 @@
44

55
**[Live Demo](https://systemslibrarian.github.io/crypto-lab-format-ward/)**
66

7-
## Overview
7+
## 1. What It Is
88

9-
Format Ward is a browser-based crypto lab demo for format-preserving encryption (FPE) using FF1 and FF3-1 from NIST SP 800-38G.
9+
Format Ward is a browser demo of format-preserving encryption using FF1 and FF3-1 with WebCrypto AES-CBC rounds in a Feistel construction. It solves the problem of protecting sensitive fields while keeping the original character set and field shape, so existing schema constraints continue to work. In this codebase, encryption and decryption are shown for PAN, SSN/phone/ZIP-style formats, and custom alphabets. This is a symmetric-key model: the same secret key material is required for both encryption and decryption.
1010

11-
The demo shows how sensitive values (credit cards, SSNs, phone numbers, ZIP codes, and custom-alphabet strings) can be encrypted while preserving original format constraints so legacy schema assumptions do not break.
11+
## 2. When to Use It
1212

13-
Primary standards references:
13+
- Legacy databases with strict field validation: FF1/FF3-1 let you encrypt values while preserving the original format, so downstream validators and fixed-width schemas continue to accept the data.
14+
- PAN tokenization workflows: preserving decimal length and structure helps payment pipelines that cannot immediately migrate away from format-bound interfaces.
15+
- Masked analytics for structured identifiers: FF1 can protect only the digit positions in SSN/phone-style strings while keeping separators in place for operational readability.
16+
- Cross-system data sharing where format compatibility is mandatory: custom-alphabet FF1 keeps agreed symbol sets and length invariant across parties.
17+
- Do not use this when you need authenticated encryption by itself: FF1/FF3-1 preserve format but do not replace integrity/authenticity controls at the protocol layer.
1418

15-
- NIST SP 800-38G: https://csrc.nist.gov/pubs/sp/800/38/g/final
16-
- NIST SP 800-38G Rev.1 (FF3-1): https://csrc.nist.gov/pubs/sp/800/38/g/r1/final
19+
## 3. Live Demo
1720

18-
## What You Can Explore
21+
Live GitHub Pages demo: https://systemslibrarian.github.io/crypto-lab-format-ward/
1922

20-
1. Credit Card Tokenization panel
21-
2. SSN / Phone / Postal format masking panel
22-
3. FF1 vs FF3-1 side-by-side timing and output comparison
23-
4. Custom alphabet FF1 encryption and decryption
23+
The demo supports both encrypt and decrypt flows in each panel and displays round-trip results so you can verify reversibility. You can run FF1 and FF3-1 side-by-side, compare output and timing, and inspect Luhn validity behavior on ciphertext for PAN examples. Exposed controls include plaintext/format selectors, AES-256 key generation, FF1 tweak input, FF3-1 tweak input (14 hex chars), and custom alphabet selection.
2424

25-
## Primitives Used
25+
## 4. What Can Go Wrong
2626

27-
- FF1 (NIST SP 800-38G)
28-
- FF3-1 (NIST SP 800-38G Rev.1)
29-
- AES via WebCrypto (`AES-CBC`) as the underlying block primitive
30-
- Feistel round structure per standard mode definitions
27+
- FF3-1 margin assumptions: FF3-1 has published differential-cryptanalysis results relative to FF1, which is why this demo marks FF1 as the preferred default for new systems.
28+
- Wrong tweak size for FF3-1: FF3-1 requires exactly a 56-bit tweak (14 hex chars), and using the wrong length breaks interoperability and security assumptions.
29+
- Small-domain misuse: very small message spaces reduce effective security for format-preserving schemes because exhaustive or statistical attacks become more practical.
30+
- Deterministic reuse patterns: reusing the same key/tweak configuration on repeated structured fields can leak equality patterns even though plaintext is hidden.
31+
- Alphabet/radix mismatch bugs: if application characters and radix mapping are inconsistent, encryption can fail or silently produce invalid domain behavior for downstream systems.
3132

32-
## Running Locally
33+
## 5. Real-World Usage
3334

34-
```bash
35-
npm install
36-
npm run dev
37-
```
38-
39-
Build and preview:
40-
41-
```bash
42-
npm run build
43-
npm run preview
44-
```
45-
46-
## GitHub Pages
47-
48-
The Vite base path is resolved automatically during GitHub Actions builds from `GITHUB_REPOSITORY`, so project-page deploys keep working after forks or repository renames.
49-
50-
If you need to override it manually, set `PAGES_BASE_PATH` before building.
51-
52-
Run vector checks:
53-
54-
```bash
55-
npm run test
56-
```
57-
58-
## Security Notes
59-
60-
- FF1 is the preferred choice for new deployments in this demo.
61-
- FF3-1 has known differential-attack literature and reduced margin compared to FF1.
62-
- The FF3/FF3-1 line of analysis was highlighted by Durak & Vaudenay (2017); this demo surfaces that caveat directly in UI and documentation.
63-
- Always treat demo code as educational and validate operational choices against your threat model and compliance requirements.
64-
65-
## Why This Matters
66-
67-
Many production systems cannot change field lengths or character constraints without expensive schema and integration rewrites.
68-
69-
FPE allows encryption while preserving the visible format shape, which is useful for tokenization, safe analytics, and controlled data sharing in constrained legacy environments.
35+
- NIST SP 800-38G and SP 800-38G Rev.1: these standards define FF1 and FF3-1 and are the baseline references used by compliant implementations.
36+
- PCI-oriented tokenization deployments: payment environments commonly use NIST FPE modes (especially FF1) to protect PAN data without breaking numeric format constraints.
37+
- OpenText Voltage SecureData: this enterprise data-protection platform documents format-preserving encryption deployments for structured fields.
38+
- Protegrity data protection platforms: Protegrity materials describe FPE usage for structured-data tokenization in regulated environments.
39+
- Application security toolkits such as Bouncy Castle: widely used libraries include FF1/FF3-1 primitives that are integrated into production JVM systems handling structured identifiers.
7040

7141
## Related Demos
7242

7343
- crypto-compare (Format-Preserving Encryption category): https://github.com/systemslibrarian/crypto-compare
7444
- crypto-lab landing page: https://github.com/systemslibrarian/crypto-lab
7545
- crypto-lab-iron-letter: https://github.com/systemslibrarian/crypto-lab-iron-letter
7646

77-
So whether you eat or drink or whatever you do, do it all for the glory of God. — 1 Corinthians 10:31
47+
> *"So whether you eat or drink or whatever you do, do it all for the glory of God." — 1 Corinthians 10:31*

index.html

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
<!doctype html>
22
<html lang="en">
33
<head>
4+
<script>
5+
(function () {
6+
const saved = localStorage.getItem('theme');
7+
document.documentElement.setAttribute('data-theme', saved ?? 'dark');
8+
})();
9+
</script>
410
<meta charset="UTF-8" />
511
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
612
<meta name="description" content="Format Ward — Interactive browser demo of Format-Preserving Encryption (FF1 and FF3-1) over real WebCrypto AES rounds, per NIST SP 800-38G." />
7-
<meta name="theme-color" content="#0a1216" media="(prefers-color-scheme: dark)" />
8-
<meta name="theme-color" content="#f8fbff" media="(prefers-color-scheme: light)" />
13+
<meta name="theme-color" content="#0a1216" />
914
<title>Format Ward | crypto-lab-format-ward</title>
1015
<link rel="stylesheet" href="./styles/main.css" />
1116
</head>
1217
<body>
1318
<div id="app"></div>
14-
<script type="module" src="./src/ui.ts"></script>
19+
<script type="module" src="./src/main.ts"></script>
1520
</body>
1621
</html>

src/main.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,28 @@
1-
// Entry placeholder kept to match repo structure.
2-
export {};
1+
import { initUI } from "./ui";
2+
3+
function installThemeToggle(): void {
4+
const root = document.documentElement;
5+
const button = document.getElementById("theme-toggle") as HTMLButtonElement | null;
6+
if (!button) {
7+
return;
8+
}
9+
10+
const syncThemeButton = (): void => {
11+
const isDark = root.getAttribute("data-theme") !== "light";
12+
button.textContent = isDark ? "🌙" : "☀️";
13+
button.setAttribute("aria-label", isDark ? "Switch to light mode" : "Switch to dark mode");
14+
};
15+
16+
syncThemeButton();
17+
18+
button.addEventListener("click", () => {
19+
const current = root.getAttribute("data-theme") === "light" ? "light" : "dark";
20+
const next = current === "dark" ? "light" : "dark";
21+
root.setAttribute("data-theme", next);
22+
localStorage.setItem("theme", next);
23+
syncThemeButton();
24+
});
25+
}
26+
27+
initUI();
28+
installThemeToggle();

src/ui.ts

Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ function setText(id: string, text: string): void {
3030
el.textContent = text;
3131
}
3232
}
33-
3433
function getInputValue(id: string): string {
3534
const el = document.getElementById(id) as HTMLInputElement | null;
3635
if (!el) {
@@ -120,35 +119,6 @@ async function runVectorSmokeCheck(): Promise<void> {
120119
}
121120
}
122121

123-
function installThemeToggle(): void {
124-
const button = document.getElementById("theme-toggle") as HTMLButtonElement | null;
125-
const root = document.documentElement;
126-
127-
const saved = localStorage.getItem("format-ward-theme");
128-
if (saved === "light" || saved === "dark") {
129-
root.dataset.theme = saved;
130-
} else {
131-
root.dataset.theme = "dark";
132-
}
133-
134-
const syncLabel = (): void => {
135-
const isDark = root.dataset.theme !== "light";
136-
if (button) {
137-
button.textContent = isDark ? "🌙" : "☀️";
138-
button.setAttribute("aria-label", isDark ? "Switch to light mode" : "Switch to dark mode");
139-
}
140-
};
141-
142-
syncLabel();
143-
144-
button?.addEventListener("click", () => {
145-
const next = root.dataset.theme === "light" ? "dark" : "light";
146-
root.dataset.theme = next;
147-
localStorage.setItem("format-ward-theme", next);
148-
syncLabel();
149-
});
150-
}
151-
152122
function wireKeyGenerators(): void {
153123
const pairs: Array<{ buttonId: string; inputId: string }> = [
154124
{ buttonId: "cc-key-gen", inputId: "cc-key" },
@@ -306,7 +276,7 @@ function template(): string {
306276
<header class="hero" role="banner">
307277
<div class="chip-row" role="list" aria-label="Category and controls">
308278
<span class="chip category" role="listitem">Format-Preserving Encryption</span>
309-
<button id="theme-toggle" class="theme-toggle" type="button" aria-label="Switch color theme"></button>
279+
<button id="theme-toggle" class="theme-toggle" type="button" aria-label="Switch to light mode"></button>
310280
</div>
311281
<h1>Format Ward</h1>
312282
<p class="subtitle">Interactive FF1 and FF3-1 demo over real WebCrypto AES rounds from NIST SP 800-38G.</p>
@@ -494,13 +464,10 @@ export function initUI(): void {
494464
}
495465

496466
app.innerHTML = template();
497-
installThemeToggle();
498467
wireKeyGenerators();
499468
wirePanel1();
500469
wirePanel2();
501470
wirePanel3();
502471
wirePanel4();
503472
runVectorSmokeCheck();
504473
}
505-
506-
initUI();

styles/main.css

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,24 +38,6 @@
3838
--focus-ring: var(--accent-2);
3939
}
4040

41-
/* Respect system-level preference when no JS theme is stored */
42-
@media (prefers-color-scheme: light) {
43-
:root:not([data-theme]) {
44-
--bg: #f8fbff;
45-
--bg-soft: #e6f0fa;
46-
--surface: #ffffff;
47-
--surface-2: #f0f6fd;
48-
--text: #10222d;
49-
--muted: #4e6370;
50-
--accent: #ef6f00;
51-
--accent-2: #0f9b8e;
52-
--danger: #b13030;
53-
--border: #c6d8e7;
54-
--shadow: rgba(20, 30, 40, 0.12);
55-
--focus-ring: var(--accent-2);
56-
}
57-
}
58-
5941
/* ── Reset ── */
6042
*,
6143
*::before,

0 commit comments

Comments
 (0)