Skip to content

Instantly share code, notes, and snippets.

@maanimis
Created March 4, 2026 16:51
Show Gist options
  • Select an option

  • Save maanimis/4c8656383817ce448aebd208c665772a to your computer and use it in GitHub Desktop.

Select an option

Save maanimis/4c8656383817ce448aebd208c665772a to your computer and use it in GitHub Desktop.
PGP key generation message signing + verify tools
<!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&#10;My name is John&#10;&#10;Type your message here…"
></textarea>
<label>Private Key</label>
<textarea
id="privKeyIn"
rows="5"
placeholder="-----BEGIN PGP PRIVATE KEY BLOCK-----&#10;...&#10;(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-----&#10;Hash: SHA512&#10;&#10;Hello everyone&#10;My name is John&#10;&#10;-----BEGIN PGP SIGNATURE-----&#10;...&#10;-----END PGP SIGNATURE-----"
></textarea>
<label>Public Key of the Signer</label>
<textarea
id="vPubIn"
rows="5"
placeholder="-----BEGIN PGP PUBLIC KEY BLOCK-----&#10;...&#10;(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