mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
[auth] enforce access control on silent NIP-46 session restore
scheduleStoredRemoteSignerRestore reconnected a stored remote-signer session on
startup via useStoredRemoteSigner({ silent: true }) with NO validator, so a
pubkey that had since been blocked could silently reconnect — the same
access-control bypass class as the nsec fix. It now passes an access-control
validator (waits for the admin lists to load, then checks canAccess); since
useStoredRemoteSigner already clears the stored session when the validator
rejects, a blocked pubkey's session is forgotten on restore. The connect and
explicit "use stored" paths already enforced this; only the silent startup
restore was missing it.
Tests: the restore supplies a validator function; the validator allows
permitted pubkeys and rejects blocked ones. Mutation-verified (dropping the
validator fails the test).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
4a38e27150
commit
ff5c641951
2 files changed
+114
-1
No files matched your search
@@ -58,6 +58,7 @@ import { createPrivateKeyCipherClosures } from "../signerHelpers.js";
|
||||
import { queueSignEvent } from "../signRequestQueue.js";
|
||||
import { signEventWithPrivateKey } from "../publishHelpers.js";
|
||||
import { ensureNostrTools, getCachedNostrTools } from "../toolkit.js";
|
||||
import { accessControl } from "../../accessControl.js";
|
||||
|
||||
export function resolveSignerCapabilities(signer) {
|
||||
const fallback = {
|
||||
@@ -1213,6 +1214,29 @@ export class SignerManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Access-control gate for restoring a stored signer. Waits for the admin
|
||||
// lists to load (so the decision is accurate on a cold start) and throws if
|
||||
// the pubkey is no longer allowed, matching the fresh-login/connect paths.
|
||||
// useStoredRemoteSigner clears the stored session when this rejects.
|
||||
buildAccessControlValidator() {
|
||||
return async (pubkey) => {
|
||||
try {
|
||||
if (typeof accessControl.waitForReady === "function") {
|
||||
await accessControl.waitForReady();
|
||||
}
|
||||
} catch (error) {
|
||||
// If readiness can't be confirmed, fall through to the live check.
|
||||
}
|
||||
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 only.");
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
async scheduleStoredRemoteSignerRestore() {
|
||||
const stored = readStoredNip46Session();
|
||||
if (!stored) {
|
||||
@@ -1221,7 +1245,13 @@ export class SignerManager {
|
||||
|
||||
const decryptAndConnect = async () => {
|
||||
try {
|
||||
await this.useStoredRemoteSigner({ silent: true });
|
||||
// Enforce access control on the silent startup restore too — without a
|
||||
// validator a since-blocked pubkey's stored session would reconnect
|
||||
// unchecked.
|
||||
await this.useStoredRemoteSigner({
|
||||
silent: true,
|
||||
validator: this.buildAccessControlValidator(),
|
||||
});
|
||||
} catch (err) {
|
||||
devLogger.warn("Failed to restore remote signer", err);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// The silent NIP-46 session restore on startup must enforce access control,
|
||||
// so a since-blocked pubkey's stored remote-signer session can't reconnect
|
||||
// unchecked.
|
||||
|
||||
import "./test-helpers/setup-localstorage.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { SignerManager } from "../js/nostr/managers/SignerManager.js";
|
||||
import { accessControl } from "../js/accessControl.js";
|
||||
|
||||
const NIP46_SESSION_KEY = "bitvid:nip46:session:v1";
|
||||
|
||||
function seedStoredSession() {
|
||||
localStorage.setItem(
|
||||
NIP46_SESSION_KEY,
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
remotePubkey: "a".repeat(64),
|
||||
clientPrivateKey: "b".repeat(64),
|
||||
relays: ["wss://relay.example"],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
test("scheduleStoredRemoteSignerRestore passes an access-control validator", async () => {
|
||||
seedStoredSession();
|
||||
const manager = new SignerManager({ relays: ["wss://relay.example"] });
|
||||
|
||||
let capturedOptions = null;
|
||||
manager.useStoredRemoteSigner = async (options) => {
|
||||
capturedOptions = options;
|
||||
return { ok: true, pubkey: "a".repeat(64) };
|
||||
};
|
||||
|
||||
await manager.scheduleStoredRemoteSignerRestore();
|
||||
// schedule fires the restore without awaiting it; let the microtask run.
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
assert.ok(capturedOptions, "the stored signer restore was attempted");
|
||||
assert.equal(
|
||||
capturedOptions.silent,
|
||||
true,
|
||||
"startup restore stays silent",
|
||||
);
|
||||
assert.equal(
|
||||
typeof capturedOptions.validator,
|
||||
"function",
|
||||
"a validator must be supplied so access control is enforced on restore",
|
||||
);
|
||||
|
||||
localStorage.removeItem(NIP46_SESSION_KEY);
|
||||
});
|
||||
|
||||
test("the restore validator allows permitted pubkeys and rejects blocked ones", async () => {
|
||||
const manager = new SignerManager({ relays: [] });
|
||||
const validator = manager.buildAccessControlValidator();
|
||||
|
||||
const origWaitForReady = accessControl.waitForReady;
|
||||
const origCanAccess = accessControl.canAccess;
|
||||
const origIsBlacklisted = accessControl.isBlacklisted;
|
||||
accessControl.waitForReady = async () => {};
|
||||
|
||||
try {
|
||||
accessControl.canAccess = () => true;
|
||||
accessControl.isBlacklisted = () => false;
|
||||
await assert.doesNotReject(
|
||||
() => validator("a".repeat(64)),
|
||||
"an allowed pubkey passes the validator",
|
||||
);
|
||||
|
||||
accessControl.canAccess = () => false;
|
||||
accessControl.isBlacklisted = () => true;
|
||||
await assert.rejects(
|
||||
() => validator("c".repeat(64)),
|
||||
/blocked/i,
|
||||
"a blocked pubkey is rejected so the stored session is forgotten",
|
||||
);
|
||||
} finally {
|
||||
accessControl.waitForReady = origWaitForReady;
|
||||
accessControl.canAccess = origCanAccess;
|
||||
accessControl.isBlacklisted = origIsBlacklisted;
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user