Last active
June 21, 2026 12:42
-
-
Save secdev02/d815a34f3858a828aaccb71f48b55419 to your computer and use it in GitHub Desktop.
HTML One Time Pad
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>OTP Substitution Cipher</title> | |
| <style> | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| :root { | |
| --bg: #ffffff; | |
| --bg2: #f5f5f3; | |
| --bg3: #ebebea; | |
| --text: #1a1a19; | |
| --text2: #6b6b67; | |
| --text3: #a0a09c; | |
| --border: rgba(0,0,0,0.12); | |
| --border2: rgba(0,0,0,0.22); | |
| --info-bg: #e6f1fb; | |
| --info-text: #185fa5; | |
| --info-border: #b5d4f4; | |
| --danger-bg: #fcebeb; | |
| --danger-text: #a32d2d; | |
| --danger-border: #f7c1c1; | |
| --success-bg: #eaf3de; | |
| --success-text: #3b6d11; | |
| --success-border: #c0dd97; | |
| --radius: 8px; | |
| --radius-lg: 12px; | |
| --mono: 'Courier New', Courier, monospace; | |
| --sans: system-ui, -apple-system, sans-serif; | |
| } | |
| @media (prefers-color-scheme: dark) { | |
| :root { | |
| --bg: #1e1e1c; | |
| --bg2: #2a2a28; | |
| --bg3: #323230; | |
| --text: #f0efe9; | |
| --text2: #a0a09c; | |
| --text3: #6b6b67; | |
| --border: rgba(255,255,255,0.12); | |
| --border2: rgba(255,255,255,0.22); | |
| --info-bg: #0c2d4a; | |
| --info-text: #85b7eb; | |
| --info-border: #185fa5; | |
| --danger-bg: #3a1010; | |
| --danger-text: #f09595; | |
| --danger-border: #a32d2d; | |
| --success-bg: #17340a; | |
| --success-text: #97c459; | |
| --success-border: #3b6d11; | |
| } | |
| } | |
| body { | |
| font-family: var(--sans); | |
| background: var(--bg); | |
| color: var(--text); | |
| min-height: 100vh; | |
| padding: 2rem 1rem; | |
| } | |
| .container { max-width: 720px; margin: 0 auto; } | |
| h1 { font-size: 20px; font-weight: 500; margin-bottom: 0.25rem; } | |
| .subtitle { | |
| font-size: 13px; | |
| color: var(--text2); | |
| margin-bottom: 1.75rem; | |
| line-height: 1.6; | |
| } | |
| .tabs { | |
| display: flex; | |
| border-bottom: 0.5px solid var(--border); | |
| margin-bottom: 1.5rem; | |
| } | |
| .tab { | |
| padding: 8px 18px; | |
| font-size: 13px; | |
| cursor: pointer; | |
| border: none; | |
| background: transparent; | |
| color: var(--text2); | |
| border-bottom: 2px solid transparent; | |
| margin-bottom: -0.5px; | |
| transition: color 0.15s; | |
| font-family: var(--sans); | |
| } | |
| .tab.active { color: var(--text); border-bottom-color: var(--text); } | |
| .tab:hover { color: var(--text); } | |
| .panel { display: none; } | |
| .panel.active { display: block; } | |
| label { | |
| display: block; | |
| font-size: 11px; | |
| color: var(--text2); | |
| margin-bottom: 5px; | |
| letter-spacing: 0.05em; | |
| text-transform: uppercase; | |
| } | |
| textarea { | |
| width: 100%; | |
| height: 110px; | |
| resize: vertical; | |
| font-family: var(--mono); | |
| font-size: 12px; | |
| background: var(--bg2); | |
| color: var(--text); | |
| border: 0.5px solid var(--border); | |
| border-radius: var(--radius); | |
| padding: 9px 11px; | |
| line-height: 1.6; | |
| } | |
| textarea:focus, input:focus, select:focus { | |
| outline: none; | |
| border-color: var(--border2); | |
| } | |
| input[type="text"], input[type="number"] { | |
| font-family: var(--mono); | |
| font-size: 12px; | |
| padding: 7px 10px; | |
| background: var(--bg2); | |
| color: var(--text); | |
| border: 0.5px solid var(--border); | |
| border-radius: var(--radius); | |
| width: 100%; | |
| } | |
| select { | |
| font-size: 12px; | |
| padding: 7px 10px; | |
| background: var(--bg2); | |
| color: var(--text); | |
| border: 0.5px solid var(--border); | |
| border-radius: var(--radius); | |
| width: 100%; | |
| } | |
| button { | |
| font-size: 12px; | |
| font-family: var(--sans); | |
| padding: 7px 14px; | |
| border-radius: var(--radius); | |
| border: 0.5px solid var(--border2); | |
| background: transparent; | |
| color: var(--text); | |
| cursor: pointer; | |
| transition: background 0.15s, color 0.15s, border-color 0.15s; | |
| white-space: nowrap; | |
| } | |
| button:hover { background: var(--bg2); } | |
| button:active { transform: scale(0.98); } | |
| button.primary { | |
| background: var(--info-bg); | |
| color: var(--info-text); | |
| border-color: var(--info-border); | |
| } | |
| button.primary:hover { opacity: 0.85; } | |
| button.danger { | |
| background: var(--danger-bg); | |
| color: var(--danger-text); | |
| border-color: var(--danger-border); | |
| } | |
| button.success { | |
| background: var(--success-bg) !important; | |
| color: var(--success-text) !important; | |
| border-color: var(--success-border) !important; | |
| } | |
| .row { | |
| display: flex; | |
| gap: 8px; | |
| flex-wrap: wrap; | |
| margin-bottom: 1rem; | |
| align-items: flex-end; | |
| } | |
| .col { flex: 1; min-width: 110px; } | |
| .mono-box { | |
| font-family: var(--mono); | |
| font-size: 12px; | |
| background: var(--bg2); | |
| border: 0.5px solid var(--border); | |
| border-radius: var(--radius); | |
| padding: 10px 12px; | |
| color: var(--text); | |
| word-break: break-all; | |
| white-space: pre-wrap; | |
| line-height: 1.8; | |
| min-height: 48px; | |
| } | |
| .output-wrap { | |
| position: relative; | |
| margin-bottom: 0.5rem; | |
| } | |
| .output-actions { | |
| display: flex; | |
| gap: 6px; | |
| margin-top: 6px; | |
| flex-wrap: wrap; | |
| } | |
| .pad-out { | |
| font-family: var(--mono); | |
| font-size: 12px; | |
| background: var(--bg2); | |
| border: 0.5px solid var(--border); | |
| border-radius: var(--radius); | |
| padding: 10px 12px; | |
| color: var(--text); | |
| max-height: 200px; | |
| overflow-y: auto; | |
| line-height: 2; | |
| word-break: break-all; | |
| white-space: pre-wrap; | |
| } | |
| .note { | |
| font-size: 11px; | |
| color: var(--text3); | |
| line-height: 1.6; | |
| margin-top: 8px; | |
| } | |
| .sec { margin-bottom: 1.25rem; } | |
| .badge { | |
| display: inline-block; | |
| font-size: 11px; | |
| padding: 2px 8px; | |
| border-radius: var(--radius); | |
| margin-left: 6px; | |
| background: var(--info-bg); | |
| color: var(--info-text); | |
| border: 0.5px solid var(--info-border); | |
| } | |
| .divider { | |
| border: none; | |
| border-top: 0.5px solid var(--border); | |
| margin: 1.25rem 0; | |
| } | |
| .status { font-size: 11px; color: var(--danger-text); margin-top: 4px; } | |
| #userSeedRow { display: none; } | |
| .save-all-bar { | |
| margin-top: 2rem; | |
| padding-top: 1.25rem; | |
| border-top: 0.5px solid var(--border); | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| flex-wrap: wrap; | |
| } | |
| .save-all-label { | |
| font-size: 11px; | |
| color: var(--text3); | |
| letter-spacing: 0.05em; | |
| text-transform: uppercase; | |
| white-space: nowrap; | |
| } | |
| .toast { | |
| position: fixed; | |
| bottom: 24px; | |
| left: 50%; | |
| transform: translateX(-50%) translateY(60px); | |
| background: var(--text); | |
| color: var(--bg); | |
| font-size: 12px; | |
| padding: 8px 18px; | |
| border-radius: var(--radius); | |
| opacity: 0; | |
| transition: opacity 0.2s, transform 0.2s; | |
| pointer-events: none; | |
| z-index: 999; | |
| white-space: nowrap; | |
| } | |
| .toast.show { | |
| opacity: 1; | |
| transform: translateX(-50%) translateY(0); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>OTP Substitution Cipher</h1> | |
| <p class="subtitle"> | |
| One-time pad generator (L'Ecuyer PRNG + Bays-Durham shuffle) combined with a positional substitution cipher. | |
| Everything runs in your browser — nothing is transmitted. | |
| </p> | |
| <div class="tabs"> | |
| <button class="tab active" onclick="switchTab('otp', this)">OTP generator</button> | |
| <button class="tab" onclick="switchTab('encode', this)">Encode</button> | |
| <button class="tab" onclick="switchTab('decode', this)">Decode</button> | |
| <button class="tab" onclick="switchTab('keys', this)">Keys</button> | |
| </div> | |
| <!-- OTP GENERATOR --> | |
| <div class="panel active" id="panel-otp"> | |
| <div class="row"> | |
| <div class="col"> | |
| <label>Number of keys</label> | |
| <input type="number" id="numKeys" value="10" min="1" max="500"> | |
| </div> | |
| <div class="col"> | |
| <label>Key length (chars)</label> | |
| <input type="number" id="keyLen" value="20" min="4" max="200"> | |
| </div> | |
| <div class="col"> | |
| <label>Group size (0 = none)</label> | |
| <input type="number" id="grpLen" value="5" min="0" max="20"> | |
| </div> | |
| </div> | |
| <div class="row"> | |
| <div class="col"> | |
| <label>Composition</label> | |
| <select id="composition"> | |
| <option value="alpha">Alphabetic</option> | |
| <option value="numeric">Numeric</option> | |
| <option value="wordlike">Word-like</option> | |
| <option value="gibberish">Gibberish (most secure)</option> | |
| </select> | |
| </div> | |
| <div class="col"> | |
| <label>Case</label> | |
| <select id="letterCase"> | |
| <option value="upper">Upper case</option> | |
| <option value="lower">Lower case</option> | |
| <option value="mixed">Mixed case</option> | |
| </select> | |
| </div> | |
| <div class="col"> | |
| <label>Seed</label> | |
| <select id="seedMode" onchange="toggleSeed()"> | |
| <option value="clock">From clock</option> | |
| <option value="user">User-defined</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="row" id="userSeedRow"> | |
| <div class="col" style="flex:2"> | |
| <label>Custom seed (digits or any text)</label> | |
| <input type="text" id="userSeed" placeholder="Enter seed string or number"> | |
| </div> | |
| </div> | |
| <div class="row"> | |
| <button class="primary" onclick="generatePad()">Generate pad</button> | |
| <button class="danger" onclick="loadPadAsCipherKey()">Use pad as cipher key →</button> | |
| </div> | |
| <label>Generated one-time pad</label> | |
| <div class="pad-out" id="padOutput">Press "Generate pad" to create keys.</div> | |
| <div class="output-actions"> | |
| <button onclick="copyTextFrom('padOutput', this)">Copy pad</button> | |
| <button onclick="saveText('padOutput', 'otp-pad.txt')">Save pad as .txt</button> | |
| </div> | |
| <p class="note"> | |
| L'Ecuyer PRNG with Bays-Durham shuffle — same algorithm as Fourmilab's OTP generator. | |
| "Use pad as cipher key" extracts the first 26 unique letters from the pad as your substitution key. | |
| </p> | |
| </div> | |
| <!-- ENCODE --> | |
| <div class="panel" id="panel-encode"> | |
| <div class="sec"> | |
| <label>Active cipher key <span class="badge" id="enc-key-badge">none</span></label> | |
| <div class="mono-box" id="enc-key-display" style="font-size:11px"> | |
| No key loaded. Generate a pad and press "Use as cipher key", or set one in the Keys tab. | |
| </div> | |
| </div> | |
| <div class="sec"> | |
| <label>Plaintext input</label> | |
| <textarea id="plainInput" placeholder="Paste your proprietary content here..."></textarea> | |
| </div> | |
| <div class="row"> | |
| <button class="primary" onclick="doEncode()">Encode →</button> | |
| <button onclick="document.getElementById('plainInput').value='';document.getElementById('cipherOutput').textContent=''">Clear</button> | |
| </div> | |
| <label>Ciphertext output</label> | |
| <div class="mono-box" id="cipherOutput"></div> | |
| <div class="output-actions"> | |
| <button onclick="copyTextFrom('cipherOutput', this)">Copy ciphertext</button> | |
| <button onclick="saveText('cipherOutput', 'ciphertext.txt')">Save as .txt</button> | |
| </div> | |
| <p class="note"> | |
| Substitution cipher with positional shift. The same plaintext letter encodes differently | |
| depending on its position — defeats simple frequency analysis. | |
| </p> | |
| </div> | |
| <!-- DECODE --> | |
| <div class="panel" id="panel-decode"> | |
| <div class="sec"> | |
| <label>Active cipher key <span class="badge" id="dec-key-badge">none</span></label> | |
| <div class="mono-box" id="dec-key-display" style="font-size:11px">No key loaded.</div> | |
| </div> | |
| <div class="sec"> | |
| <label>Ciphertext input</label> | |
| <textarea id="cipherInput" placeholder="Paste encoded text here..."></textarea> | |
| </div> | |
| <div class="row"> | |
| <button class="primary" onclick="doDecode()">Decode →</button> | |
| <button onclick="document.getElementById('cipherInput').value='';document.getElementById('plainOutput').textContent=''">Clear</button> | |
| </div> | |
| <label>Plaintext output</label> | |
| <div class="mono-box" id="plainOutput"></div> | |
| <div class="output-actions"> | |
| <button onclick="copyTextFrom('plainOutput', this)">Copy plaintext</button> | |
| <button onclick="saveText('plainOutput', 'plaintext.txt')">Save as .txt</button> | |
| </div> | |
| </div> | |
| <!-- KEYS --> | |
| <div class="panel" id="panel-keys"> | |
| <div class="sec"> | |
| <label>Active substitution key</label> | |
| <div class="mono-box" id="keyDisplay" style="font-size:11px">No key loaded.</div> | |
| <div class="output-actions"> | |
| <button onclick="copyTextFrom('keyDisplay', this)">Copy key</button> | |
| <button onclick="saveKeyFile()">Save key as .txt</button> | |
| </div> | |
| </div> | |
| <hr class="divider"> | |
| <div class="sec"> | |
| <label>Generate random key (independent of OTP)</label> | |
| <div class="row"> | |
| <button onclick="genRandKey()">Generate random key</button> | |
| </div> | |
| </div> | |
| <hr class="divider"> | |
| <div class="sec"> | |
| <label>Import key — 26 unique A–Z letters</label> | |
| <div class="row"> | |
| <div class="col" style="flex:2"> | |
| <input type="text" id="importField" maxlength="26" | |
| placeholder="QWERTYUIOPASDFGHJKLZXCVBNM" | |
| oninput="this.value=this.value.toUpperCase()"> | |
| </div> | |
| <button onclick="importKey()">Import</button> | |
| </div> | |
| <div id="importErr" class="status"></div> | |
| </div> | |
| <hr class="divider"> | |
| <p class="note"> | |
| <strong>How it works:</strong> Each plaintext letter is first substituted via the key mapping, | |
| then shifted by its position index mod 26. Identical letters encode differently throughout | |
| the text, making LLM pattern-matching and frequency analysis ineffective without the key. | |
| </p> | |
| </div> | |
| <!-- SAVE ALL BAR --> | |
| <div class="save-all-bar"> | |
| <span class="save-all-label">Save bundle</span> | |
| <button onclick="saveBundle()">Save all as .txt</button> | |
| <button onclick="copyBundle(this)">Copy all to clipboard</button> | |
| </div> | |
| </div> | |
| <div class="toast" id="toast"></div> | |
| <script> | |
| // ── L'Ecuyer two-sequence PRNG with Bays-Durham shuffle ────────────────── | |
| var rng = (function () { | |
| var m1 = 2147483563, m2 = 2147483399; | |
| var a1 = 40014, a2 = 40692; | |
| var q1 = 53668, q2 = 52774; | |
| var r1 = 12211, r2 = 3791; | |
| var TABLE_SIZE = 32; | |
| var s1, s2, y, table; | |
| function init(seed) { | |
| seed = Math.abs(Math.floor(seed)) % 2147483647 || 1; | |
| s1 = seed; s2 = seed; | |
| table = new Array(TABLE_SIZE); | |
| for (var i = TABLE_SIZE + 7; i >= 0; i--) { | |
| var k = Math.floor(s1 / q1); | |
| s1 = a1 * (s1 - k * q1) - r1 * k; | |
| if (s1 < 0) s1 += m1; | |
| if (i < TABLE_SIZE) table[i] = s1; | |
| } | |
| y = table[0]; | |
| } | |
| function rand() { | |
| var k = Math.floor(s1 / q1); | |
| s1 = a1 * (s1 - k * q1) - r1 * k; | |
| if (s1 < 0) s1 += m1; | |
| k = Math.floor(s2 / q2); | |
| s2 = a2 * (s2 - k * q2) - r2 * k; | |
| if (s2 < 0) s2 += m2; | |
| var j = Math.floor(y / (1 + (m1 - 1) / TABLE_SIZE)); | |
| y = table[j] - s2; | |
| table[j] = s1; | |
| if (y < 1) y += m1 - 1; | |
| return y / (m1 - 1); | |
| } | |
| function seedFromStr(s) { | |
| if (/^\d+$/.test(s)) return parseInt(s, 10) % 2147483647; | |
| var h = 0; | |
| for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) & 0x7fffffff; | |
| return h || 1; | |
| } | |
| return { init: init, rand: rand, seedFromStr: seedFromStr }; | |
| })(); | |
| // ── constants ───────────────────────────────────────────────────────────── | |
| var AZ = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; | |
| var VOWELS = 'aeiou'; | |
| var CONSONANTS = 'bcdfghjklmnpqrstvwxyz'; | |
| var GIBBERISH = 'abcdefghijklmnopqrstuvwxyz0123456789!@#%^&*_+=~|;:,.?'; | |
| var cipherKey = null; | |
| // ── toast notification ──────────────────────────────────────────────────── | |
| function showToast(msg) { | |
| var t = document.getElementById('toast'); | |
| t.textContent = msg; | |
| t.classList.add('show'); | |
| setTimeout(function () { t.classList.remove('show'); }, 1800); | |
| } | |
| // ── key management ──────────────────────────────────────────────────────── | |
| function updateKeyDisplays() { | |
| if (!cipherKey) return; | |
| var line = 'Plain: ' + AZ + '\nCipher: ' + cipherKey; | |
| document.getElementById('keyDisplay').textContent = line; | |
| document.getElementById('enc-key-display').textContent = line; | |
| document.getElementById('dec-key-display').textContent = line; | |
| document.getElementById('enc-key-badge').textContent = cipherKey.slice(0, 6) + '...'; | |
| document.getElementById('dec-key-badge').textContent = cipherKey.slice(0, 6) + '...'; | |
| } | |
| function genRandKey() { | |
| var az = AZ.split(''); | |
| for (var i = az.length - 1; i > 0; i--) { | |
| var j = Math.floor(Math.random() * (i + 1)); | |
| var t = az[i]; az[i] = az[j]; az[j] = t; | |
| } | |
| cipherKey = az.join(''); | |
| updateKeyDisplays(); | |
| } | |
| function importKey() { | |
| var raw = document.getElementById('importField').value.trim().toUpperCase(); | |
| var err = document.getElementById('importErr'); | |
| if (raw.length !== 26) { err.textContent = 'Must be exactly 26 characters.'; return; } | |
| var s = new Set(raw.split('')); | |
| if (s.size !== 26) { err.textContent = 'All 26 letters must appear exactly once.'; return; } | |
| for (var i = 0; i < raw.length; i++) { | |
| if (AZ.indexOf(raw[i]) < 0) { err.textContent = 'Only A-Z letters allowed.'; return; } | |
| } | |
| err.textContent = ''; | |
| cipherKey = raw; | |
| updateKeyDisplays(); | |
| showToast('Key imported'); | |
| } | |
| // ── OTP generation ──────────────────────────────────────────────────────── | |
| function applyCase(ch, mode) { | |
| if (mode === 'upper') return ch.toUpperCase(); | |
| if (mode === 'lower') return ch.toLowerCase(); | |
| return rng.rand() < 0.5 ? ch.toUpperCase() : ch.toLowerCase(); | |
| } | |
| function genChar(comp, cs) { | |
| var r = rng.rand(); | |
| if (comp === 'numeric') return String(Math.floor(r * 10)); | |
| if (comp === 'alpha') return applyCase(AZ[Math.floor(r * 26)], cs); | |
| if (comp === 'wordlike') { | |
| var pool = rng.rand() < 0.4 ? VOWELS : CONSONANTS; | |
| return applyCase(pool[Math.floor(rng.rand() * pool.length)], cs); | |
| } | |
| var c = GIBBERISH[Math.floor(rng.rand() * GIBBERISH.length)]; | |
| return /[a-z]/.test(c) ? applyCase(c, cs) : c; | |
| } | |
| function generatePad() { | |
| var n = Math.max(1, Math.min(500, parseInt(document.getElementById('numKeys').value) || 10)); | |
| var kl = Math.max(1, Math.min(200, parseInt(document.getElementById('keyLen').value) || 20)); | |
| var gl = Math.max(0, parseInt(document.getElementById('grpLen').value) || 0); | |
| var comp = document.getElementById('composition').value; | |
| var cs = document.getElementById('letterCase').value; | |
| var seedMode = document.getElementById('seedMode').value; | |
| var seed; | |
| if (seedMode === 'user') { | |
| var sv = document.getElementById('userSeed').value.trim(); | |
| seed = sv ? rng.seedFromStr(sv) : Date.now() % 2147483647; | |
| } else { | |
| seed = Date.now() % 2147483647; | |
| } | |
| rng.init(seed); | |
| var lines = []; | |
| for (var i = 0; i < n; i++) { | |
| var chars = []; | |
| for (var j = 0; j < kl; j++) chars.push(genChar(comp, cs)); | |
| var key; | |
| if (gl > 0) { | |
| var groups = []; | |
| for (var k = 0; k < chars.length; k += gl) groups.push(chars.slice(k, k + gl).join('')); | |
| key = groups.join('-'); | |
| } else { | |
| key = chars.join(''); | |
| } | |
| lines.push(key); | |
| } | |
| document.getElementById('padOutput').textContent = lines.join('\n'); | |
| } | |
| function loadPadAsCipherKey() { | |
| var raw = document.getElementById('padOutput').textContent; | |
| if (!raw || raw === 'Press "Generate pad" to create keys.') { alert('Generate a pad first.'); return; } | |
| var letters = raw.replace(/[^a-zA-Z]/g, '').toUpperCase(); | |
| var seen = new Set(); | |
| var key = ''; | |
| for (var i = 0; i < letters.length && key.length < 26; i++) { | |
| if (!seen.has(letters[i])) { seen.add(letters[i]); key += letters[i]; } | |
| } | |
| if (key.length < 26) { | |
| alert('Pad does not contain enough unique letters (got ' + key.length + '/26). Try a larger pad.'); | |
| return; | |
| } | |
| cipherKey = key; | |
| updateKeyDisplays(); | |
| switchTab('encode', document.querySelectorAll('.tab')[1]); | |
| showToast('Key loaded from pad'); | |
| } | |
| // ── cipher ──────────────────────────────────────────────────────────────── | |
| function encodeChar(ch, pos) { | |
| var up = ch.toUpperCase(), idx = AZ.indexOf(up); | |
| if (idx < 0) return ch; | |
| var mapped = AZ.indexOf(cipherKey[idx]); | |
| var shifted = (mapped + (pos % 26)) % 26; | |
| var enc = AZ[shifted]; | |
| return ch === up ? enc : enc.toLowerCase(); | |
| } | |
| function decodeChar(ch, pos) { | |
| var up = ch.toUpperCase(), idx = AZ.indexOf(up); | |
| if (idx < 0) return ch; | |
| var unshifted = ((idx - (pos % 26)) + 26) % 26; | |
| var dec = AZ[cipherKey.indexOf(AZ[unshifted])]; | |
| return ch === up ? dec : dec.toLowerCase(); | |
| } | |
| function applyTransform(text, fn) { | |
| var pos = 0; | |
| return text.split('').map(function (c) { | |
| var out = fn(c, pos); | |
| if (AZ.indexOf(c.toUpperCase()) >= 0) pos++; | |
| return out; | |
| }).join(''); | |
| } | |
| function doEncode() { | |
| if (!cipherKey) { alert('No key loaded.'); return; } | |
| document.getElementById('cipherOutput').textContent = | |
| applyTransform(document.getElementById('plainInput').value, encodeChar); | |
| showToast('Encoded'); | |
| } | |
| function doDecode() { | |
| if (!cipherKey) { alert('No key loaded.'); return; } | |
| document.getElementById('plainOutput').textContent = | |
| applyTransform(document.getElementById('cipherInput').value, decodeChar); | |
| showToast('Decoded'); | |
| } | |
| // ── save / copy helpers ─────────────────────────────────────────────────── | |
| function getTextFrom(id) { | |
| return document.getElementById(id).textContent || ''; | |
| } | |
| function flashBtn(btn, label) { | |
| var orig = btn.textContent; | |
| btn.textContent = label; | |
| btn.classList.add('success'); | |
| setTimeout(function () { | |
| btn.textContent = orig; | |
| btn.classList.remove('success'); | |
| }, 1400); | |
| } | |
| function copyTextFrom(id, btn) { | |
| var t = getTextFrom(id); | |
| if (!t) { showToast('Nothing to copy'); return; } | |
| navigator.clipboard.writeText(t).then(function () { | |
| if (btn) flashBtn(btn, 'Copied!'); | |
| showToast('Copied to clipboard'); | |
| }).catch(function () { | |
| var ta = document.createElement('textarea'); | |
| ta.value = t; | |
| document.body.appendChild(ta); | |
| ta.select(); | |
| document.execCommand('copy'); | |
| document.body.removeChild(ta); | |
| if (btn) flashBtn(btn, 'Copied!'); | |
| showToast('Copied to clipboard'); | |
| }); | |
| } | |
| function saveText(id, filename) { | |
| var t = getTextFrom(id); | |
| if (!t) { showToast('Nothing to save'); return; } | |
| triggerDownload(t, filename); | |
| showToast('Saved ' + filename); | |
| } | |
| function saveKeyFile() { | |
| if (!cipherKey) { showToast('No key loaded'); return; } | |
| var content = 'OTP Substitution Cipher Key\n'; | |
| content += 'Generated: ' + new Date().toISOString() + '\n\n'; | |
| content += 'Plain: ' + AZ + '\n'; | |
| content += 'Cipher: ' + cipherKey + '\n'; | |
| triggerDownload(content, 'cipher-key.txt'); | |
| showToast('Saved cipher-key.txt'); | |
| } | |
| function bundleContent() { | |
| var ts = new Date().toISOString(); | |
| var pad = getTextFrom('padOutput'); | |
| var cipher = getTextFrom('cipherOutput'); | |
| var plain = getTextFrom('plainOutput'); | |
| var key = cipherKey ? ('Plain: ' + AZ + '\nCipher: ' + cipherKey) : 'No key loaded'; | |
| var out = ''; | |
| out += '========================================\n'; | |
| out += ' OTP Substitution Cipher — Session Bundle\n'; | |
| out += ' ' + ts + '\n'; | |
| out += '========================================\n\n'; | |
| out += '--- CIPHER KEY ---\n' + key + '\n\n'; | |
| if (pad && pad !== 'Press "Generate pad" to create keys.') { | |
| out += '--- ONE-TIME PAD ---\n' + pad + '\n\n'; | |
| } | |
| if (cipher) { | |
| out += '--- CIPHERTEXT ---\n' + cipher + '\n\n'; | |
| } | |
| if (plain) { | |
| out += '--- PLAINTEXT ---\n' + plain + '\n\n'; | |
| } | |
| return out; | |
| } | |
| function saveBundle() { | |
| var content = bundleContent(); | |
| triggerDownload(content, 'otp-cipher-bundle.txt'); | |
| showToast('Saved otp-cipher-bundle.txt'); | |
| } | |
| function copyBundle(btn) { | |
| var content = bundleContent(); | |
| navigator.clipboard.writeText(content).then(function () { | |
| if (btn) flashBtn(btn, 'Copied!'); | |
| showToast('Bundle copied to clipboard'); | |
| }).catch(function () { | |
| var ta = document.createElement('textarea'); | |
| ta.value = content; | |
| document.body.appendChild(ta); | |
| ta.select(); | |
| document.execCommand('copy'); | |
| document.body.removeChild(ta); | |
| if (btn) flashBtn(btn, 'Copied!'); | |
| showToast('Bundle copied to clipboard'); | |
| }); | |
| } | |
| function triggerDownload(text, filename) { | |
| var blob = new Blob([text], { type: 'text/plain;charset=utf-8' }); | |
| var url = URL.createObjectURL(blob); | |
| var a = document.createElement('a'); | |
| a.href = url; | |
| a.download = filename; | |
| document.body.appendChild(a); | |
| a.click(); | |
| setTimeout(function () { | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| }, 100); | |
| } | |
| // ── navigation ──────────────────────────────────────────────────────────── | |
| function switchTab(name, el) { | |
| document.querySelectorAll('.tab').forEach(function (t) { t.classList.remove('active'); }); | |
| document.querySelectorAll('.panel').forEach(function (p) { p.classList.remove('active'); }); | |
| if (el) el.classList.add('active'); | |
| document.getElementById('panel-' + name).classList.add('active'); | |
| } | |
| function toggleSeed() { | |
| var m = document.getElementById('seedMode').value; | |
| document.getElementById('userSeedRow').style.display = m === 'user' ? 'flex' : 'none'; | |
| } | |
| genRandKey(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment