Compare commits

...
5 Commits
6 changed files with 314 additions and 200 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "post_quantum_nostr",
"version": "0.0.2",
"version": "0.0.7",
"description": "A migration strategy for bringing post-quantum security to Nostr without breaking the social graph, without requiring consensus on a single post-quantum algorithm, and without forcing existing users to abandon their identities.",
"main": "index.js",
"scripts": {
+261 -194
View File
@@ -11,6 +11,14 @@
<link rel="shortcut icon" type="image/x-icon" href="data:image/x-icon;," />
<style>
#divHeaderText {
font-size: calc(max(var(--header-height), var(--header-min-height)) * 0.5) !important;
}
#divHeaderFlexRight {
padding-right: 20px;
}
#divBody {
flex-direction: column !important;
flex-wrap: nowrap !important;
@@ -128,8 +136,9 @@
.pq-warning strong { color: var(--accent-color); }
.pq-seed-display {
background: var(--primary-color);
color: var(--secondary-color);
background: var(--secondary-color);
color: var(--primary-color);
border: var(--border);
border-radius: var(--border-radius);
padding: 20px;
margin: 15px 0;
@@ -210,7 +219,8 @@
.pq-status-error { background: var(--accent-color); color: var(--secondary-color); }
.pq-event-preview {
background: var(--primary-color); color: var(--secondary-color); border-radius: var(--border-radius);
background: var(--secondary-color); color: var(--primary-color); border: var(--border);
border-radius: var(--border-radius);
padding: 15px; font-size: 11px; white-space: pre-wrap; word-break: break-all;
max-height: 400px; overflow-y: auto; text-align: left;
}
@@ -325,10 +335,6 @@
<li>Publish the event to relays</li>
</ol>
</div>
<div class="pq-warning">
<strong>Important:</strong> Your seed phrase is generated and handled entirely in your browser.
It is never sent to any server. Write it down on paper — it's your quantum-safe backup.
</div>
<button class="pq-button" id="pqStartBtn">Start Migration</button>
</div>
@@ -341,27 +347,24 @@
The code is open source and auditable.
</div>
<!-- Toggle: Generate vs Bring your own -->
<!-- Mode toggle + copy row (always visible) -->
<div class="pq-button-row" style="margin-bottom: 15px;">
<button class="pq-button pq-button-secondary pq-seed-toggle pq-seed-toggle-active" id="pqSeedToggleGenerate">Generate New Seed</button>
<button class="pq-button pq-button-secondary pq-seed-toggle" id="pqSeedToggleOwn">I Have a Seed Phrase</button>
<button class="pq-button pq-button-secondary" id="pqCopySeedBtn">Copy</button>
</div>
<!-- Generate mode -->
<div id="pqSeedGeneratePanel">
<div class="pq-seed-display" id="pqSeedDisplay"></div>
<div class="pq-button-row">
<button class="pq-button pq-button-secondary" id="pqCopySeedBtn">Copy</button>
<button class="pq-button pq-button-secondary" id="pqRegenerateBtn">Generate New Seed</button>
</div>
<div class="pq-info-text" style="margin-top: 15px; font-size: 13px;">
<strong>Optional:</strong> Add extra entropy by moving your mouse and typing in the box below.
This mixes your input with the browser's random number generator, making the seed unpredictable
even if the browser's RNG is compromised. You can skip this — a seed has already been generated for you above.
</div>
<div id="pqEntropyBox" style="background: var(--primary-color); color: var(--secondary-color); border-radius: var(--border-radius); padding: 15px; font-size: 13px; min-height: 60px; margin: 10px 0; cursor: text; text-align: left;" tabindex="0">
<div id="pqEntropyBox" style="background: var(--secondary-color); color: var(--primary-color); border: var(--border); border-radius: var(--border-radius); padding: 15px; font-size: 13px; min-height: 60px; margin: 10px 0; cursor: text; text-align: left;" tabindex="0">
<span id="pqEntropyHint">Optional: click here and type random characters, move your mouse to add entropy...</span>
<div style="margin-top: 8px; height: 6px; background: var(--secondary-color); border-radius: 3px; overflow: hidden;">
<div style="margin-top: 8px; height: 6px; background: var(--muted-color); border-radius: 3px; overflow: hidden;">
<div id="pqEntropyBar" style="height: 100%; width: 0%; background: var(--accent-color); transition: width 0.2s;"></div>
</div>
</div>
@@ -403,47 +406,37 @@
<div class="pq-progress-bar">
<div class="pq-progress-fill" id="pqDeriveProgress"></div>
</div>
<div class="pq-key-list" id="pqKeyList">
<div class="pq-key-item" id="pqKeyMlDsa44">
<div class="pq-key-icon"></div>
<div class="pq-key-details">
<div class="pq-key-name">ML-DSA-44 (Dilithium)</div>
<div class="pq-key-meta">FIPS 204 · Cat 2 · 1312-byte pubkey · m/44'/1237'/0'/0/1</div>
<div class="pq-key-pubkey" id="pqKeyMlDsa44Pub"></div>
</div>
<div class="pq-checklist" id="pqKeyList" style="margin-bottom: 15px;">
<div class="pq-checklist-item" id="pqKeyMlDsa44">
<span class="pq-checklist-box"></span>
<span>ML-DSA-44 (Dilithium) — FIPS 204 · Cat 2 · 1312-byte pubkey · m/44'/1237'/0'/0/1
<div class="pq-key-pubkey" id="pqKeyMlDsa44Pub" style="margin-top: 2px;"></div>
</span>
</div>
<div class="pq-key-item" id="pqKeyMlDsa65">
<div class="pq-key-icon"></div>
<div class="pq-key-details">
<div class="pq-key-name">ML-DSA-65 (Dilithium)</div>
<div class="pq-key-meta">FIPS 204 · Cat 3 · 1952-byte pubkey · m/44'/1237'/0'/0/2</div>
<div class="pq-key-pubkey" id="pqKeyMlDsa65Pub"></div>
</div>
<div class="pq-checklist-item" id="pqKeyMlDsa65">
<span class="pq-checklist-box"></span>
<span>ML-DSA-65 (Dilithium) — FIPS 204 · Cat 3 · 1952-byte pubkey · m/44'/1237'/0'/0/2
<div class="pq-key-pubkey" id="pqKeyMlDsa65Pub" style="margin-top: 2px;"></div>
</span>
</div>
<div class="pq-key-item" id="pqKeySlhDsa">
<div class="pq-key-icon"></div>
<div class="pq-key-details">
<div class="pq-key-name">SLH-DSA-128s (SPHINCS+)</div>
<div class="pq-key-meta">FIPS 205 · Cat 1 · 32-byte pubkey · m/44'/1237'/0'/0/3+4</div>
<div class="pq-key-pubkey" id="pqKeySlhDsaPub"></div>
</div>
<div class="pq-checklist-item" id="pqKeySlhDsa">
<span class="pq-checklist-box"></span>
<span>SLH-DSA-128s (SPHINCS+) — FIPS 205 · Cat 1 · 32-byte pubkey · m/44'/1237'/0'/0/3+4
<div class="pq-key-pubkey" id="pqKeySlhDsaPub" style="margin-top: 2px;"></div>
</span>
</div>
<div class="pq-key-item" id="pqKeyFalcon">
<div class="pq-key-icon"></div>
<div class="pq-key-details">
<div class="pq-key-name">Falcon-512</div>
<div class="pq-key-meta">FIPS 206 (draft) · Cat 1 · 897-byte pubkey · m/44'/1237'/0'/0/5+6</div>
<div class="pq-key-pubkey" id="pqKeyFalconPub"></div>
<div class="pq-checklist-item" id="pqKeyFalcon">
<span class="pq-checklist-box"></span>
<span>Falcon-512 — FIPS 206 (draft) · Cat 1 · 897-byte pubkey · m/44'/1237'/0'/0/5+6
<div class="pq-key-pubkey" id="pqKeyFalconPub" style="margin-top: 2px;"></div>
<div style="font-size: 11px; color: var(--accent-color); margin-top: 4px;">⚠ Draft standard — may need re-issuing if FIPS 206 changes</div>
</div>
</span>
</div>
<div class="pq-key-item" id="pqKeyMlKem">
<div class="pq-key-icon"></div>
<div class="pq-key-details">
<div class="pq-key-name">ML-KEM-768 (Kyber)</div>
<div class="pq-key-meta">FIPS 203 · Cat 3 · 1184-byte pubkey · m/44'/1237'/0'/0/7+8</div>
<div class="pq-key-pubkey" id="pqKeyMlKemPub"></div>
</div>
<div class="pq-checklist-item" id="pqKeyMlKem">
<span class="pq-checklist-box"></span>
<span>ML-KEM-768 (Kyber) — FIPS 203 · Cat 3 · 1184-byte pubkey · m/44'/1237'/0'/0/7+8
<div class="pq-key-pubkey" id="pqKeyMlKemPub" style="margin-top: 2px;"></div>
</span>
</div>
</div>
<div id="pqDeriveStatus"></div>
@@ -460,50 +453,6 @@
full kind 1 event (including its secp256k1 signature) is submitted to OpenTimestamps.
</div>
<div id="pqSignStatus"></div>
<div class="pq-key-list" id="pqSignChecklist">
<div class="pq-key-item" id="pqSignPqMlDsa44">
<div class="pq-key-icon"></div>
<div class="pq-key-details">
<div class="pq-key-name">PQ signature: ML-DSA-44</div>
<div class="pq-key-meta">Done in browser (pure JS)</div>
</div>
</div>
<div class="pq-key-item" id="pqSignPqMlDsa65">
<div class="pq-key-icon"></div>
<div class="pq-key-details">
<div class="pq-key-name">PQ signature: ML-DSA-65</div>
<div class="pq-key-meta">Done in browser (pure JS)</div>
</div>
</div>
<div class="pq-key-item" id="pqSignPqSlhDsa">
<div class="pq-key-icon"></div>
<div class="pq-key-details">
<div class="pq-key-name">PQ signature: SLH-DSA-128s</div>
<div class="pq-key-meta">Done in browser (pure JS)</div>
</div>
</div>
<div class="pq-key-item" id="pqSignPqFalcon">
<div class="pq-key-icon"></div>
<div class="pq-key-details">
<div class="pq-key-name">PQ signature: Falcon-512</div>
<div class="pq-key-meta">Done in browser (pure JS)</div>
</div>
</div>
<div class="pq-key-item" id="pqSignSecp">
<div class="pq-key-icon"></div>
<div class="pq-key-details">
<div class="pq-key-name">secp256k1 signature: kind 1 announcement</div>
<div class="pq-key-meta">Requires approval from your signer</div>
</div>
</div>
<div class="pq-key-item" id="pqSignSecpWrapper">
<div class="pq-key-icon"></div>
<div class="pq-key-details">
<div class="pq-key-name">secp256k1 signature: kind 11112 wrapper</div>
<div class="pq-key-meta">Requires approval from your signer</div>
</div>
</div>
</div>
<button class="pq-button" id="pqSignBtn">Sign Event</button>
<div id="pqEventPreviewWrap" class="pq-hidden" style="margin-top: 15px;">
<div class="pq-info-text" style="margin-bottom: 5px;">Event preview:</div>
@@ -527,12 +476,17 @@
<div class="pq-info-text" style="margin-top: 15px; margin-bottom: 5px;"><strong>Kind 11112 wrapper event:</strong></div>
<div id="pqEventIdDisplay" style="margin: 5px 0; font-size: 11px; color: var(--primary-color); word-break: break-all;"></div>
<div class="pq-event-preview" id="pqSuccessEventPreview"></div>
<div class="pq-button-row" style="margin-top: 10px;">
<button class="pq-button pq-button-secondary" id="pqCopyKind11112Btn">Copy Kind 11112 Event</button>
</div>
<div style="margin-top: 15px;">
<div class="pq-info-text" style="margin-bottom: 5px;"><strong>Relays to publish to:</strong></div>
<input type="text" class="pq-relay-input" id="pqRelaysInput"
value="wss://laantungir.net/relay,wss://relay.damus.io,wss://nos.lol,wss://relay.primal.net"
placeholder="wss://relay1.com,wss://relay2.com" />
<div id="pqRelayList" style="display: flex; flex-direction: column; gap: 8px; margin-bottom: 10px;"></div>
<div class="pq-button-row">
<input type="text" class="pq-relay-input" id="pqRelayAddInput" style="flex: 1; margin: 0;" placeholder="wss://relay.example.com" />
<button class="pq-button pq-button-secondary" id="pqRelayAddBtn" style="flex: 0 0 auto; width: auto; padding: 12px 20px;">Add Relay</button>
</div>
</div>
<div id="pqPublishStatus"></div>
@@ -540,6 +494,9 @@
<div class="pq-button-row" style="margin-top: 15px;">
<button class="pq-button" id="pqPublishBtn">Publish Both Events to Relays</button>
</div>
<div class="pq-button-row">
<button class="pq-button pq-button-secondary pq-hidden" id="pqOtsContinueBtn">Continue to OpenTimestamps</button>
</div>
<div class="pq-button-row">
<button class="pq-button pq-button-secondary" id="pqDoneBtn">Done</button>
</div>
@@ -549,47 +506,41 @@
<div id="pqOtsStep" class="pq-card pq-hidden">
<div class="pq-card-title">OpenTimestamps Attestation</div>
<div class="pq-info-text">
The NIP-QR event is being timestamped on the Bitcoin blockchain via OpenTimestamps.
This proves the event existed at this point in time, preventing a future quantum attacker
from backdating a fraudulent event. Bitcoin confirmation typically takes 10-30 minutes.
Your event hash has already been submitted to OpenTimestamps in the previous step, and a
<strong>pending</strong> timestamp proof is embedded in the kind 11112 event you just published.
OpenTimestamps anchors that hash into the Bitcoin blockchain, proving your event existed at this
point in time — preventing a future quantum attacker from backdating a fraudulent event.
</div>
<div class="pq-button-row" id="pqOtsSubmitButtonRow">
<button class="pq-button" id="pqOtsStartBtn">Submit to OpenTimestamps</button>
<div class="pq-info-text">
Bitcoin confirmation typically takes <strong>10-30 minutes</strong>. You can leave this page open
to watch the progress below — the polling log updates every minute. Once the Bitcoin attestation is
confirmed, this page will automatically re-sign and republish the kind 11112 event with the upgraded
proof. You're free to close the page; the workflow resumes on your next visit.
</div>
<div id="pqOtsStatus"></div>
<!-- Polling log window -->
<div class="pq-info-text" style="margin-top: 10px; margin-bottom: 5px;">Polling log:</div>
<div id="pqOtsLog" style="background: var(--primary-color); color: var(--secondary-color); border-radius: var(--border-radius); padding: 10px; font-size: 11px; font-family: monospace; height: 120px; overflow-y: auto; text-align: left; margin-bottom: 15px;"></div>
<div id="pqOtsLog" style="background: var(--secondary-color); color: var(--primary-color); border: var(--border); border-radius: var(--border-radius); padding: 10px; font-size: 11px; font-family: monospace; height: 120px; overflow-y: auto; text-align: left; margin-bottom: 15px;"></div>
<div class="pq-key-list">
<div class="pq-key-item" id="pqOtsSubmit">
<div class="pq-key-icon"></div>
<div class="pq-key-details">
<div class="pq-key-name">Submit hash to OpenTimestamps</div>
<div class="pq-key-meta">Creates a complete pending detached .ots proof</div>
</div>
<div class="pq-checklist" style="margin-bottom: 15px;">
<div class="pq-checklist-item pq-done" id="pqOtsSubmit">
<span class="pq-checklist-box"></span>
<span>Submit hash to OpenTimestamps — pending .ots proof created</span>
</div>
<div class="pq-key-item" id="pqOtsPendingPublish">
<div class="pq-key-icon"></div>
<div class="pq-key-details">
<div class="pq-key-name">Pending proof embedded in kind 11112 event</div>
<div class="pq-key-meta">The pending .ots proof is embedded as a tag in the initial event</div>
</div>
<div class="pq-checklist-item pq-done" id="pqOtsPendingPublish">
<span class="pq-checklist-box"></span>
<span>Pending proof embedded in kind 11112 event</span>
</div>
<div class="pq-key-item" id="pqOtsConfirm">
<div class="pq-key-icon"></div>
<div class="pq-key-details">
<div class="pq-key-name">Wait for Bitcoin confirmation</div>
<div class="pq-key-meta" id="pqOtsConfirmDetail">Polling every 60 seconds...</div>
</div>
<div class="pq-checklist-item pq-active" id="pqOtsConfirm">
<span class="pq-checklist-box"></span>
<span>Wait for Bitcoin confirmation
<div class="pq-key-meta" id="pqOtsConfirmDetail" style="margin-top: 2px;">Polling every 60 seconds...</div>
</span>
</div>
<div class="pq-key-item" id="pqOtsConfirmedPublish">
<div class="pq-key-icon"></div>
<div class="pq-key-details">
<div class="pq-key-name">Republish kind 11112 with confirmed proof</div>
<div class="pq-key-meta">Republishes the event with the upgraded Bitcoin-attested proof</div>
</div>
<div class="pq-checklist-item" id="pqOtsConfirmedPublish">
<span class="pq-checklist-box"></span>
<span>Republish kind 11112 with confirmed proof — automatic on confirmation</span>
</div>
</div>
<div id="pqOtsProofWrap" class="pq-hidden" style="margin-top: 15px;">
@@ -766,7 +717,7 @@
console.log('[ots] Found saved OTS workflow from previous session, event:', pendingOts.eventId);
pendingOtsBytes = pendingOts.ots;
pqEvent = { id: pendingOts.eventId, kind: pendingOts.targetKind || NIP_QR_KIND };
if (pendingOts.relayUrls) document.getElementById('pqRelaysInput').value = pendingOts.relayUrls;
if (pendingOts.relayUrls) setRelayUrls(pendingOts.relayUrls);
// F-H3: Build the OTS resume notice with safe DOM construction
const otsNotice = document.createElement('div');
@@ -802,8 +753,16 @@
function setKeyIcon(elementId, icon) {
const el = document.getElementById(elementId);
const iconEl = el.querySelector('.pq-key-icon');
iconEl.textContent = icon;
if (!el) return;
// Supports both the old .pq-key-icon elements and the new checklist
// .pq-checklist-box elements (used in Step 3 and Step 6).
const iconEl = el.querySelector('.pq-key-icon') || el.querySelector('.pq-checklist-box');
if (iconEl) iconEl.textContent = icon;
if (icon && el.classList.contains('pq-checklist-item')) {
el.classList.add('pq-done');
} else if (!icon && el.classList.contains('pq-checklist-item')) {
el.classList.remove('pq-done');
}
}
/* ================================================================
@@ -1077,10 +1036,6 @@
setStatus(pqSignStatus, 'info', ' Building kind 1 announcement event...');
const kind1Template = buildKind1Announcement(currentPubkey, blockHeight, pqKeys);
setKeyIcon('pqSignPqMlDsa44', 'Done');
setKeyIcon('pqSignPqMlDsa65', 'Done');
setKeyIcon('pqSignPqSlhDsa', 'Done');
setKeyIcon('pqSignPqFalcon', 'Done');
setStatus(pqSignStatus, 'info', 'PQ signatures complete. Requesting secp256k1 signature for kind 1 announcement...');
const kind1Signed = await window.nostr.signEvent(kind1Template);
@@ -1124,8 +1079,6 @@
}
const kind11112Signed = await window.nostr.signEvent(kind11112Template);
setKeyIcon('pqSignSecp', 'Done');
setKeyIcon('pqSignSecpWrapper', 'Done');
pqEvent = {
id: kind11112Signed.id,
@@ -1156,7 +1109,7 @@
document.getElementById('pqKind1Preview').textContent = JSON.stringify(kind1Event, null, 2);
pqEventIdDisplay.textContent = `Event ID: ${pqEvent.id}`;
document.getElementById('pqSuccessEventPreview').textContent = eventJsonStr;
setTimeout(() => showView('pqPublishStep'), 1000);
setTimeout(() => { renderRelayList(); showView('pqPublishStep'); }, 1000);
} catch (error) {
console.error('[post-quantum] Sign failed:', error);
@@ -1196,6 +1149,112 @@
}));
}
/* ================================================================
RELAY LIST MANAGEMENT
================================================================ */
const DEFAULT_RELAYS = [
'wss://laantungir.net/relay',
'wss://relay.damus.io',
'wss://nos.lol',
'wss://relay.primal.net'
];
let pqRelays = DEFAULT_RELAYS.slice();
// Per-relay publish status: { url: { kind1: bool, kind11112: bool } }
const pqRelayStatus = {};
function makeStatusCheckbox(label) {
const wrap = document.createElement('label');
wrap.style.cssText = 'display: inline-flex; align-items: center; gap: 4px; font-size: 12px; color: var(--muted-color); cursor: default; flex: 0 0 auto;';
const box = document.createElement('span');
box.className = 'pq-checklist-box';
box.textContent = '';
box.style.cssText = 'width: 16px; height: 16px; border: 2px solid var(--muted-color); border-radius: 4px; display: inline-flex; align-items: center; justify-content: center; font-size: 11px; font-weight: bold;';
const lbl = document.createElement('span');
lbl.textContent = label;
wrap.appendChild(box);
wrap.appendChild(lbl);
return { wrap, box };
}
function setRelayStatus(url, kind, ok) {
if (!pqRelayStatus[url]) pqRelayStatus[url] = { kind1: false, kind11112: false };
pqRelayStatus[url][kind] = ok;
const row = document.querySelector(`.pq-relay-row[data-relay="${CSS.escape(url)}"]`);
if (!row) return;
const box = row.querySelector(`.pq-relay-status-${kind}`);
if (box) {
box.textContent = ok ? '✓' : '';
box.style.background = ok ? 'var(--accent-color)' : 'transparent';
box.style.color = ok ? 'var(--secondary-color)' : 'var(--primary-color)';
box.style.borderColor = ok ? 'var(--accent-color)' : 'var(--muted-color)';
}
}
function renderRelayList() {
const listEl = document.getElementById('pqRelayList');
if (!listEl) return;
listEl.innerHTML = '';
pqRelays.forEach((url, idx) => {
const row = document.createElement('div');
row.className = 'pq-relay-row';
row.dataset.relay = url;
row.style.cssText = 'display: flex; align-items: center; gap: 10px; background: var(--secondary-color); border: var(--border); border-radius: var(--border-radius); padding: 10px 14px;';
const urlSpan = document.createElement('span');
urlSpan.textContent = url;
urlSpan.style.cssText = 'flex: 1; font-size: 14px; color: var(--primary-color); word-break: break-all;';
row.appendChild(urlSpan);
const k1 = makeStatusCheckbox('k1');
k1.box.classList.add('pq-relay-status-kind1');
row.appendChild(k1.wrap);
const k11112 = makeStatusCheckbox('k11112');
k11112.box.classList.add('pq-relay-status-kind11112');
row.appendChild(k11112.wrap);
const removeBtn = document.createElement('button');
removeBtn.className = 'pq-button pq-button-secondary';
removeBtn.textContent = 'Remove';
removeBtn.style.cssText = 'flex: 0 0 auto; width: auto; padding: 8px 16px; font-size: 13px;';
removeBtn.addEventListener('click', () => {
pqRelays.splice(idx, 1);
delete pqRelayStatus[url];
renderRelayList();
});
row.appendChild(removeBtn);
listEl.appendChild(row);
// Restore any existing status
const st = pqRelayStatus[url];
if (st) {
if (st.kind1) setRelayStatus(url, 'kind1', true);
if (st.kind11112) setRelayStatus(url, 'kind11112', true);
}
});
}
function getRelayUrls() {
return pqRelays.slice();
}
function setRelayUrls(urls) {
// Accept either a comma-separated string or an array
if (typeof urls === 'string') {
pqRelays = urls.split(',').map(s => s.trim()).filter(Boolean);
} else if (Array.isArray(urls)) {
pqRelays = urls.slice();
}
if (pqRelays.length === 0) pqRelays = DEFAULT_RELAYS.slice();
renderRelayList();
}
function getRelayUrlsString() {
return pqRelays.join(',');
}
/* ================================================================
EVENT LISTENERS
================================================================ */
@@ -1255,19 +1314,6 @@
}
});
// "Generate New Seed" — generates a fresh seed using pure CSPRNG (no user entropy)
document.getElementById('pqRegenerateBtn').addEventListener('click', () => {
userEntropyChunks = [];
generateAndShowSeed();
// Reset entropy UI
const entropyBar = document.getElementById('pqEntropyBar');
const entropyHint = document.getElementById('pqEntropyHint');
const regenWithEntropyBtn = document.getElementById('pqRegenerateWithEntropyBtn');
if (entropyBar) entropyBar.style.width = '0%';
if (entropyHint) entropyHint.textContent = 'Optional: click here and type random characters, move your mouse to add entropy...';
if (regenWithEntropyBtn) { regenWithEntropyBtn.classList.add('pq-hidden'); regenWithEntropyBtn.disabled = true; }
});
// "Generate with Extra Entropy" — generates a fresh seed mixing CSPRNG + user entropy
document.getElementById('pqRegenerateWithEntropyBtn').addEventListener('click', () => { generateAndShowSeed(); });
@@ -1283,10 +1329,11 @@
setStepActive(5);
publishStatus.innerHTML = '<div class="pq-status pq-status-info">Publishing kind 1 announcement to relays...</div>';
try {
const relayUrls = document.getElementById('pqRelaysInput').value.split(',').map(s => s.trim()).filter(s => s);
const relayUrls = getRelayUrls();
// Publish kind 1 announcement first
const results1 = await publishToRelays(kind1Event, relayUrls);
results1.forEach(r => setRelayStatus(r.relay, 'kind1', r.success));
const success1 = results1.filter(r => r.success).length;
const fail1 = results1.filter(r => !r.success).length;
otsLog(`Kind 1 announcement published: ${success1} ok, ${fail1} failed.`);
@@ -1294,15 +1341,18 @@
// Publish kind 11112 wrapper
publishStatus.innerHTML = '<div class="pq-status pq-status-info">Publishing kind 11112 wrapper to relays...</div>';
const results2 = await publishToRelays(pqEvent, relayUrls);
results2.forEach(r => setRelayStatus(r.relay, 'kind11112', r.success));
const success2 = results2.filter(r => r.success).length;
const fail2 = results2.filter(r => !r.success).length;
otsLog(`Kind 11112 wrapper published: ${success2} ok, ${fail2} failed.`);
setStatus(publishStatus, 'success', `Published! Kind 1: ${success1}/${relayUrls.length} relays. Kind 11112: ${success2}/${relayUrls.length} relays.`);
publishBtn.textContent = 'Published';
publishBtn.disabled = true;
setStepDone(5);
// Present the OpenTimestamps approval step; submission starts on user approval.
prepareOtsStep();
// Show a Continue button so the user can review the relay results before advancing.
const continueBtn = document.getElementById('pqOtsContinueBtn');
if (continueBtn) continueBtn.classList.remove('pq-hidden');
} catch (error) {
setStatus(publishStatus, 'error', `Publish failed: ${error.message}`);
publishBtn.disabled = false;
@@ -1311,6 +1361,43 @@
document.getElementById('pqDoneBtn').addEventListener('click', () => { showAuthedView(); });
// Continue to OpenTimestamps step (shown after successful publish)
document.getElementById('pqOtsContinueBtn').addEventListener('click', () => { prepareOtsStep(); });
// Add Relay button
document.getElementById('pqRelayAddBtn').addEventListener('click', () => {
const input = document.getElementById('pqRelayAddInput');
const url = input.value.trim();
if (!url) return;
if (!/^wss?:\/\//.test(url)) {
setStatus(document.getElementById('pqPublishStatus'), 'error', 'Relay URL must start with wss:// or ws://');
return;
}
if (pqRelays.includes(url)) {
setStatus(document.getElementById('pqPublishStatus'), 'error', 'That relay is already in the list.');
return;
}
pqRelays.push(url);
input.value = '';
renderRelayList();
document.getElementById('pqPublishStatus').innerHTML = '';
});
// Allow pressing Enter in the add-relay input to add the relay
document.getElementById('pqRelayAddInput').addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); document.getElementById('pqRelayAddBtn').click(); }
});
// Copy Kind 11112 Event button
document.getElementById('pqCopyKind11112Btn').addEventListener('click', () => {
if (!pqEvent) return;
navigator.clipboard.writeText(JSON.stringify(pqEvent, null, 2)).then(() => {
const btn = document.getElementById('pqCopyKind11112Btn');
btn.textContent = 'Copied!';
setTimeout(() => { btn.textContent = 'Copy Kind 11112 Event'; }, 2000);
});
});
// Sign out button
const pqSignOutBtn = document.getElementById('pqSignOutBtn');
pqSignOutBtn.addEventListener('click', async () => {
@@ -1334,6 +1421,7 @@
userEntropyChunks = [];
pendingOtsBytes = null;
currentBlockHeight = 0;
pqRelays = DEFAULT_RELAYS.slice();
// Clear auth/session storage, then restore only the OTS workflow record.
try {
localStorage.clear();
@@ -1370,32 +1458,16 @@
}
function prepareOtsStep() {
showView('pqOtsStep');
setStepActive(6);
const otsStatus = document.getElementById('pqOtsStatus');
const otsLogEl = document.getElementById('pqOtsLog');
const startBtn = document.getElementById('pqOtsStartBtn');
if (otsLogEl) otsLogEl.innerHTML = '';
otsStatus.innerHTML = '<div class="pq-status pq-status-info">Review this step, then submit the NIP-QR event hash to OpenTimestamps.</div>';
startBtn.disabled = false;
startBtn.textContent = 'Submit to OpenTimestamps';
document.getElementById('pqOtsSubmitButtonRow').classList.remove('pq-hidden');
setKeyIcon('pqOtsSubmit', '');
setKeyIcon('pqOtsPendingPublish', '');
setKeyIcon('pqOtsConfirm', '');
setKeyIcon('pqOtsConfirmedPublish', '');
otsLog('Waiting for user approval to submit the event hash.');
// The OTS submission already happened in step 4. Auto-start the polling flow.
startOtsFlow();
}
async function startOtsFlow() {
showView('pqOtsStep');
setStepActive(6);
const otsStatus = document.getElementById('pqOtsStatus');
const startBtn = document.getElementById('pqOtsStartBtn');
startBtn.disabled = true;
startBtn.textContent = 'Processing...';
setKeyIcon('pqOtsSubmit', '');
setKeyIcon('pqOtsPendingPublish', '');
const otsLogEl = document.getElementById('pqOtsLog');
if (otsLogEl) otsLogEl.innerHTML = '';
setKeyIcon('pqOtsConfirm', '');
setKeyIcon('pqOtsConfirmedPublish', '');
@@ -1409,7 +1481,6 @@
if (existing && existing.eventId === pqEvent.id) {
pendingOtsBytes = existing.ots;
otsLog(`Loaded saved OTS workflow for event ${pqEvent.id.substring(0, 16)}...`);
setKeyIcon('pqOtsSubmit', 'Done');
if (existing.pendingPublished) setKeyIcon('pqOtsPendingPublish', 'Done');
if (existing.confirmedPublished) {
setKeyIcon('pqOtsConfirm', 'Done');
@@ -1418,13 +1489,11 @@
setStepActive(7);
otsStatus.innerHTML = '<div class="pq-status pq-status-success">Confirmed timestamp proof was already published.</div>';
showOtsProof(pendingOtsBytes);
startBtn.textContent = 'Completed';
return;
}
showOtsProof(pendingOtsBytes);
setKeyIcon('pqOtsPendingPublish', 'Done');
otsStatus.innerHTML = '<div class="pq-status pq-status-info">Pending proof is embedded in the kind 11112 event. Waiting for Bitcoin confirmation...</div>';
startBtn.textContent = 'Submitted';
startOtsPolling();
return;
}
@@ -1437,18 +1506,16 @@
pendingOtsBytes = base64ToBytes(otsTag[1]);
if (isDetachedOtsFile(pendingOtsBytes)) {
otsLog(`Found pending OTS proof embedded in event (${pendingOtsBytes.length} bytes).`);
setKeyIcon('pqOtsSubmit', 'Done');
showOtsProof(pendingOtsBytes);
savePendingOts(pqEvent.id, pendingOtsBytes, {
ownerPubkey: currentPubkey,
targetKind: pqEvent.kind,
relayUrls: document.getElementById('pqRelaysInput').value,
relayUrls: getRelayUrlsString(),
pendingPublished: true,
confirmedPublished: false
});
setKeyIcon('pqOtsPendingPublish', 'Done');
otsStatus.innerHTML = '<div class="pq-status pq-status-info">Pending proof is embedded in the kind 11112 event. Waiting for Bitcoin confirmation (polling every 60 seconds)...</div>';
startBtn.textContent = 'Submitted';
startOtsPolling();
return;
}
@@ -1463,8 +1530,6 @@
if (!sha256Tag || !sha256Tag[1]) {
otsLog('ERROR: No sha256 tag found in event. Cannot timestamp.');
otsStatus.innerHTML = '<div class="pq-status pq-status-error">Event is missing the sha256 tag.</div>';
startBtn.disabled = false;
startBtn.textContent = 'Retry';
return;
}
const fullHash = sha256Tag[1];
@@ -1475,7 +1540,6 @@
const otsBytes = await timestampEvent(fullHash);
pendingOtsBytes = otsBytes;
otsLog(`Created pending detached .ots file (${otsBytes.length} bytes).`);
setKeyIcon('pqOtsSubmit', 'Done');
showOtsProof(otsBytes);
// Republish the event with the OTS proof embedded
@@ -1485,19 +1549,16 @@
savePendingOts(pqEvent.id, otsBytes, {
ownerPubkey: currentPubkey,
targetKind: pqEvent.kind,
relayUrls: document.getElementById('pqRelaysInput').value,
relayUrls: getRelayUrlsString(),
pendingPublished: true,
confirmedPublished: false
});
otsStatus.innerHTML = '<div class="pq-status pq-status-info">Pending proof embedded. Waiting for Bitcoin confirmation (polling every 60 seconds)...</div>';
startBtn.textContent = 'Submitted';
startOtsPolling();
} catch (error) {
console.error('[ots] Submission failed:', error);
otsLog(`ERROR: ${error.message}`);
setStatus(otsStatus, 'error', `OpenTimestamps workflow failed: ${error.message}`);
startBtn.disabled = false;
startBtn.textContent = 'Retry OpenTimestamps Submission';
}
}
@@ -1549,11 +1610,19 @@
// Structural check found a Bitcoin attestation tag but full
// verification failed — log the errors for diagnosis.
const errs = verifyResult.errors.length > 0 ? ` Errors: ${verifyResult.errors.join('; ')}` : '';
otsLog(`Poll ${pollCount}: Bitcoin attestation tag found but verification failed.${errs} Will retry.`);
const detail = result.detail ? ` (${result.detail.replace(/\s+/g, ' ').slice(0, 180)})` : '';
confirmDetail.textContent = `Attestation found but unverified (attempt ${pollCount}). Retrying in 60s...`;
otsLog(`Poll ${pollCount}: Bitcoin attestation tag found but verification incomplete.${errs} Will retry.`);
confirmDetail.textContent = `Attestation found, verifying (attempt ${pollCount}). Retrying in 60s...`;
} else {
const detail = result.detail ? ` (${result.detail.replace(/\s+/g, ' ').slice(0, 180)})` : '';
// Still pending — the upgrade helper's stderr often contains
// "Failed! Timestamp not complete" which is normal pending output
// from ots-cli, not an actual error. Sanitize it for display.
const rawDetail = (result.detail || '').replace(/\s+/g, ' ').trim();
const friendlyDetail = rawDetail
.replace(/Failed!\s*Timestamp not complete\.?\s*/gi, '')
.replace(/Failed!/gi, 'Pending')
.replace(/Pending confirmation in Bitcoin blockchain/gi, 'Waiting for Bitcoin confirmation')
.trim();
const detail = friendlyDetail ? ` (${friendlyDetail.slice(0, 180)})` : '';
otsLog(`Poll ${pollCount}: Still pending${detail}. Next poll in 60 seconds.`);
confirmDetail.textContent = `Still pending (attempt ${pollCount}). Next poll in 60s...`;
}
@@ -1572,7 +1641,7 @@
try {
otsLog('Building upgraded kind 11112 event with confirmed OTS proof...');
const savedWorkflow = loadPendingOts();
const configuredRelays = document.getElementById('pqRelaysInput').value || (savedWorkflow && savedWorkflow.relayUrls) || 'wss://laantungir.net/relay';
const configuredRelays = getRelayUrlsString() || (savedWorkflow && savedWorkflow.relayUrls) || 'wss://laantungir.net/relay';
// Build the upgraded event: copy all tags from the original, replace the ots tag
const upgradedTemplate = buildUpgradedEvent(pqEvent, upgradedOtsBytes);
@@ -1610,8 +1679,6 @@
}
// OTS event listeners
document.getElementById('pqOtsStartBtn').addEventListener('click', () => { startOtsFlow(); });
document.getElementById('pqOtsDownloadBtn').addEventListener('click', () => {
if (pendingOtsBytes) {
const blob = new Blob([pendingOtsBytes], { type: 'application/octet-stream' });
+6 -1
View File
@@ -1054,7 +1054,12 @@ export function parseOtsFile(otsBytes) {
* - other = operation tag; apply op to current msg → result; recurse into subtree with result
*/
function _parseTimestamp(reader, msg, attestations, depth) {
if (depth > 64) throw new Error('OTS: recursion limit exceeded');
// The OTS timestamp tree can be deeply nested, especially after many
// upgrade cycles (each upgrade can add new branches/operations). The
// reference Python implementation doesn't impose a low limit here.
// 512 is generous enough for heavily upgraded proofs while still
// guarding against pathological/malicious inputs.
if (depth > 512) throw new Error('OTS: recursion limit exceeded');
let tag = reader.readByte();
while (tag === 0xff) {
+3 -3
View File
@@ -1,5 +1,5 @@
{
"VERSION": "v0.0.2",
"VERSION_NUMBER": "0.0.2",
"BUILD_DATE": "2026-07-17T23:14:56.946Z"
"VERSION": "v0.0.7",
"VERSION_NUMBER": "0.0.7",
"BUILD_DATE": "2026-07-19T11:33:54.796Z"
}
+1 -1
View File
@@ -10154,7 +10154,7 @@ function parseOtsFile(otsBytes) {
return { fileHashOp, targetDigest, attestations };
}
function _parseTimestamp(reader, msg, attestations, depth) {
if (depth > 64) throw new Error("OTS: recursion limit exceeded");
if (depth > 512) throw new Error("OTS: recursion limit exceeded");
let tag = reader.readByte();
while (tag === 255) {
const current = reader.readByte();
+42
View File
@@ -820,6 +820,48 @@
otsRepublishBtn.disabled = false;
}
});
/* ================================================================
AUTO-DETECT SIGNED-IN USER
On page load, check for a NIP-07 signer / nostr-login-lite. If the
user is already signed in, pre-fill the pubkey field and auto-query
relays for their kind 11112 event.
================================================================ */
async function initAutoDetect() {
// Initialize nostr-login-lite if available (shares session with main page)
if (window.NOSTR_LOGIN_LITE) {
try {
if (!window.NOSTR_LOGIN_LITE._initialized) {
await window.NOSTR_LOGIN_LITE.init({
methods: { extension: true, local: true, nip46: true, seedphrase: true, nsigner: true }
});
}
} catch (e) {
console.log('[verify] nostr-login-lite init failed:', e.message);
}
}
if (!window.nostr || !window.nostr.getPublicKey) return;
let pubkeyHex = null;
try {
pubkeyHex = await window.nostr.getPublicKey();
} catch (e) {
console.log('[verify] Not signed in:', e.message);
return;
}
if (!pubkeyHex) return;
console.log('[verify] Auto-detected signed-in pubkey:', pubkeyHex);
const pubkeyInput = document.getElementById('pqPubkeyInput');
const npub = hexToNpub(pubkeyHex);
pubkeyInput.value = npub || pubkeyHex;
// Auto-trigger the query
queryBtn.click();
}
initAutoDetect();
</script>
</body>