Compare commits

...
2 Commits
6 changed files with 242 additions and 84 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "nostr_quantum_preparation",
"version": "0.0.33",
"version": "0.0.35",
"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": {
+17 -18
View File
@@ -104,16 +104,6 @@
line-height: 20px;
}
/* Inline braille spinner shown next to the "Wait for Bitcoin
confirmation" label. Frames advanced by JS (startInlineSpinner). */
.pq-inline-spinner {
display: inline-block;
color: var(--accent-color);
font-size: 16px;
line-height: 1;
margin-right: 6px;
vertical-align: -2px;
}
.pq-checklist-link {
color: var(--accent-color);
@@ -366,9 +356,14 @@
<li>A new seed phrase. This should be kept secret and private.</li>
<li>A proof archive. This information is published to relays, but keeping a copy yourself is useful in case the relays don't keep the event.</li>
</ol>
In case quantum computers break nostr, you can use your seed phrase to create a new nostr
</div>
<div class="pq-info-text">
In case quantum computers break nostr, you can use your new seed phrase to create a new nostr
identity, and prove that your old nostr identity created this new one.
</div>
<div class="pq-info-text" id="noSignerWarning" style="color: var(--accent-color); display: none;">
No Nostr signer detected. Install a NIP-07 browser extension (like nos2x or Alby) and reload.
</div>
@@ -611,8 +606,8 @@
<span>Pending proof embedded in kind 9999 event</span>
</div>
<div class="pq-checklist-item pq-active" id="pqOtsConfirm">
<span class="pq-checklist-box"></span>
<span><span class="pq-inline-spinner" id="pqOtsConfirmSpinner"></span>Wait for Bitcoin confirmation
<span class="pq-checklist-box" id="pqOtsConfirmBox"></span>
<span>Wait for Bitcoin confirmation
<div class="pq-key-meta" id="pqOtsConfirmDetail" style="margin-top: 2px;">Polling every 60 seconds...</div>
</span>
</div>
@@ -621,11 +616,15 @@
<span>Publish new kind 9999 with confirmed proof — automatic on confirmation</span>
</div>
</div>
<div id="pqOtsProofWrap" class="pq-hidden" style="margin-top: 15px;">
<div class="pq-info-text" style="margin-bottom: 5px;">OTS proof file (base64):</div>
<div class="pq-event-preview" id="pqOtsProof" style="max-height: 150px;"></div>
<div class="pq-button-row" style="margin-top: 10px;">
<button class="pq-button pq-button-secondary" id="pqOtsDownloadBtn">Download .ots file</button>
<div id="pqOtsArchiveWrap" class="pq-hidden" style="margin-top: 15px;">
<div class="pq-info-text" style="margin-bottom: 8px;">
Bitcoin confirmation verified and the upgraded proof carrier has been published to relays.
Download the upgraded proof archive below — it contains the kind 1 event, the upgraded kind 9999
proof carrier, and the <strong>confirmed</strong> OTS proof (with Bitcoin attestation). Keep a copy
in case relays delete the event.
</div>
<div class="pq-button-row">
<button class="pq-button pq-button-secondary" id="pqOtsArchiveBtn">Download Upgraded Proof Archive</button>
</div>
</div>
</div>
+51 -35
View File
@@ -140,24 +140,25 @@
}
}
/* Inline braille spinner next to the "Wait for Bitcoin confirmation"
label. Same frames as the checklist-box spinner. */
let inlineSpinnerInterval = null;
function startInlineSpinner(elementId) {
const el = document.getElementById(elementId);
if (!el) return;
stopInlineSpinner(elementId);
/* Braille spinner on the "Wait for Bitcoin confirmation" checklist box
(pqOtsConfirm). Same frames as the step-6 checklist-box spinner. */
let otsConfirmSpinnerInterval = null;
function startOtsConfirmSpinner() {
const box = document.getElementById('pqOtsConfirmBox');
if (!box) return;
stopOtsConfirmSpinner();
box.classList.add('pq-spinner');
let frame = 0;
el.textContent = SPINNER_FRAMES[0];
inlineSpinnerInterval = setInterval(() => {
box.textContent = SPINNER_FRAMES[0];
otsConfirmSpinnerInterval = setInterval(() => {
frame = (frame + 1) % SPINNER_FRAMES.length;
el.textContent = SPINNER_FRAMES[frame];
box.textContent = SPINNER_FRAMES[frame];
}, 80);
}
function stopInlineSpinner() {
if (inlineSpinnerInterval) { clearInterval(inlineSpinnerInterval); inlineSpinnerInterval = null; }
const el = document.getElementById('pqOtsConfirmSpinner');
if (el) el.textContent = '';
function stopOtsConfirmSpinner() {
if (otsConfirmSpinnerInterval) { clearInterval(otsConfirmSpinnerInterval); otsConfirmSpinnerInterval = null; }
const box = document.getElementById('pqOtsConfirmBox');
if (box) { box.classList.remove('pq-spinner'); box.textContent = ''; }
}
/* ================================================================
@@ -1066,7 +1067,7 @@
// Stop OTS polling, but preserve its persisted workflow for the next login.
if (otsPollInterval) { clearInterval(otsPollInterval); otsPollInterval = null; }
clearStepSpinner(6);
stopInlineSpinner();
stopOtsConfirmSpinner();
pendingOtsBytes = null;
const persistedOts = localStorage.getItem('pq-pending-ots');
// Logout from nostr-login-lite
@@ -1163,7 +1164,8 @@
if (otsLogEl) otsLogEl.innerHTML = '';
setKeyIcon('pqOtsConfirm', '');
setKeyIcon('pqOtsConfirmedPublish', '');
stopInlineSpinner();
stopOtsConfirmSpinner();
hideUpgradedArchiveButton();
// Check for a saved workflow from a previous session (same event ID)
let existing = loadPendingOts();
@@ -1182,10 +1184,10 @@
setStepDone(6);
setStepActive(7);
otsStatus.innerHTML = '<div class="pq-status pq-status-success">Confirmed timestamp proof was already published.</div>';
showOtsProof(pendingOtsBytes);
showUpgradedArchiveButton();
return;
}
showOtsProof(pendingOtsBytes);
hideUpgradedArchiveButton();
setKeyIcon('pqOtsPendingPublish', 'Done');
otsStatus.innerHTML = '<div class="pq-status pq-status-info">Pending proof is embedded in the kind 9999 event. Waiting for Bitcoin confirmation...</div>';
startOtsPolling();
@@ -1200,7 +1202,6 @@
pendingOtsBytes = base64ToBytes(otsTag[1]);
if (isDetachedOtsFile(pendingOtsBytes)) {
otsLog(`Found pending OTS proof embedded in event (${pendingOtsBytes.length} bytes).`);
showOtsProof(pendingOtsBytes);
savePendingOts(pqEvent.id, pendingOtsBytes, {
ownerPubkey: currentPubkey,
targetKind: pqEvent.kind,
@@ -1236,8 +1237,6 @@
const otsBytes = await timestampEvent(fullHash);
pendingOtsBytes = otsBytes;
otsLog(`Created pending detached .ots file (${otsBytes.length} bytes).`);
showOtsProof(otsBytes);
// Republish the event with the OTS proof embedded
otsStatus.innerHTML = '<div class="pq-status pq-status-info">Publishing new kind 9999 event with embedded OTS proof...</div>';
await publishUpgradedEvent(otsBytes);
@@ -1260,11 +1259,17 @@
}
}
function showOtsProof(otsBytes) {
const proofWrap = document.getElementById('pqOtsProofWrap');
const proofEl = document.getElementById('pqOtsProof');
proofEl.textContent = bytesToBase64(otsBytes);
proofWrap.classList.remove('pq-hidden');
// The raw OTS proof base64 display and standalone .ots download have been
// removed from step 6. Instead, after confirmation + publish, the user is
// offered an upgraded proof archive (kind 1 + upgraded kind 9999 + confirmed
// OTS proof) via showUpgradedArchiveButton().
function showUpgradedArchiveButton() {
const wrap = document.getElementById('pqOtsArchiveWrap');
if (wrap) wrap.classList.remove('pq-hidden');
}
function hideUpgradedArchiveButton() {
const wrap = document.getElementById('pqOtsArchiveWrap');
if (wrap) wrap.classList.add('pq-hidden');
}
function startOtsPolling() {
@@ -1275,7 +1280,7 @@
// to the "Wait for Bitcoin confirmation" label so the user can see
// the page is still actively working while waiting for Bitcoin.
setStepSpinner(6);
startInlineSpinner('pqOtsConfirmSpinner');
startOtsConfirmSpinner();
async function poll() {
pollCount++;
@@ -1290,8 +1295,6 @@
proofCarrierEvent: pqEvent,
kind1Event
});
showOtsProof(pendingOtsBytes);
// Run full cryptographic verification: parse the proof, bind the
// target digest to the event's sha256 tag, walk the Merkle path,
// and check the block header against a Bitcoin API.
@@ -1308,7 +1311,7 @@
: verifyResult.trustMode || 'unknown';
otsLog(`Poll ${pollCount}: Bitcoin attestation VERIFIED (block ${att ? att.height : '?'}, mined ${date} UTC, trust: ${trustLabel}). Proof: ${pendingOtsBytes.length} bytes.`);
setKeyIcon('pqOtsConfirm', 'Done');
stopInlineSpinner();
stopOtsConfirmSpinner();
confirmDetail.textContent = `Verified on Bitcoin (block ${att ? att.height : '?'}, ${trustLabel})`;
if (otsPollInterval) { clearInterval(otsPollInterval); otsPollInterval = null; }
@@ -1385,6 +1388,7 @@
setStatus(otsStatus, 'success', `Upgraded proof carrier published to ${successCount} relays. Timestamping complete.`);
setStepDone(6);
setStepActive(7);
showUpgradedArchiveButton();
return validatedEvent;
} catch (error) {
otsLog(`ERROR publishing upgraded event: ${error.message}`);
@@ -1393,16 +1397,28 @@
}
}
// OTS event listeners
document.getElementById('pqOtsDownloadBtn').addEventListener('click', () => {
if (pendingOtsBytes) {
const blob = new Blob([pendingOtsBytes], { type: 'application/octet-stream' });
// OTS event listener: download the upgraded proof archive (kind 1 event +
// upgraded kind 9999 proof carrier + confirmed OTS proof) after Bitcoin
// confirmation and publish.
document.getElementById('pqOtsArchiveBtn').addEventListener('click', () => {
if (!kind1Event || !pqEvent || !pendingOtsBytes) {
console.warn('[post-quantum] Cannot build upgraded archive: missing kind1Event, proofCarrier, or OTS proof');
return;
}
try {
const archiveJson = buildProofArchive(kind1Event, pqEvent, pendingOtsBytes);
const blob = new Blob([archiveJson], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${pqEvent.id.substring(0, 16)}.ots`;
a.download = `nostr-pq-proof-${pqEvent.id.substring(0, 12)}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
console.log('[post-quantum] Upgraded proof archive downloaded');
} catch (e) {
console.error('[post-quantum] Failed to build upgraded archive:', e);
}
});
+145 -8
View File
@@ -22,19 +22,26 @@
================================================================ */
const tabRelay = document.getElementById('pqTabRelay');
const tabPaste = document.getElementById('pqTabPaste');
const tabSignIn = document.getElementById('pqTabSignIn');
const panelRelay = document.getElementById('pqPanelRelay');
const panelPaste = document.getElementById('pqPanelPaste');
const panelSignIn = document.getElementById('pqPanelSignIn');
function switchTab(which) {
const isRelay = which === 'relay';
const isPaste = which === 'paste';
const isSignIn = which === 'signin';
tabRelay.classList.toggle('pq-tab-active', isRelay);
tabPaste.classList.toggle('pq-tab-active', !isRelay);
tabPaste.classList.toggle('pq-tab-active', isPaste);
tabSignIn.classList.toggle('pq-tab-active', isSignIn);
panelRelay.classList.toggle('pq-tab-panel-active', isRelay);
panelPaste.classList.toggle('pq-tab-panel-active', !isRelay);
panelPaste.classList.toggle('pq-tab-panel-active', isPaste);
panelSignIn.classList.toggle('pq-tab-panel-active', isSignIn);
}
tabRelay.addEventListener('click', () => switchTab('relay'));
tabPaste.addEventListener('click', () => switchTab('paste'));
tabSignIn.addEventListener('click', () => switchTab('signin'));
/* ================================================================
SHARED DOM + STATE
@@ -607,6 +614,141 @@
}
});
/* ================================================================
AUTHENTICATION (nostr-login-lite / NIP-07)
================================================================ */
async function initNostrLoginLite() {
if (!window.NOSTR_LOGIN_LITE) return false;
if (!window.NOSTR_LOGIN_LITE._initialized) {
await window.NOSTR_LOGIN_LITE.init({
methods: { extension: true, local: true, nip46: true, seedphrase: true, nsigner: true }
});
}
return true;
}
// Fill the pubkey field and auto-query relays for the signed-in user's
// kind 9999 event. Switches to the Query Relay tab so the user sees the
// results. Returns the pubkey hex on success, null otherwise.
async function queryAsSignedInUser() {
if (!window.nostr || !window.nostr.getPublicKey) {
throw new Error('No Nostr signer available. Install a NIP-07 browser extension (nos2x, Alby) or use nostr-login-lite.');
}
const pubkeyHex = await window.nostr.getPublicKey();
if (!pubkeyHex) throw new Error('No pubkey returned by signer.');
console.log('[verify] Signed-in pubkey:', pubkeyHex);
const pubkeyInput = document.getElementById('pqPubkeyInput');
const npub = hexToNpub(pubkeyHex);
pubkeyInput.value = npub || pubkeyHex;
// Show the query tab so the user sees the results
switchTab('relay');
queryBtn.click();
return pubkeyHex;
}
/* ================================================================
TAB 3: SIGN IN
================================================================ */
const signInBtn = document.getElementById('pqSignInBtn');
const signInError = document.getElementById('pqSignInError');
const signInStatus = document.getElementById('pqSignInStatus');
signInBtn.addEventListener('click', async () => {
signInBtn.disabled = true;
signInError.innerHTML = '';
setStatus(signInStatus, 'info', 'Signing in...');
try {
if (window.NOSTR_LOGIN_LITE) {
await initNostrLoginLite();
// Try a restored session first
if (window.nostr) {
try {
const pk = await window.nostr.getPublicKey();
if (pk) {
await queryAsSignedInUser();
setStatus(signInStatus, 'success', 'Signed in. Querying your event...');
return;
}
} catch (e) {
console.log('[verify] Not yet authenticated, launching login modal...');
}
}
// Launch the login modal and wait for auth events
const pubkeyHex = await new Promise((resolve, reject) => {
let settled = false;
const timeout = setTimeout(() => {
if (!settled) { settled = true; cleanup(); reject(new Error('Login timed out')); }
}, 120000);
async function tryGetPubkey() {
if (settled) return;
if (window.nostr) {
try {
const pk = await window.nostr.getPublicKey();
if (pk && !settled) { settled = true; cleanup(); resolve(pk); }
} catch (e) { console.log('[verify] getPublicKey not ready:', e.message); }
}
}
function authHandler(e) {
if (e.type === 'nlMethodSelected' && e.detail && e.detail.pubkey) {
if (!settled) { settled = true; cleanup(); resolve(e.detail.pubkey); }
return;
}
tryGetPubkey();
setTimeout(tryGetPubkey, 200);
setTimeout(tryGetPubkey, 500);
setTimeout(tryGetPubkey, 1000);
setTimeout(tryGetPubkey, 2000);
}
function cleanup() {
window.removeEventListener('nlAuth', authHandler);
window.removeEventListener('nlAuthRestored', authHandler);
window.removeEventListener('nlMethodSelected', authHandler);
window.removeEventListener('nlAuthComplete', authHandler);
clearTimeout(timeout);
}
window.addEventListener('nlAuth', authHandler);
window.addEventListener('nlAuthRestored', authHandler);
window.addEventListener('nlMethodSelected', authHandler);
window.addEventListener('nlAuthComplete', authHandler);
try {
const result = window.NOSTR_LOGIN_LITE.launch();
if (result && typeof result.then === 'function') {
result.then(() => {
tryGetPubkey(); setTimeout(tryGetPubkey, 200); setTimeout(tryGetPubkey, 500);
}).catch(e => { if (!settled) { settled = true; cleanup(); reject(e); } });
}
} catch (e) {
if (!settled) { settled = true; cleanup(); reject(e); }
}
});
// Fill the pubkey field with the launched-session pubkey and query
const pubkeyInput = document.getElementById('pqPubkeyInput');
const npub = hexToNpub(pubkeyHex);
pubkeyInput.value = npub || pubkeyHex;
switchTab('relay');
queryBtn.click();
setStatus(signInStatus, 'success', 'Signed in. Querying your event...');
} else {
// No nostr-login-lite — fall back to a plain NIP-07 extension
await queryAsSignedInUser();
setStatus(signInStatus, 'success', 'Signed in. Querying your event...');
}
} catch (error) {
console.error('[verify] Sign-in failed:', error);
setStatus(signInError, 'error', error.message);
setStatus(signInStatus, 'error', 'Sign-in failed.');
} finally {
signInBtn.disabled = false;
}
});
/* ================================================================
AUTO-DETECT SIGNED-IN USER
On page load, check for a NIP-07 signer / nostr-login-lite. If the
@@ -614,14 +756,9 @@
relays for their kind 9999 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 }
});
}
await initNostrLoginLite();
} catch (e) {
console.log('[verify] nostr-login-lite init failed:', e.message);
}
+3 -3
View File
@@ -1,5 +1,5 @@
{
"VERSION": "v0.0.33",
"VERSION_NUMBER": "0.0.33",
"BUILD_DATE": "2026-07-26T15:58:51.892Z"
"VERSION": "v0.0.35",
"VERSION_NUMBER": "0.0.35",
"BUILD_DATE": "2026-07-27T17:03:35.968Z"
}
+25 -19
View File
@@ -254,35 +254,19 @@
<div class="pq-card">
<div class="pq-card-title">Verify Post-Quantum Key Link</div>
<div class="pq-info-text" style="color: var(--accent-color); font-size: 13px;">
<strong>Note:</strong> Passing these checks confirms the keys are linked and the signatures
are valid — it does not mean the identity is post-quantum secure. The Bitcoin timestamp check
relies on a public block explorer rather than running a full Bitcoin node, so a compromised
explorer could fool it.
</div>
<div class="pq-info-text">
You can check a post-quantum key-link event two ways: look it up on a relay by pubkey, or
paste the event JSON directly. The event you're checking (kind 9999) wraps a public
announcement (kind 1) and carries a Bitcoin timestamp proof. Verification checks each piece
separately and reports the results below: both Nostr signatures, the link between the two
events, each post-quantum signature (ML-DSA-44, ML-DSA-65, SLH-DSA-128s, Falcon-512 —
ML-KEM-768 is for encryption, so it has no signature to check), that all required algorithms
are present, that the event really belongs to the claimed identity, and the Bitcoin timestamp.
</div>
<div class="pq-info-text">
<a href="./" class="pq-link">Back to preparation page</a>
</div>
<div class="pq-tabs">
<div class="pq-tab pq-tab-active" id="pqTabRelay">Query Relay</div>
<div class="pq-tab" id="pqTabPaste">Paste Event JSON</div>
<div class="pq-tab" id="pqTabSignIn">Sign In</div>
</div>
<!-- TAB 1: Query Relay -->
<div class="pq-tab-panel pq-tab-panel-active" id="pqPanelRelay">
<div class="pq-info-text">
Enter a Nostr pubkey (hex or npub) and one or more relay URLs. The page will query the
relay(s) for the latest kind 9999 event from that pubkey and verify it.
relay(s) for the latest kind 9999 event from that pubkey and verify the signatures are
valid, and whether it has been submitted to Open Time Stamps.
</div>
<input type="text" class="pq-input" id="pqPubkeyInput"
placeholder="Pubkey (hex or npub), e.g. 3bf0c63f869a8972... or npub1..." />
@@ -319,6 +303,17 @@
<input type="file" id="pqArchiveFileInput" accept="application/json,.json" style="margin-top: 6px; font-size: 13px;" />
</div>
<!-- TAB 3: SIGN IN -->
<div class="pq-tab-panel" id="pqPanelSignIn">
<div class="pq-info-text">
Sign in with your Nostr identity (NIP-07 extension or nostr-login-lite) to look up your
own kind 9999 event automatically.
</div>
<div id="pqSignInError"></div>
<button class="pq-button" id="pqSignInBtn">Sign In</button>
<div id="pqSignInStatus"></div>
</div>
<!-- Shared results area -->
<div id="pqResultsWrap" style="display: none;">
<div class="pq-info-text" style="margin-top: 15px; margin-bottom: 5px;">
@@ -346,6 +341,17 @@
</div>
<div id="pqOtsUpgradeStatus"></div>
</div>
<hr style="border: var(--border); margin: 25px 0 15px;" />
<div class="pq-info-text" style="color: var(--accent-color); font-size: 13px;">
<strong>Note:</strong> Passing every check does not mean that your current nostr identity
is now post-quantum secure. This tool is the preparation step. Full quantum safety for
day-to-day Nostr use still requires companion protocols that are not yet built.
</div>
<div class="pq-info-text">
<a href="./" class="pq-link">Back to preparation page</a>
</div>
</div>
</div>