mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-04 11:52:25 +00:00
[ui] #36: clear, actionable storage-unlock error for a locked nsec session after reload
Root cause (user-confirmed repro): a persisted "remember this key" nsec session
restores the logged-in pubkey + UI on reload, but NOT the in-memory signer (the
private key is only passphrase-encrypted). Storage unlock then has no signer, and
the fallback (ensureActiveSignerForPubkey) can only rebuild an EXTENSION signer —
so an nsec user dead-ended on the misleading "No active signer found. Please login."
even though they still look logged in. The crypto layer itself is correct: an
empirical repro shows storageService.unlock round-trips master-key encrypt/decrypt
fine with real nsec cipher closures.
Fix: detect the locked persisted-nsec case (getLockedStoredNsecSession via
getStoredSessionActorMetadata, scoped to the account being unlocked) and surface an
actionable error/status ("re-enter your passphrase from the Login menu to unlock
your saved key, then unlock storage") in both the Unlock-button flow and the passive
Storage-pane status text, instead of the generic no-signer / no-decryptor messages.
New error code storage-unlock-locked-nsec-session.
Tests: tests/storage-unlock-locked-nsec.test.mjs (6 behavioral). Build + lint clean;
existing storage-service tests still 14/14.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
eb4d071203
commit
a669040740
3 files changed
+179
-6
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { devLogger } from "../../utils/logger.js";
|
||||
import { devLogger, userLogger } from "../../utils/logger.js";
|
||||
import { PROVIDERS } from "../../services/storageService.js";
|
||||
import {
|
||||
prepareS3Connection,
|
||||
@@ -170,6 +170,11 @@ export class ProfileStorageController {
|
||||
} else if (this.storageUnlockFailure?.message) {
|
||||
this.storageStatusText.textContent = `Locked (${this.storageUnlockFailure.message})`;
|
||||
this.storageStatusText.className = "text-xs text-status-danger";
|
||||
} else if (this.getLockedStoredNsecSession(pubkey)) {
|
||||
// Saved nsec key not yet re-unlocked after reload — point the user at the fix.
|
||||
this.storageStatusText.textContent =
|
||||
"Locked — re-unlock your saved key (Login → passphrase) to manage storage.";
|
||||
this.storageStatusText.className = "text-xs text-status-warning";
|
||||
} else {
|
||||
this.storageStatusText.textContent = "Locked";
|
||||
this.storageStatusText.className = "text-xs text-status-warning";
|
||||
@@ -451,6 +456,8 @@ export class ProfileStorageController {
|
||||
return "Storage unlock requires extension encryption permission. Approve the prompt, then retry unlock.";
|
||||
case "storage-unlock-no-decryptor":
|
||||
return "Your signer cannot decrypt storage keys. Use a signer with NIP-44 or NIP-04 decrypt support.";
|
||||
case "storage-unlock-locked-nsec-session":
|
||||
return "Your saved key is locked after reloading the page. Open the Login menu and re-enter your passphrase to unlock your saved key, then unlock storage.";
|
||||
case "storage-unlock-decrypt-failed":
|
||||
return "Unable to decrypt your saved storage key. Retry unlock and confirm the active account matches.";
|
||||
default:
|
||||
@@ -481,6 +488,47 @@ export class ProfileStorageController {
|
||||
this.storageUnlockFailure = null;
|
||||
}
|
||||
|
||||
// After a page reload, a persisted ("remember this key") nsec session restores the
|
||||
// logged-in pubkey + UI but NOT the in-memory signer — the private key is held only
|
||||
// passphrase-encrypted. Storage unlock then has no signer (or a decrypt-less stub),
|
||||
// and the generic "No active signer / cannot decrypt" errors are misleading because
|
||||
// the user still looks logged in. Detect this exact case (a saved nsec key for the
|
||||
// account we're unlocking) so we can tell them to re-unlock their saved key instead.
|
||||
getLockedStoredNsecSession(pubkey) {
|
||||
const client = this.mainController.services?.nostrClient;
|
||||
if (!client || typeof client.getStoredSessionActorMetadata !== "function") {
|
||||
return null;
|
||||
}
|
||||
let meta = null;
|
||||
try {
|
||||
meta = client.getStoredSessionActorMetadata();
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
if (!meta || meta.hasEncryptedKey !== true || meta.source !== "nsec") {
|
||||
return null;
|
||||
}
|
||||
const metaPubkey = this.mainController.normalizeHexPubkey(meta.pubkey);
|
||||
// Only treat it as "this account is locked" when the saved key is for the pubkey
|
||||
// we're actually trying to unlock (don't hijack a different-account situation).
|
||||
if (pubkey && metaPubkey && metaPubkey !== pubkey) {
|
||||
return null;
|
||||
}
|
||||
return meta;
|
||||
}
|
||||
|
||||
reportLockedNsecSession() {
|
||||
userLogger.info(
|
||||
"[storage-unlock] saved nsec key is locked (no in-memory signer after reload); prompting re-unlock.",
|
||||
);
|
||||
const error = new Error(
|
||||
"Your saved key is locked after reloading the page. Re-enter your passphrase from the Login menu to unlock it, then unlock storage.",
|
||||
);
|
||||
error.code = "storage-unlock-locked-nsec-session";
|
||||
this.setStorageUnlockFailureState(error);
|
||||
this.mainController.showError(this.getStorageUnlockFailureMessage(error));
|
||||
}
|
||||
|
||||
async requestStorageUnlockPermissions() {
|
||||
const client = this.mainController.services?.nostrClient;
|
||||
if (!client || typeof client.ensureExtensionPermissions !== "function") {
|
||||
@@ -521,6 +569,10 @@ export class ProfileStorageController {
|
||||
}
|
||||
|
||||
if (!signer) {
|
||||
if (this.getLockedStoredNsecSession(pubkey)) {
|
||||
this.reportLockedNsecSession();
|
||||
return;
|
||||
}
|
||||
this.mainController.showError("No active signer found. Please login.");
|
||||
return;
|
||||
}
|
||||
@@ -592,6 +644,12 @@ export class ProfileStorageController {
|
||||
typeof signer?.decrypt === "function";
|
||||
|
||||
if (!hasNip44Decrypt && !hasNip04Decrypt) {
|
||||
// A restored-but-locked persisted nsec session can leave a decrypt-less stub
|
||||
// signer; route it to the re-unlock guidance rather than the generic message.
|
||||
if (this.getLockedStoredNsecSession(pubkey)) {
|
||||
this.reportLockedNsecSession();
|
||||
return;
|
||||
}
|
||||
const missingDecryptError = new Error(
|
||||
"This signer cannot decrypt storage keys (NIP-44/NIP-04 missing)."
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user