Created
March 4, 2026 16:51
-
-
Save maanimis/4c8656383817ce448aebd208c665772a to your computer and use it in GitHub Desktop.
PGP key generation message signing + verify tools
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>PGP Vault</title> | |
| <!-- <script src="./openpgp.min.js"></script> --> | |
| <script src="https://unpkg.com/openpgp@6.3.0/dist/openpgp.min.js"></script> | |
| <style> | |
| /* ── System-safe font stack — no external requests ── */ | |
| :root { | |
| --font-mono: "Courier New", Courier, monospace; | |
| --font-display: "Trebuchet MS", "Lucida Grande", sans-serif; | |
| --bg: #07080f; | |
| --surface: #0d0e1a; | |
| --panel: #10111f; | |
| --border: #1c1d30; | |
| --border2: #252640; | |
| --g: #00e87a; | |
| --b: #5c8aff; | |
| --v: #b06fff; | |
| --r: #ff5566; | |
| --y: #ffc040; | |
| --text: #d8daf8; | |
| --muted: #4a4c70; | |
| --dim: #2a2b48; | |
| --nav-h: 60px; | |
| --rad: 8px; | |
| } | |
| *, | |
| *::before, | |
| *::after { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| html { | |
| scroll-behavior: smooth; | |
| } | |
| body { | |
| background: var(--bg); | |
| color: var(--text); | |
| font-family: var(--font-mono); | |
| font-size: 13px; | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| } | |
| body::before { | |
| content: ""; | |
| position: fixed; | |
| inset: 0; | |
| pointer-events: none; | |
| z-index: 0; | |
| background: | |
| radial-gradient( | |
| ellipse 70% 50% at 15% 0%, | |
| rgba(0, 232, 122, 0.07) 0%, | |
| transparent 55% | |
| ), | |
| radial-gradient( | |
| ellipse 60% 45% at 85% 100%, | |
| rgba(92, 138, 255, 0.07) 0%, | |
| transparent 55% | |
| ), | |
| radial-gradient( | |
| ellipse 40% 30% at 50% 50%, | |
| rgba(176, 111, 255, 0.04) 0%, | |
| transparent 60% | |
| ); | |
| } | |
| /* ─────────────────── NAV ─────────────────── */ | |
| nav { | |
| position: sticky; | |
| top: 0; | |
| z-index: 100; | |
| height: var(--nav-h); | |
| background: rgba(7, 8, 15, 0.92); | |
| backdrop-filter: blur(18px); | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| align-items: center; | |
| padding: 0 28px; | |
| gap: 0; | |
| } | |
| .nav-logo { | |
| display: flex; | |
| align-items: center; | |
| gap: 11px; | |
| margin-right: 32px; | |
| text-decoration: none; | |
| flex-shrink: 0; | |
| } | |
| .nav-hex { | |
| width: 30px; | |
| height: 30px; | |
| background: linear-gradient(135deg, var(--g), var(--b)); | |
| clip-path: polygon( | |
| 50% 0%, | |
| 100% 25%, | |
| 100% 75%, | |
| 50% 100%, | |
| 0% 75%, | |
| 0% 25% | |
| ); | |
| animation: hexPulse 3s ease-in-out infinite; | |
| } | |
| @keyframes hexPulse { | |
| 0%, | |
| 100% { | |
| opacity: 1; | |
| } | |
| 50% { | |
| opacity: 0.5; | |
| } | |
| } | |
| .nav-wordmark { | |
| font-family: var(--font-display); | |
| font-size: 17px; | |
| font-weight: 700; | |
| letter-spacing: 0.1em; | |
| text-transform: uppercase; | |
| background: linear-gradient(90deg, var(--g), var(--b)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .nav-tabs { | |
| display: flex; | |
| align-items: stretch; | |
| height: 100%; | |
| flex: 1; | |
| } | |
| .nav-tab { | |
| display: flex; | |
| align-items: center; | |
| gap: 9px; | |
| padding: 0 22px; | |
| height: 100%; | |
| cursor: pointer; | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| letter-spacing: 0.14em; | |
| text-transform: uppercase; | |
| color: var(--muted); | |
| border: none; | |
| background: none; | |
| border-bottom: 2px solid transparent; | |
| transition: | |
| color 0.18s, | |
| border-color 0.18s, | |
| background 0.18s; | |
| white-space: nowrap; | |
| } | |
| .nav-tab .t-icon { | |
| font-size: 14px; | |
| } | |
| .nav-tab .t-num { | |
| width: 18px; | |
| height: 18px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 9px; | |
| font-weight: 700; | |
| background: var(--dim); | |
| color: var(--muted); | |
| transition: | |
| background 0.18s, | |
| color 0.18s; | |
| } | |
| .nav-tab:hover { | |
| color: var(--text); | |
| background: rgba(255, 255, 255, 0.03); | |
| } | |
| .nav-tab[data-tab="keygen"].active { | |
| border-bottom-color: var(--g); | |
| color: var(--text); | |
| } | |
| .nav-tab[data-tab="keygen"].active .t-num { | |
| background: var(--g); | |
| color: var(--bg); | |
| } | |
| .nav-tab[data-tab="sign"].active { | |
| border-bottom-color: var(--b); | |
| color: var(--text); | |
| } | |
| .nav-tab[data-tab="sign"].active .t-num { | |
| background: var(--b); | |
| color: #fff; | |
| } | |
| .nav-tab[data-tab="verify"].active { | |
| border-bottom-color: var(--v); | |
| color: var(--text); | |
| } | |
| .nav-tab[data-tab="verify"].active .t-num { | |
| background: var(--v); | |
| color: #fff; | |
| } | |
| .nav-badge { | |
| margin-left: auto; | |
| flex-shrink: 0; | |
| font-size: 9px; | |
| letter-spacing: 0.12em; | |
| text-transform: uppercase; | |
| color: var(--muted); | |
| padding: 4px 12px; | |
| border: 1px solid var(--border); | |
| border-radius: 20px; | |
| } | |
| /* ─────────────────── PAGES ─────────────────── */ | |
| .page { | |
| display: none; | |
| position: relative; | |
| z-index: 1; | |
| animation: fadeIn 0.25s ease; | |
| min-height: calc(100vh - var(--nav-h)); | |
| } | |
| .page.active { | |
| display: block; | |
| } | |
| @keyframes fadeIn { | |
| from { | |
| opacity: 0; | |
| transform: translateY(8px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| /* Page 1 two-col layout */ | |
| #page-keygen { | |
| display: none; | |
| } | |
| #page-keygen.active { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| } | |
| .kg-form { | |
| padding: 36px 40px; | |
| border-right: 1px solid var(--border); | |
| } | |
| .kg-results { | |
| padding: 36px 40px; | |
| } | |
| /* Pages 2 & 3 centred */ | |
| #page-sign, | |
| #page-verify { | |
| max-width: 840px; | |
| margin: 0 auto; | |
| padding: 40px 36px; | |
| } | |
| /* ─────────────────── FORM ELEMENTS ─────────────────── */ | |
| .sec-title { | |
| font-family: var(--font-display); | |
| font-size: 12px; | |
| font-weight: 700; | |
| letter-spacing: 0.22em; | |
| text-transform: uppercase; | |
| margin-bottom: 28px; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .sec-title::after { | |
| content: ""; | |
| flex: 1; | |
| height: 1px; | |
| background: linear-gradient(90deg, var(--border2), transparent); | |
| margin-left: 10px; | |
| } | |
| .c-g { | |
| color: var(--g); | |
| } | |
| .c-b { | |
| color: var(--b); | |
| } | |
| .c-v { | |
| color: var(--v); | |
| } | |
| label { | |
| display: block; | |
| font-size: 9px; | |
| letter-spacing: 0.18em; | |
| text-transform: uppercase; | |
| color: var(--muted); | |
| margin-bottom: 7px; | |
| margin-top: 18px; | |
| } | |
| label:first-of-type { | |
| margin-top: 0; | |
| } | |
| input[type="text"], | |
| input[type="email"], | |
| input[type="password"], | |
| textarea, | |
| select { | |
| width: 100%; | |
| background: var(--panel); | |
| border: 1px solid var(--border2); | |
| border-radius: var(--rad); | |
| color: var(--text); | |
| font-family: var(--font-mono); | |
| font-size: 12px; | |
| padding: 10px 14px; | |
| outline: none; | |
| transition: | |
| border-color 0.2s, | |
| box-shadow 0.2s; | |
| line-height: 1.55; | |
| } | |
| input:focus, | |
| textarea:focus { | |
| border-color: var(--b); | |
| box-shadow: 0 0 0 3px rgba(92, 138, 255, 0.12); | |
| } | |
| textarea { | |
| resize: vertical; | |
| min-height: 110px; | |
| } | |
| .row2 { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 14px; | |
| } | |
| select { | |
| appearance: none; | |
| cursor: pointer; | |
| background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%234a4c70'/%3E%3C/svg%3E"); | |
| background-repeat: no-repeat; | |
| background-position: right 13px center; | |
| } | |
| /* ─────────────────── BUTTONS ─────────────────── */ | |
| .btn { | |
| margin-top: 22px; | |
| width: 100%; | |
| padding: 13px 20px; | |
| border: none; | |
| border-radius: var(--rad); | |
| font-family: var(--font-display); | |
| font-size: 11px; | |
| font-weight: 700; | |
| letter-spacing: 0.16em; | |
| text-transform: uppercase; | |
| cursor: pointer; | |
| transition: | |
| opacity 0.18s, | |
| transform 0.12s; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .btn:active:not(:disabled) { | |
| transform: scale(0.98); | |
| } | |
| .btn:disabled { | |
| opacity: 0.38; | |
| cursor: not-allowed; | |
| } | |
| .btn::after { | |
| content: ""; | |
| position: absolute; | |
| inset: 0; | |
| background: rgba(255, 255, 255, 0.12); | |
| opacity: 0; | |
| transition: opacity 0.18s; | |
| } | |
| .btn:hover:not(:disabled)::after { | |
| opacity: 1; | |
| } | |
| .btn-g { | |
| background: linear-gradient(135deg, #00c468, var(--g)); | |
| color: var(--bg); | |
| box-shadow: 0 4px 22px rgba(0, 232, 122, 0.22); | |
| } | |
| .btn-b { | |
| background: linear-gradient(135deg, #3d6af0, var(--b)); | |
| color: #fff; | |
| box-shadow: 0 4px 22px rgba(92, 138, 255, 0.22); | |
| } | |
| .btn-v { | |
| background: linear-gradient(135deg, #8b52f5, var(--v)); | |
| color: #fff; | |
| box-shadow: 0 4px 22px rgba(176, 111, 255, 0.22); | |
| } | |
| /* ─────────────────── STATUS BAR ─────────────────── */ | |
| .status-bar { | |
| display: flex; | |
| align-items: center; | |
| gap: 9px; | |
| margin-top: 14px; | |
| min-height: 22px; | |
| font-size: 10px; | |
| letter-spacing: 0.08em; | |
| color: var(--muted); | |
| } | |
| .sdot { | |
| width: 7px; | |
| height: 7px; | |
| border-radius: 50%; | |
| background: var(--muted); | |
| flex-shrink: 0; | |
| } | |
| .sdot.ok { | |
| background: var(--g); | |
| } | |
| .sdot.err { | |
| background: var(--r); | |
| } | |
| .sdot.info { | |
| background: var(--b); | |
| } | |
| .sdot.warn { | |
| background: var(--y); | |
| } | |
| @keyframes blink2 { | |
| 0%, | |
| 100% { | |
| opacity: 1; | |
| } | |
| 50% { | |
| opacity: 0.15; | |
| } | |
| } | |
| .sdot.spin { | |
| animation: blink2 0.7s infinite; | |
| background: var(--b); | |
| } | |
| .sp-ring { | |
| display: none; | |
| width: 15px; | |
| height: 15px; | |
| border: 2px solid rgba(255, 255, 255, 0.08); | |
| border-top-color: var(--b); | |
| border-radius: 50%; | |
| animation: spin 0.65s linear infinite; | |
| flex-shrink: 0; | |
| } | |
| @keyframes spin { | |
| to { | |
| transform: rotate(360deg); | |
| } | |
| } | |
| .sp-ring.show { | |
| display: block; | |
| } | |
| /* ─────────────────── OUTPUT BLOCKS ─────────────────── */ | |
| .out-blk { | |
| margin-top: 20px; | |
| background: var(--panel); | |
| border: 1px solid var(--border2); | |
| border-radius: var(--rad); | |
| overflow: hidden; | |
| } | |
| .out-hd { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 8px 14px; | |
| border-bottom: 1px solid var(--border); | |
| background: rgba(255, 255, 255, 0.018); | |
| } | |
| .out-lbl { | |
| font-size: 9px; | |
| letter-spacing: 0.18em; | |
| text-transform: uppercase; | |
| color: var(--muted); | |
| } | |
| .cp-btn { | |
| font-size: 9px; | |
| letter-spacing: 0.12em; | |
| text-transform: uppercase; | |
| color: var(--muted); | |
| background: none; | |
| border: 1px solid var(--border2); | |
| border-radius: 4px; | |
| padding: 3px 11px; | |
| cursor: pointer; | |
| font-family: var(--font-mono); | |
| transition: | |
| color 0.15s, | |
| border-color 0.15s; | |
| } | |
| .cp-btn:hover { | |
| color: var(--text); | |
| border-color: var(--muted); | |
| } | |
| .cp-btn.copied { | |
| color: var(--g); | |
| border-color: var(--g); | |
| } | |
| .out-con { | |
| padding: 14px; | |
| font-size: 11px; | |
| line-height: 1.75; | |
| color: var(--muted); | |
| word-break: break-all; | |
| white-space: pre-wrap; | |
| max-height: 220px; | |
| overflow-y: auto; | |
| } | |
| .out-con.tall { | |
| max-height: 360px; | |
| } | |
| .out-con.filled { | |
| color: var(--text); | |
| } | |
| .out-con::-webkit-scrollbar { | |
| width: 4px; | |
| } | |
| .out-con::-webkit-scrollbar-thumb { | |
| background: var(--border); | |
| border-radius: 2px; | |
| } | |
| .ph { | |
| color: var(--muted); | |
| font-style: italic; | |
| } | |
| /* ─────────────────── KEY CHIP / FP ─────────────────── */ | |
| .key-chip { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 7px; | |
| padding: 5px 12px; | |
| background: rgba(0, 232, 122, 0.07); | |
| border: 1px solid rgba(0, 232, 122, 0.18); | |
| border-radius: 20px; | |
| font-size: 9px; | |
| letter-spacing: 0.13em; | |
| text-transform: uppercase; | |
| color: var(--g); | |
| margin-top: 10px; | |
| } | |
| .fp { | |
| font-size: 10px; | |
| color: var(--muted); | |
| margin-top: 7px; | |
| word-break: break-all; | |
| } | |
| .fp em { | |
| color: var(--b); | |
| font-style: normal; | |
| } | |
| /* ─────────────────── VERIFY RESULT CARD ─────────────────── */ | |
| .vcard { | |
| margin-top: 24px; | |
| border-radius: var(--rad); | |
| padding: 22px 24px; | |
| border: 1px solid var(--border2); | |
| display: none; | |
| animation: fadeIn 0.22s ease; | |
| } | |
| .vcard.show { | |
| display: block; | |
| } | |
| .vcard.valid { | |
| border-color: rgba(0, 232, 122, 0.4); | |
| background: rgba(0, 232, 122, 0.06); | |
| } | |
| .vcard.bad { | |
| border-color: rgba(255, 85, 102, 0.4); | |
| background: rgba(255, 85, 102, 0.06); | |
| } | |
| .vcard.warn { | |
| border-color: rgba(255, 192, 64, 0.4); | |
| background: rgba(255, 192, 64, 0.06); | |
| } | |
| .vc-head { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| margin-bottom: 16px; | |
| } | |
| .vc-icon { | |
| font-size: 26px; | |
| } | |
| .vc-title { | |
| font-family: var(--font-display); | |
| font-size: 16px; | |
| font-weight: 700; | |
| } | |
| .vcard.valid .vc-title { | |
| color: var(--g); | |
| } | |
| .vcard.bad .vc-title { | |
| color: var(--r); | |
| } | |
| .vcard.warn .vc-title { | |
| color: var(--y); | |
| } | |
| .vc-row { | |
| display: flex; | |
| gap: 10px; | |
| margin-top: 9px; | |
| font-size: 11px; | |
| } | |
| .vc-k { | |
| color: var(--muted); | |
| min-width: 100px; | |
| flex-shrink: 0; | |
| } | |
| .vc-v { | |
| color: var(--text); | |
| word-break: break-all; | |
| white-space: pre-wrap; | |
| } | |
| /* ─────────────────── RESPONSIVE ─────────────────── */ | |
| @media (max-width: 820px) { | |
| #page-keygen.active { | |
| grid-template-columns: 1fr; | |
| } | |
| .kg-form { | |
| border-right: none; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| #page-sign, | |
| #page-verify { | |
| padding: 28px 18px; | |
| } | |
| nav { | |
| padding: 0 14px; | |
| } | |
| .nav-tab { | |
| padding: 0 13px; | |
| font-size: 10px; | |
| } | |
| .nav-badge { | |
| display: none; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- ══════════════ NAVIGATION ══════════════ --> | |
| <nav> | |
| <a class="nav-logo" href="#"> | |
| <div class="nav-hex"></div> | |
| <span class="nav-wordmark">PGP Vault</span> | |
| </a> | |
| <div class="nav-tabs"> | |
| <button | |
| class="nav-tab active" | |
| data-tab="keygen" | |
| onclick="switchTab('keygen')" | |
| > | |
| <span class="t-icon">⬡</span> | |
| <span class="t-num">1</span> | |
| Key Generation | |
| </button> | |
| <button class="nav-tab" data-tab="sign" onclick="switchTab('sign')"> | |
| <span class="t-icon">✦</span> | |
| <span class="t-num">2</span> | |
| Sign Message | |
| </button> | |
| <button class="nav-tab" data-tab="verify" onclick="switchTab('verify')"> | |
| <span class="t-icon">◈</span> | |
| <span class="t-num">3</span> | |
| Verify Signature | |
| </button> | |
| </div> | |
| <div class="nav-badge">Local · No Network</div> | |
| </nav> | |
| <!-- ══════════════ PAGE 1 · KEY GENERATION ══════════════ --> | |
| <div id="page-keygen" class="page active"> | |
| <div class="kg-form"> | |
| <div class="sec-title"> | |
| <span class="c-g">⬡</span> Generate Key Pair | |
| </div> | |
| <div class="row2"> | |
| <div> | |
| <label>Name</label> | |
| <input | |
| type="text" | |
| id="keyName" | |
| placeholder="John Doe" | |
| value="John Doe" | |
| /> | |
| </div> | |
| <div> | |
| <label>Email</label> | |
| <input | |
| type="email" | |
| id="keyEmail" | |
| placeholder="john@example.com" | |
| value="john@example.com" | |
| /> | |
| </div> | |
| </div> | |
| <label | |
| >Passphrase | |
| <small | |
| style=" | |
| color: var(--muted); | |
| font-size: 9px; | |
| text-transform: none; | |
| letter-spacing: 0; | |
| " | |
| >(optional)</small | |
| ></label | |
| > | |
| <input | |
| type="password" | |
| id="passphrase" | |
| placeholder="Leave empty for no passphrase" | |
| /> | |
| <div class="row2"> | |
| <div> | |
| <label>Key Type</label> | |
| <select id="keyType"> | |
| <option value="ecc">ECC · Ed25519</option> | |
| <option value="rsa2048">RSA 2048</option> | |
| <option value="rsa4096">RSA 4096</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label>Expiry</label> | |
| <select id="expiry"> | |
| <option value="0">Never</option> | |
| <option value="365">1 Year</option> | |
| <option value="730">2 Years</option> | |
| </select> | |
| </div> | |
| </div> | |
| <button class="btn btn-g" id="generateBtn" onclick="generateKeys()"> | |
| ⬡ Generate Key Pair | |
| </button> | |
| <div class="status-bar"> | |
| <div class="sp-ring" id="genSp"></div> | |
| <div class="sdot" id="genDot"></div> | |
| <span id="genMsg">Ready to generate</span> | |
| </div> | |
| <div id="fpRow" style="display: none; margin-top: 14px"> | |
| <div class="key-chip"> | |
| <svg | |
| width="10" | |
| height="10" | |
| viewBox="0 0 24 24" | |
| fill="none" | |
| stroke="currentColor" | |
| stroke-width="2.5" | |
| > | |
| <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" /> | |
| </svg> | |
| Key pair active | |
| </div> | |
| <div class="fp">Fingerprint: <em id="fpVal">—</em></div> | |
| </div> | |
| </div> | |
| <div class="kg-results"> | |
| <div class="sec-title"><span class="c-g">⬡</span> Output</div> | |
| <div class="out-blk"> | |
| <div class="out-hd"> | |
| <span class="out-lbl">Public Key</span> | |
| <button class="cp-btn" onclick="cpEl('pubOut', this)">Copy</button> | |
| </div> | |
| <div class="out-con" id="pubOut"> | |
| <span class="ph">Generate a key pair to see the public key…</span> | |
| </div> | |
| </div> | |
| <div class="out-blk"> | |
| <div class="out-hd"> | |
| <span class="out-lbl">Private Key</span> | |
| <button class="cp-btn" onclick="cpEl('privOut', this)">Copy</button> | |
| </div> | |
| <div class="out-con" id="privOut"> | |
| <span class="ph">Generate a key pair to see the private key…</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ══════════════ PAGE 2 · SIGN ══════════════ --> | |
| <div id="page-sign" class="page"> | |
| <div class="sec-title"><span class="c-b">✦</span> Sign a Message</div> | |
| <label>Message to Sign</label> | |
| <textarea | |
| id="msgInput" | |
| rows="7" | |
| placeholder="Hello everyone My name is John Type your message here…" | |
| ></textarea> | |
| <label>Private Key</label> | |
| <textarea | |
| id="privKeyIn" | |
| rows="5" | |
| placeholder="-----BEGIN PGP PRIVATE KEY BLOCK----- ... (auto-filled when you generate a key on tab 1)" | |
| ></textarea> | |
| <label | |
| >Passphrase | |
| <small | |
| style=" | |
| color: var(--muted); | |
| font-size: 9px; | |
| text-transform: none; | |
| letter-spacing: 0; | |
| " | |
| >(if key is protected)</small | |
| ></label | |
| > | |
| <input | |
| type="password" | |
| id="signPass" | |
| placeholder="Leave empty if no passphrase" | |
| /> | |
| <button class="btn btn-b" id="signBtn" onclick="signMessage()"> | |
| ✦ Sign Message | |
| </button> | |
| <div class="status-bar"> | |
| <div class="sp-ring" id="signSp"></div> | |
| <div class="sdot" id="signDot"></div> | |
| <span id="signMsg">Paste a private key and message to sign</span> | |
| </div> | |
| <div class="out-blk"> | |
| <div class="out-hd"> | |
| <span class="out-lbl">Signed Message · PGP Clearsign</span> | |
| <button class="cp-btn" onclick="cpEl('signOut', this)">Copy</button> | |
| </div> | |
| <div class="out-con tall" id="signOut"> | |
| <span class="ph">Signed output will appear here…</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ══════════════ PAGE 3 · VERIFY ══════════════ --> | |
| <div id="page-verify" class="page"> | |
| <div class="sec-title"> | |
| <span class="c-v">◈</span> Verify a Signed Message | |
| </div> | |
| <label | |
| >Signed Message | |
| <small | |
| style=" | |
| color: var(--muted); | |
| font-size: 9px; | |
| text-transform: none; | |
| letter-spacing: 0; | |
| " | |
| >(paste full PGP clearsigned block)</small | |
| ></label | |
| > | |
| <textarea | |
| id="vMsgIn" | |
| rows="10" | |
| placeholder="-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA512 Hello everyone My name is John -----BEGIN PGP SIGNATURE----- ... -----END PGP SIGNATURE-----" | |
| ></textarea> | |
| <label>Public Key of the Signer</label> | |
| <textarea | |
| id="vPubIn" | |
| rows="5" | |
| placeholder="-----BEGIN PGP PUBLIC KEY BLOCK----- ... (auto-filled when you generate a key on tab 1)" | |
| ></textarea> | |
| <button class="btn btn-v" id="verifyBtn" onclick="verifyMessage()"> | |
| ◈ Verify Signature | |
| </button> | |
| <div class="status-bar"> | |
| <div class="sp-ring" id="verifySp"></div> | |
| <div class="sdot" id="verifyDot"></div> | |
| <span id="verifyMsg" | |
| >Paste a signed message and public key to verify</span | |
| > | |
| </div> | |
| <div class="vcard" id="vCard"> | |
| <div class="vc-head"> | |
| <span class="vc-icon" id="vcIcon"></span> | |
| <span class="vc-title" id="vcTitle"></span> | |
| </div> | |
| <div class="vc-row"> | |
| <span class="vc-k">Signed by</span> | |
| <span class="vc-v" id="vcSigner">—</span> | |
| </div> | |
| <div class="vc-row"> | |
| <span class="vc-k">Key ID</span> | |
| <span class="vc-v" id="vcKeyId">—</span> | |
| </div> | |
| <div class="vc-row"> | |
| <span class="vc-k">Created</span> | |
| <span class="vc-v" id="vcCreated">—</span> | |
| </div> | |
| <div class="vc-row"> | |
| <span class="vc-k">Hash Algo</span> | |
| <span class="vc-v" id="vcHash">—</span> | |
| </div> | |
| <div class="vc-row"> | |
| <span class="vc-k">Message</span> | |
| <span class="vc-v" id="vcMessage"></span> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| /* ── Tab switching ── */ | |
| function switchTab(name) { | |
| document | |
| .querySelectorAll(".nav-tab") | |
| .forEach((t) => t.classList.toggle("active", t.dataset.tab === name)); | |
| document | |
| .querySelectorAll(".page") | |
| .forEach((p) => | |
| p.classList.toggle("active", p.id === "page-" + name), | |
| ); | |
| } | |
| /* ── Helpers ── */ | |
| function setSt(pfx, cls, msg) { | |
| document.getElementById(pfx + "Dot").className = "sdot " + (cls || ""); | |
| document.getElementById(pfx + "Msg").textContent = msg; | |
| } | |
| function spin(pfx, on) { | |
| document.getElementById(pfx + "Sp").className = | |
| "sp-ring" + (on ? " show" : ""); | |
| document.getElementById(pfx + "Dot").style.display = on ? "none" : ""; | |
| } | |
| function fill(id, txt) { | |
| const el = document.getElementById(id); | |
| el.textContent = txt; | |
| el.classList.add("filled"); | |
| } | |
| function cpEl(id, btn) { | |
| const el = document.getElementById(id); | |
| if (!el || el.querySelector(".ph")) return; | |
| navigator.clipboard.writeText(el.textContent).then(() => { | |
| btn.textContent = "Copied!"; | |
| btn.classList.add("copied"); | |
| setTimeout(() => { | |
| btn.textContent = "Copy"; | |
| btn.classList.remove("copied"); | |
| }, 1800); | |
| }); | |
| } | |
| /* ── KEY GENERATION ── */ | |
| async function generateKeys() { | |
| const name = | |
| document.getElementById("keyName").value.trim() || "Anonymous"; | |
| const email = | |
| document.getElementById("keyEmail").value.trim() || | |
| "anon@example.com"; | |
| const passphrase = document.getElementById("passphrase").value; | |
| const ktype = document.getElementById("keyType").value; | |
| const expDays = parseInt(document.getElementById("expiry").value); | |
| const btn = document.getElementById("generateBtn"); | |
| btn.disabled = true; | |
| spin("gen", true); | |
| setSt("gen", "spin", "Generating cryptographic key pair…"); | |
| try { | |
| const opts = { | |
| userIDs: [{ name, email }], | |
| passphrase: passphrase || undefined, | |
| }; | |
| if (ktype === "ecc") { | |
| opts.type = "ecc"; | |
| opts.curve = "ed25519"; | |
| } else { | |
| opts.type = "rsa"; | |
| opts.rsaBits = ktype === "rsa4096" ? 4096 : 2048; | |
| } | |
| if (expDays > 0) opts.keyExpirationTime = expDays * 86400; | |
| const { privateKey, publicKey } = await openpgp.generateKey(opts); | |
| fill("pubOut", publicKey); | |
| fill("privOut", privateKey); | |
| // Auto-fill other panels | |
| document.getElementById("privKeyIn").value = privateKey; | |
| document.getElementById("signPass").value = passphrase || ""; | |
| document.getElementById("vPubIn").value = publicKey; | |
| const rk = await openpgp.readKey({ armoredKey: publicKey }); | |
| const fp = rk.getFingerprint().toUpperCase().match(/.{4}/g).join(" "); | |
| document.getElementById("fpVal").textContent = fp; | |
| document.getElementById("fpRow").style.display = "block"; | |
| setSt( | |
| "gen", | |
| "ok", | |
| `Key pair ready · ${ktype.toUpperCase()} · ${name} <${email}>`, | |
| ); | |
| setSt("sign", "info", "Private key loaded — ready to sign"); | |
| setSt("verify", "info", "Public key loaded — ready to verify"); | |
| } catch (e) { | |
| setSt("gen", "err", "Error: " + e.message); | |
| } finally { | |
| spin("gen", false); | |
| btn.disabled = false; | |
| } | |
| } | |
| /* ── SIGN ── */ | |
| async function signMessage() { | |
| const message = document.getElementById("msgInput").value; | |
| const armoredKey = document.getElementById("privKeyIn").value.trim(); | |
| const passphrase = document.getElementById("signPass").value; | |
| const btn = document.getElementById("signBtn"); | |
| if (!message.trim()) { | |
| setSt("sign", "err", "Message is empty"); | |
| return; | |
| } | |
| if (!armoredKey) { | |
| setSt("sign", "err", "No private key provided"); | |
| return; | |
| } | |
| btn.disabled = true; | |
| spin("sign", true); | |
| setSt("sign", "spin", "Signing message…"); | |
| try { | |
| const privKey = passphrase | |
| ? await openpgp.decryptKey({ | |
| privateKey: await openpgp.readPrivateKey({ armoredKey }), | |
| passphrase, | |
| }) | |
| : await openpgp.readPrivateKey({ armoredKey }); | |
| const unsigned = await openpgp.createCleartextMessage({ | |
| text: message, | |
| }); | |
| const signed = await openpgp.sign({ | |
| message: unsigned, | |
| signingKeys: privKey, | |
| }); | |
| fill("signOut", signed); | |
| setSt("sign", "ok", "Message signed successfully"); | |
| } catch (e) { | |
| setSt("sign", "err", "Error: " + e.message); | |
| } finally { | |
| spin("sign", false); | |
| btn.disabled = false; | |
| } | |
| } | |
| /* ── VERIFY ── */ | |
| async function verifyMessage() { | |
| const armoredMsg = document.getElementById("vMsgIn").value.trim(); | |
| const armoredPub = document.getElementById("vPubIn").value.trim(); | |
| const btn = document.getElementById("verifyBtn"); | |
| const card = document.getElementById("vCard"); | |
| if (!armoredMsg) { | |
| setSt("verify", "err", "No signed message provided"); | |
| return; | |
| } | |
| if (!armoredPub) { | |
| setSt("verify", "err", "No public key provided"); | |
| return; | |
| } | |
| btn.disabled = true; | |
| spin("verify", true); | |
| setSt("verify", "spin", "Verifying signature…"); | |
| card.className = "vcard"; | |
| try { | |
| const publicKey = await openpgp.readKey({ armoredKey: armoredPub }); | |
| const message = await openpgp.readCleartextMessage({ | |
| cleartextMessage: armoredMsg, | |
| }); | |
| const result = await openpgp.verify({ | |
| message, | |
| verificationKeys: publicKey, | |
| }); | |
| const sig = result.signatures[0]; | |
| let valid = false; | |
| try { | |
| await sig.verified; | |
| valid = true; | |
| } catch (_) {} | |
| const uid = publicKey.getUserIDs()[0] || "Unknown"; | |
| const keyId = sig.keyID?.toHex?.()?.toUpperCase() || "—"; | |
| const pkt = sig.signature?.packets?.[0]; | |
| const created = pkt?.created | |
| ? new Date(pkt.created).toUTCString() | |
| : "—"; | |
| const hashMap = { | |
| 8: "SHA-256", | |
| 9: "SHA-384", | |
| 10: "SHA-512", | |
| 11: "SHA-224", | |
| }; | |
| const hash = pkt?.hashAlgorithm | |
| ? hashMap[pkt.hashAlgorithm] || "ALG-" + pkt.hashAlgorithm | |
| : "—"; | |
| const msgTxt = result.data || "—"; | |
| if (valid) { | |
| card.className = "vcard show valid"; | |
| document.getElementById("vcIcon").textContent = "✅"; | |
| document.getElementById("vcTitle").textContent = "Signature Valid"; | |
| setSt("verify", "ok", "Signature is valid and trusted"); | |
| } else { | |
| card.className = "vcard show bad"; | |
| document.getElementById("vcIcon").textContent = "❌"; | |
| document.getElementById("vcTitle").textContent = | |
| "Signature Invalid"; | |
| setSt("verify", "err", "Signature could not be verified"); | |
| } | |
| document.getElementById("vcSigner").textContent = uid; | |
| document.getElementById("vcKeyId").textContent = keyId; | |
| document.getElementById("vcCreated").textContent = created; | |
| document.getElementById("vcHash").textContent = hash; | |
| document.getElementById("vcMessage").textContent = msgTxt; | |
| } catch (e) { | |
| card.className = "vcard show warn"; | |
| document.getElementById("vcIcon").textContent = "⚠️"; | |
| document.getElementById("vcTitle").textContent = "Verification Error"; | |
| ["vcSigner", "vcKeyId", "vcCreated", "vcHash"].forEach( | |
| (id) => (document.getElementById(id).textContent = "—"), | |
| ); | |
| document.getElementById("vcMessage").textContent = e.message; | |
| setSt("verify", "warn", "Error: " + e.message); | |
| } finally { | |
| spin("verify", false); | |
| btn.disabled = false; | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment