mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-30 01:12:39 +00:00
Audit of the nsec (direct private key) login found the client's
registerPrivateKeySigner silently ignored the access-control validator AND
the persist/passphrase options, so:
- Access-control bypass (security): nsec logins skipped the blacklist/whitelist
gate that NIP-07 and NIP-46 logins enforce. registerPrivateKeySigner now runs
the validator BEFORE activating the signer and forgets a since-blocked stored
key on denial.
- "Remember this key" never worked: it persisted nothing (the payload used a
shape persistSessionActor rejects), and there was no way to detect or unlock a
saved key — getStoredSessionActorMetadata and unlockStoredSessionActor were
referenced by the UI/provider but defined nowhere. Implemented both on the
client: persist now writes the canonical { privateKeyEncrypted, encryption }
shape (PBKDF2 250k + AES-GCM, already in place), the login modal can detect a
saved key, and unlock decrypts with the passphrase and re-activates via the
same proven createNsecAdapter path. The nsec provider now passes the
access-control validator to the unlock path too.
- Fixed misleading copy: the provider claimed BIP-39 seed support, but only
hex/nsec are accepted.
Untouched-but-verified: decode/validate (hex64 + nsec1), logout clears the
in-memory key, no key logging.
Tests (tests/nsec-login.test.mjs): a denied validator rejects login and
activates no signer; persist -> unlock round-trips the same pubkey (wrong
passphrase fails); unlock re-checks access control and forgets a blocked key.
The validator-enforcement and persist-shape fixes are both mutation-verified.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
167 lines
4.7 KiB
JavaScript
167 lines
4.7 KiB
JavaScript
import { accessControl } from "../../accessControl.js";
|
|
|
|
const PROVIDER_ID = "nsec";
|
|
const PROVIDER_LABEL = "nsec or seed (direct private key)";
|
|
const PROVIDER_DESCRIPTION =
|
|
"Paste an nsec or hex private key. Keep it encrypted on this device if you choose.";
|
|
const PROVIDER_CAPABILITIES = Object.freeze([
|
|
Object.freeze({
|
|
id: "signing",
|
|
label: "Signs locally",
|
|
variant: "warning",
|
|
}),
|
|
Object.freeze({
|
|
id: "session",
|
|
label: "Encrypted backup optional",
|
|
variant: "neutral",
|
|
}),
|
|
]);
|
|
const PROVIDER_BUTTON = Object.freeze({
|
|
variant: "ghost",
|
|
});
|
|
const PROVIDER_MESSAGES = Object.freeze({
|
|
loading: "Unlocking direct key…",
|
|
slow: "Still deriving your key…",
|
|
error: "Failed to unlock the direct key. Check your secret and try again.",
|
|
});
|
|
|
|
function normalizePubkey(result) {
|
|
if (typeof result === "string") {
|
|
return result;
|
|
}
|
|
|
|
if (!result || typeof result !== "object") {
|
|
return "";
|
|
}
|
|
|
|
if (typeof result.pubkey === "string") {
|
|
return result.pubkey;
|
|
}
|
|
|
|
if (typeof result.publicKey === "string") {
|
|
return result.publicKey;
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
function normalizeOptions(options) {
|
|
if (!options || typeof options !== "object") {
|
|
return {
|
|
secret: "",
|
|
persist: false,
|
|
passphrase: "",
|
|
unlockStored: false,
|
|
};
|
|
}
|
|
|
|
return {
|
|
secret: typeof options.secret === "string" ? options.secret : "",
|
|
persist: options.persist === true,
|
|
passphrase: typeof options.passphrase === "string" ? options.passphrase : "",
|
|
unlockStored: options.unlockStored === true,
|
|
};
|
|
}
|
|
|
|
export default {
|
|
id: PROVIDER_ID,
|
|
label: PROVIDER_LABEL,
|
|
description: PROVIDER_DESCRIPTION,
|
|
eyebrow: "Advanced",
|
|
tone: "warning",
|
|
badgeVariant: "warning",
|
|
capabilities: PROVIDER_CAPABILITIES,
|
|
button: PROVIDER_BUTTON,
|
|
messages: PROVIDER_MESSAGES,
|
|
errorMessage: PROVIDER_MESSAGES.error,
|
|
async login({ nostrClient, options } = {}) {
|
|
if (!nostrClient || typeof nostrClient !== "object") {
|
|
const error = new Error("Direct key login is not available.");
|
|
error.code = "provider-unavailable";
|
|
throw error;
|
|
}
|
|
|
|
if (
|
|
typeof nostrClient.derivePrivateKeyFromSecret !== "function" ||
|
|
typeof nostrClient.registerPrivateKeySigner !== "function"
|
|
) {
|
|
const error = new Error("Direct key login is not supported in this environment.");
|
|
error.code = "provider-unavailable";
|
|
throw error;
|
|
}
|
|
|
|
const normalized = normalizeOptions(options);
|
|
|
|
// Access gate enforced before any signer is activated, for both fresh
|
|
// logins and stored-key unlocks.
|
|
const validator = (pubkey) => {
|
|
if (!accessControl.canAccess(pubkey)) {
|
|
if (accessControl.isBlacklisted(pubkey)) {
|
|
throw new Error("Your account has been blocked on this platform.");
|
|
}
|
|
throw new Error("Access restricted to admins and moderators users only.");
|
|
}
|
|
return true;
|
|
};
|
|
|
|
if (normalized.unlockStored) {
|
|
if (typeof nostrClient.unlockStoredSessionActor !== "function") {
|
|
const error = new Error("Stored key unlock is not supported.");
|
|
error.code = "unlock-unavailable";
|
|
throw error;
|
|
}
|
|
|
|
const suppliedPassphrase = normalized.passphrase;
|
|
if (typeof suppliedPassphrase !== "string" || !suppliedPassphrase.trim()) {
|
|
const error = new Error("A passphrase is required to unlock the saved key.");
|
|
error.code = "passphrase-required";
|
|
throw error;
|
|
}
|
|
|
|
const unlockResult = await nostrClient.unlockStoredSessionActor(
|
|
suppliedPassphrase,
|
|
{ validator },
|
|
);
|
|
const unlockedPubkey = normalizePubkey(unlockResult);
|
|
|
|
return {
|
|
authType: PROVIDER_ID,
|
|
pubkey: unlockedPubkey,
|
|
};
|
|
}
|
|
|
|
const trimmedSecret = normalized.secret.trim();
|
|
if (!trimmedSecret) {
|
|
const error = new Error("A private key is required to continue.");
|
|
error.code = "secret-required";
|
|
throw error;
|
|
}
|
|
|
|
const { privateKey, pubkey } = await nostrClient.derivePrivateKeyFromSecret(trimmedSecret);
|
|
|
|
const shouldPersist = normalized.persist === true;
|
|
const passphrase = shouldPersist ? normalized.passphrase : "";
|
|
|
|
if (shouldPersist && (!passphrase || !passphrase.trim())) {
|
|
const error = new Error("A passphrase is required to remember this key.");
|
|
error.code = "passphrase-required";
|
|
throw error;
|
|
}
|
|
|
|
const registrationResult = await nostrClient.registerPrivateKeySigner({
|
|
privateKey,
|
|
pubkey,
|
|
persist: shouldPersist,
|
|
passphrase: shouldPersist ? passphrase : undefined,
|
|
validator,
|
|
});
|
|
|
|
const registeredPubkey = normalizePubkey(registrationResult) || pubkey;
|
|
|
|
return {
|
|
authType: PROVIDER_ID,
|
|
pubkey: registeredPubkey,
|
|
};
|
|
},
|
|
};
|