228 lines
5.5 KiB
JavaScript
228 lines
5.5 KiB
JavaScript
import { Mutex } from "async-mutex";
|
|
import {
|
|
getEventHash,
|
|
getPublicKey,
|
|
getSignature,
|
|
nip19,
|
|
validateEvent,
|
|
} from "nostr-tools";
|
|
import { nip04 } from "nostr-tools";
|
|
import browser from "webextension-polyfill";
|
|
|
|
import {
|
|
NO_PERMISSIONS_REQUIRED,
|
|
getPermissionStatus,
|
|
showNotification,
|
|
updatePermission,
|
|
} from "./common";
|
|
|
|
const { encrypt, decrypt } = nip04;
|
|
|
|
let openPrompt = null;
|
|
const promptMutex = new Mutex();
|
|
let releasePromptMutex = () => {};
|
|
|
|
browser.runtime.onInstalled.addListener((_, __, reason) => {
|
|
if (reason === "install") browser.runtime.openOptionsPage();
|
|
});
|
|
|
|
browser.runtime.onMessage.addListener(async (req, sender) => {
|
|
const { prompt } = req;
|
|
|
|
if (prompt) {
|
|
handlePromptMessage(req, sender);
|
|
} else {
|
|
return handleContentScriptMessage(req);
|
|
}
|
|
});
|
|
|
|
browser.runtime.onMessageExternal.addListener(
|
|
async ({ type, params }, sender) => {
|
|
const extensionId = new URL(sender.url).host;
|
|
return handleContentScriptMessage({ type, params, host: extensionId });
|
|
},
|
|
);
|
|
|
|
browser.windows.onRemoved.addListener((windowId) => {
|
|
if (openPrompt) {
|
|
// calling this with a simple "no" response will not store anything, so it's fine
|
|
// it will just return a failure
|
|
handlePromptMessage({ accept: false }, null);
|
|
}
|
|
});
|
|
|
|
async function handleContentScriptMessage({ type, params, host }) {
|
|
if (NO_PERMISSIONS_REQUIRED[type]) {
|
|
// authorized, and we won't do anything with private key here, so do a separate handler
|
|
switch (type) {
|
|
case "replaceURL": {
|
|
const { protocol_handler: ph } = await browser.storage.local.get([
|
|
"protocol_handler",
|
|
]);
|
|
if (!ph) return false;
|
|
|
|
const { url } = params;
|
|
const raw = url.split("nostr:")[1];
|
|
const { type, data } = nip19.decode(raw);
|
|
const replacements = {
|
|
raw,
|
|
hrp: type,
|
|
hex:
|
|
type === "npub" || type === "note"
|
|
? data
|
|
: type === "nprofile"
|
|
? data.pubkey
|
|
: type === "nevent"
|
|
? data.id
|
|
: null,
|
|
p_or_e: { npub: "p", note: "e", nprofile: "p", nevent: "e" }[type],
|
|
u_or_n: { npub: "u", note: "n", nprofile: "u", nevent: "n" }[type],
|
|
relay0: type === "nprofile" ? data.relays[0] : null,
|
|
relay1: type === "nprofile" ? data.relays[1] : null,
|
|
relay2: type === "nprofile" ? data.relays[2] : null,
|
|
};
|
|
let result = ph;
|
|
// biome-ignore lint/complexity/noForEach: TODO: fix this
|
|
Object.entries(replacements).forEach(([pattern, value]) => {
|
|
result = result.replace(new RegExp(`{ *${pattern} *}`, "g"), value);
|
|
});
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
return;
|
|
} else {
|
|
// acquire mutex here before reading policies
|
|
releasePromptMutex = await promptMutex.acquire();
|
|
|
|
const allowed = await getPermissionStatus(
|
|
host,
|
|
type,
|
|
type === "signEvent" ? params.event : undefined,
|
|
);
|
|
|
|
if (allowed === true) {
|
|
// authorized, proceed
|
|
releasePromptMutex();
|
|
showNotification(host, allowed, type, params);
|
|
} else if (allowed === false) {
|
|
// denied, just refuse immediately
|
|
releasePromptMutex();
|
|
showNotification(host, allowed, type, params);
|
|
return {
|
|
error: "denied",
|
|
};
|
|
} else {
|
|
// ask for authorization
|
|
try {
|
|
const id = Math.random().toString().slice(4);
|
|
const qs = new URLSearchParams({
|
|
host,
|
|
id,
|
|
params: JSON.stringify(params),
|
|
type,
|
|
});
|
|
|
|
// prompt will be resolved with true or false
|
|
const accept = await new Promise((resolve, reject) => {
|
|
openPrompt = { resolve, reject };
|
|
const url = `${browser.runtime.getURL(
|
|
"prompt.html",
|
|
)}?${qs.toString()}`;
|
|
|
|
if (browser.windows) {
|
|
browser.windows.create({
|
|
url,
|
|
type: "popup",
|
|
width: 600,
|
|
height: 600,
|
|
});
|
|
} else {
|
|
browser.tabs.create({
|
|
url,
|
|
active: true,
|
|
});
|
|
}
|
|
});
|
|
|
|
// denied, stop here
|
|
if (!accept) return { error: "denied" };
|
|
} catch (err) {
|
|
// errored, stop here
|
|
releasePromptMutex();
|
|
return {
|
|
error: `error: ${err}`,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// if we're here this means it was accepted
|
|
const results = await browser.storage.local.get("private_key");
|
|
if (!results || !results.private_key) {
|
|
return { error: "no private key found" };
|
|
}
|
|
|
|
const sk = results.private_key;
|
|
|
|
try {
|
|
switch (type) {
|
|
case "getPublicKey": {
|
|
return getPublicKey(sk);
|
|
}
|
|
case "getRelays": {
|
|
const results = await browser.storage.local.get("relays");
|
|
return results.relays || {};
|
|
}
|
|
case "signEvent": {
|
|
const { event } = params;
|
|
|
|
if (!event.pubkey) event.pubkey = getPublicKey(sk);
|
|
if (!event.id) event.id = getEventHash(event);
|
|
if (!validateEvent(event))
|
|
return { error: { message: "invalid event" } };
|
|
|
|
event.sig = await getSignature(event, sk);
|
|
return event;
|
|
}
|
|
case "nip04.encrypt": {
|
|
const { peer, plaintext } = params;
|
|
return encrypt(sk, peer, plaintext);
|
|
}
|
|
case "nip04.decrypt": {
|
|
const { peer, ciphertext } = params;
|
|
return decrypt(sk, peer, ciphertext);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
return { error: { message: error.message, stack: error.stack } };
|
|
}
|
|
}
|
|
|
|
async function handlePromptMessage({ host, type, accept, conditions }, sender) {
|
|
// return response
|
|
openPrompt?.resolve?.(accept);
|
|
|
|
// update policies
|
|
if (conditions) {
|
|
await updatePermission(host, type, accept, conditions);
|
|
}
|
|
|
|
// cleanup this
|
|
openPrompt = null;
|
|
|
|
// release mutex here after updating policies
|
|
releasePromptMutex();
|
|
|
|
// close prompt
|
|
if (sender) {
|
|
if (browser.windows) {
|
|
browser.windows.remove(sender.tab.windowId);
|
|
} else {
|
|
// Android Firefox
|
|
browser.tabs.remove(sender.tab.id);
|
|
}
|
|
}
|
|
}
|