migrate background related things from chrome
This commit is contained in:
@@ -5,37 +5,26 @@
|
||||
"version": "0.0.1",
|
||||
"homepage_url": "https://getgooti.com",
|
||||
"options_page": "options.html",
|
||||
"permissions": [
|
||||
"storage"
|
||||
],
|
||||
"permissions": ["storage"],
|
||||
"action": {
|
||||
"default_popup": "index.html",
|
||||
"default_icon": "gooti-with-bg.png"
|
||||
},
|
||||
"background": {
|
||||
"scripts": [
|
||||
"background.js"
|
||||
]
|
||||
"scripts": ["background.js"]
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"run_at": "document_end",
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"js": [
|
||||
"gooti-content-script.js"
|
||||
]
|
||||
"run_at": "document_start",
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["gooti-content-script.js"],
|
||||
"all_frames": true
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": [
|
||||
"gooti-extension.js"
|
||||
],
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
]
|
||||
"resources": ["gooti-extension.js"],
|
||||
"matches": ["<all_urls>"]
|
||||
}
|
||||
],
|
||||
"browser_specific_settings": {
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html data-bs-theme="dark">
|
||||
<head>
|
||||
<title>Gooti</title>
|
||||
<link rel="stylesheet" type="text/css" href="styles.css" />
|
||||
<script src="scripts.js"></script>
|
||||
<style>
|
||||
body {
|
||||
background: var(--background);
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.color-primary {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.page {
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr 60px;
|
||||
grid-template-columns: 1fr;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: var(--size);
|
||||
background: var(--background-light);
|
||||
border-radius: 8px;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.json {
|
||||
white-space: pre;
|
||||
overflow-y: auto;
|
||||
font-size: 12px;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.text {
|
||||
white-space: normal;
|
||||
overflow-y: auto;
|
||||
font-size: 12px;
|
||||
color: gray;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="sam-flex-column" style="overflow-y: auto">
|
||||
<div class="sam-text-header">
|
||||
<span id="titleSpan" style="font-weight: 400 !important"></span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
class="host-INSERT sam-align-self-center sam-text-muted"
|
||||
style="font-weight: 500"
|
||||
></span>
|
||||
|
||||
<!-- Card for getPublicKey -->
|
||||
<div id="cardGetPublicKey" class="card sam-mt sam-ml sam-mr">
|
||||
<span style="text-align: center">
|
||||
<b><span class="host-INSERT color-primary"></span></b>
|
||||
is requesting permission to<br />
|
||||
<br />
|
||||
<b class="color-primary">read your public key</b> <br />
|
||||
<br />
|
||||
<span>
|
||||
for the selected identity
|
||||
<span
|
||||
style="font-weight: 500"
|
||||
class="nick-INSERT color-primary"
|
||||
></span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Card for getRelays -->
|
||||
<div id="cardGetRelays" class="card sam-mt sam-ml sam-mr">
|
||||
<span style="text-align: center">
|
||||
<b><span class="host-INSERT color-primary"></span></b>
|
||||
is requesting permission to<br />
|
||||
<br />
|
||||
<b class="color-primary">read your relays</b> <br />
|
||||
<br />
|
||||
<span>
|
||||
for the selected identity
|
||||
<span
|
||||
style="font-weight: 500"
|
||||
class="nick-INSERT color-primary"
|
||||
></span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Card for signEvent -->
|
||||
<div id="cardSignEvent" class="card sam-mt sam-ml sam-mr">
|
||||
<span style="text-align: center">
|
||||
<b><span class="host-INSERT color-primary"></span></b>
|
||||
is requesting permission to<br />
|
||||
<br />
|
||||
<b class="color-primary">sign an event</b> (kind
|
||||
<span id="kindSpan"></span>) <br />
|
||||
<br />
|
||||
<span>
|
||||
for the selected identity
|
||||
<span
|
||||
style="font-weight: 500"
|
||||
class="nick-INSERT color-primary"
|
||||
></span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Card2 for signEvent -->
|
||||
<div id="card2SignEvent" class="card sam-mt sam-ml sam-mr">
|
||||
<div id="card2SignEvent_json" class="json"></div>
|
||||
</div>
|
||||
|
||||
<!-- Card for nip04.encrypt -->
|
||||
<div id="cardNip04Encrypt" class="card sam-mt sam-ml sam-mr">
|
||||
<span style="text-align: center">
|
||||
<b><span class="host-INSERT color-primary"></span></b>
|
||||
is requesting permission to<br />
|
||||
<br />
|
||||
<b class="color-primary">encrypt a text</b> (NIP04) <br />
|
||||
<br />
|
||||
<span>
|
||||
for the selected identity
|
||||
<span
|
||||
style="font-weight: 500"
|
||||
class="nick-INSERT color-primary"
|
||||
></span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Card2 for nip04.encrypt -->
|
||||
<div id="card2Nip04Encrypt" class="card sam-mt sam-ml sam-mr">
|
||||
<div id="card2Nip04Encrypt_text" class="text"></div>
|
||||
</div>
|
||||
|
||||
<!-- Card for nip04.decrypt -->
|
||||
<div id="cardNip04Decrypt" class="card sam-mt sam-ml sam-mr">
|
||||
<span style="text-align: center">
|
||||
<b><span class="host-INSERT color-primary"></span></b>
|
||||
is requesting permission to<br />
|
||||
<br />
|
||||
<b class="color-primary">decrypt a text</b> (NIP04) <br />
|
||||
<br />
|
||||
<span>
|
||||
for the selected identity
|
||||
<span
|
||||
style="font-weight: 500"
|
||||
class="nick-INSERT color-primary"
|
||||
></span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Card2 for nip04.decrypt -->
|
||||
<div id="card2Nip04Decrypt" class="card sam-mt sam-ml sam-mr">
|
||||
<div id="card2Nip04Decrypt_text" class="text"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!------------->
|
||||
<!-- ACTIONS -->
|
||||
<!------------->
|
||||
<div class="sam-footer-grid-2">
|
||||
<div class="btn-group">
|
||||
<button id="rejectButton" type="button" class="btn btn-secondary">
|
||||
Reject
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary dropdown-toggle dropdown-toggle-split"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="visually-hidden">Toggle Dropdown</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<button id="rejectJustOnceButton" class="dropdown-item">
|
||||
just once
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button id="approveButton" type="button" class="btn btn-primary">
|
||||
Approve
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary dropdown-toggle dropdown-toggle-split"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="visually-hidden">Toggle Dropdown</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<button id="approveJustOnceButton" class="dropdown-item" href="#">
|
||||
just once
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="prompt.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
288
projects/firefox/src/background-common.ts
Normal file
288
projects/firefox/src/background-common.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {
|
||||
BrowserSessionData,
|
||||
BrowserSyncData,
|
||||
BrowserSyncFlow,
|
||||
CryptoHelper,
|
||||
GootiMetaData,
|
||||
Identity_DECRYPTED,
|
||||
Nip07Method,
|
||||
Nip07MethodPolicy,
|
||||
NostrHelper,
|
||||
Permission_DECRYPTED,
|
||||
Permission_ENCRYPTED,
|
||||
} from '@common';
|
||||
import { Event, EventTemplate, finalizeEvent, nip04 } from 'nostr-tools';
|
||||
import { FirefoxMetaHandler } from './app/common/data/firefox-meta-handler';
|
||||
import browser from 'webextension-polyfill';
|
||||
|
||||
export const debug = function (message: any) {
|
||||
const dateString = new Date().toISOString();
|
||||
console.log(`[Gooti - ${dateString}]: ${JSON.stringify(message)}`);
|
||||
};
|
||||
|
||||
export type PromptResponse =
|
||||
| 'reject'
|
||||
| 'reject-once'
|
||||
| 'approve'
|
||||
| 'approve-once';
|
||||
|
||||
export interface PromptResponseMessage {
|
||||
id: string;
|
||||
response: PromptResponse;
|
||||
}
|
||||
|
||||
export interface BackgroundRequestMessage {
|
||||
method: Nip07Method;
|
||||
params: any;
|
||||
host: string;
|
||||
}
|
||||
|
||||
export const getBrowserSessionData = async function (): Promise<
|
||||
BrowserSessionData | undefined
|
||||
> {
|
||||
const browserSessionData = await browser.storage.session.get(null);
|
||||
if (Object.keys(browserSessionData).length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return browserSessionData as unknown as BrowserSessionData;
|
||||
};
|
||||
|
||||
export const getBrowserSyncData = async function (): Promise<
|
||||
BrowserSyncData | undefined
|
||||
> {
|
||||
const gootiMetaHandler = new FirefoxMetaHandler();
|
||||
const gootiMetaData =
|
||||
(await gootiMetaHandler.loadFullData()) as GootiMetaData;
|
||||
|
||||
let browserSyncData: BrowserSyncData | undefined;
|
||||
|
||||
if (gootiMetaData.syncFlow === BrowserSyncFlow.NO_SYNC) {
|
||||
browserSyncData = (await browser.storage.local.get(
|
||||
null
|
||||
)) as unknown as BrowserSyncData;
|
||||
} else if (gootiMetaData.syncFlow === BrowserSyncFlow.BROWSER_SYNC) {
|
||||
browserSyncData = (await browser.storage.sync.get(
|
||||
null
|
||||
)) as unknown as BrowserSyncData;
|
||||
}
|
||||
|
||||
return browserSyncData;
|
||||
};
|
||||
|
||||
export const savePermissionsToBrowserSyncStorage = async function (
|
||||
permissions: Permission_ENCRYPTED[]
|
||||
): Promise<void> {
|
||||
const gootiMetaHandler = new FirefoxMetaHandler();
|
||||
const gootiMetaData =
|
||||
(await gootiMetaHandler.loadFullData()) as GootiMetaData;
|
||||
|
||||
if (gootiMetaData.syncFlow === BrowserSyncFlow.NO_SYNC) {
|
||||
await browser.storage.local.set({ permissions });
|
||||
} else if (gootiMetaData.syncFlow === BrowserSyncFlow.BROWSER_SYNC) {
|
||||
await browser.storage.sync.set({ permissions });
|
||||
}
|
||||
};
|
||||
|
||||
export const checkPermissions = function (
|
||||
browserSessionData: BrowserSessionData,
|
||||
identity: Identity_DECRYPTED,
|
||||
host: string,
|
||||
method: Nip07Method,
|
||||
params: any
|
||||
): boolean | undefined {
|
||||
const permissions = browserSessionData.permissions.filter(
|
||||
(x) =>
|
||||
x.identityId === identity.id && x.host === host && x.method === method
|
||||
);
|
||||
|
||||
if (permissions.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (method === 'getPublicKey') {
|
||||
// No evaluation of params required.
|
||||
return permissions.every((x) => x.methodPolicy === 'allow');
|
||||
}
|
||||
|
||||
if (method === 'getRelays') {
|
||||
// No evaluation of params required.
|
||||
return permissions.every((x) => x.methodPolicy === 'allow');
|
||||
}
|
||||
|
||||
if (method === 'signEvent') {
|
||||
// Evaluate params.
|
||||
const eventTemplate = params as EventTemplate;
|
||||
if (
|
||||
permissions.find(
|
||||
(x) => x.methodPolicy === 'allow' && typeof x.kind === 'undefined'
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
permissions.some(
|
||||
(x) => x.methodPolicy === 'allow' && x.kind === eventTemplate.kind
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
permissions.some(
|
||||
(x) => x.methodPolicy === 'deny' && x.kind === eventTemplate.kind
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (method === 'nip04.encrypt') {
|
||||
// No evaluation of params required.
|
||||
return permissions.every((x) => x.methodPolicy === 'allow');
|
||||
}
|
||||
|
||||
if (method === 'nip04.decrypt') {
|
||||
// No evaluation of params required.
|
||||
return permissions.every((x) => x.methodPolicy === 'allow');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const storePermission = async function (
|
||||
browserSessionData: BrowserSessionData,
|
||||
identity: Identity_DECRYPTED,
|
||||
host: string,
|
||||
method: Nip07Method,
|
||||
methodPolicy: Nip07MethodPolicy,
|
||||
kind?: number
|
||||
) {
|
||||
const browserSyncData = await getBrowserSyncData();
|
||||
if (!browserSyncData) {
|
||||
throw new Error(`Could not retrieve sync data`);
|
||||
}
|
||||
|
||||
const permission: Permission_DECRYPTED = {
|
||||
id: crypto.randomUUID(),
|
||||
identityId: identity.id,
|
||||
host,
|
||||
method,
|
||||
methodPolicy,
|
||||
kind,
|
||||
};
|
||||
|
||||
// Store session data
|
||||
await browser.storage.session.set({
|
||||
permissions: [...browserSessionData.permissions, permission],
|
||||
});
|
||||
|
||||
// Encrypt permission to store in sync storage (depending on sync flow).
|
||||
const encryptedPermission = await encryptPermission(
|
||||
permission,
|
||||
browserSessionData.iv,
|
||||
browserSessionData.vaultPassword as string
|
||||
);
|
||||
|
||||
await savePermissionsToBrowserSyncStorage([
|
||||
...browserSyncData.permissions,
|
||||
encryptedPermission,
|
||||
]);
|
||||
};
|
||||
|
||||
export const getPosition = async function (width: number, height: number) {
|
||||
let left = 0;
|
||||
let top = 0;
|
||||
|
||||
try {
|
||||
const lastFocused = await browser.windows.getLastFocused();
|
||||
|
||||
if (
|
||||
lastFocused &&
|
||||
lastFocused.top !== undefined &&
|
||||
lastFocused.left !== undefined &&
|
||||
lastFocused.width !== undefined &&
|
||||
lastFocused.height !== undefined
|
||||
) {
|
||||
// Position window in the center of the lastFocused window
|
||||
top = Math.round(lastFocused.top + (lastFocused.height - height) / 2);
|
||||
left = Math.round(lastFocused.left + (lastFocused.width - width) / 2);
|
||||
} else {
|
||||
console.error('Last focused window properties are undefined.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting window position:', error);
|
||||
}
|
||||
|
||||
return {
|
||||
top,
|
||||
left,
|
||||
};
|
||||
};
|
||||
|
||||
export const signEvent = function (
|
||||
eventTemplate: EventTemplate,
|
||||
privkey: string
|
||||
): Event {
|
||||
return finalizeEvent(eventTemplate, NostrHelper.hex2bytes(privkey));
|
||||
};
|
||||
|
||||
export const nip04Encrypt = async function (
|
||||
privkey: string,
|
||||
peerPubkey: string,
|
||||
plaintext: string
|
||||
): Promise<string> {
|
||||
return await nip04.encrypt(
|
||||
NostrHelper.hex2bytes(privkey),
|
||||
peerPubkey,
|
||||
plaintext
|
||||
);
|
||||
};
|
||||
|
||||
export const nip04Decrypt = async function (
|
||||
privkey: string,
|
||||
peerPubkey: string,
|
||||
ciphertext: string
|
||||
): Promise<string> {
|
||||
return await nip04.decrypt(
|
||||
NostrHelper.hex2bytes(privkey),
|
||||
peerPubkey,
|
||||
ciphertext
|
||||
);
|
||||
};
|
||||
|
||||
const encryptPermission = async function (
|
||||
permission: Permission_DECRYPTED,
|
||||
iv: string,
|
||||
password: string
|
||||
): Promise<Permission_ENCRYPTED> {
|
||||
const encryptedPermission: Permission_ENCRYPTED = {
|
||||
id: await encrypt(permission.id, iv, password),
|
||||
identityId: await encrypt(permission.identityId, iv, password),
|
||||
host: await encrypt(permission.host, iv, password),
|
||||
method: await encrypt(permission.method, iv, password),
|
||||
methodPolicy: await encrypt(permission.methodPolicy, iv, password),
|
||||
};
|
||||
|
||||
if (typeof permission.kind !== 'undefined') {
|
||||
encryptedPermission.kind = await encrypt(
|
||||
permission.kind.toString(),
|
||||
iv,
|
||||
password
|
||||
);
|
||||
}
|
||||
|
||||
return encryptedPermission;
|
||||
};
|
||||
|
||||
const encrypt = async function (
|
||||
value: string,
|
||||
iv: string,
|
||||
password: string
|
||||
): Promise<string> {
|
||||
return await CryptoHelper.encrypt(value, iv, password);
|
||||
};
|
||||
@@ -0,0 +1,148 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { NostrHelper } from '@common';
|
||||
import {
|
||||
BackgroundRequestMessage,
|
||||
checkPermissions,
|
||||
debug,
|
||||
getBrowserSessionData,
|
||||
getPosition,
|
||||
nip04Decrypt,
|
||||
nip04Encrypt,
|
||||
PromptResponse,
|
||||
PromptResponseMessage,
|
||||
signEvent,
|
||||
storePermission,
|
||||
} from './background-common';
|
||||
import browser from 'webextension-polyfill';
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
type Relays = Record<string, { read: boolean; write: boolean }>;
|
||||
|
||||
const openPrompts = new Map<
|
||||
string,
|
||||
{
|
||||
resolve: (response: PromptResponse) => void;
|
||||
reject: (reason?: any) => void;
|
||||
}
|
||||
>();
|
||||
|
||||
browser.runtime.onMessage.addListener(async (message /*, sender*/) => {
|
||||
debug('Message received');
|
||||
const request = message as BackgroundRequestMessage | PromptResponseMessage;
|
||||
debug(request);
|
||||
|
||||
if ((request as PromptResponseMessage)?.id) {
|
||||
// Handle prompt response
|
||||
const promptResponse = request as PromptResponseMessage;
|
||||
const openPrompt = openPrompts.get(promptResponse.id);
|
||||
if (!openPrompt) {
|
||||
throw new Error(
|
||||
'Prompt response could not be matched to any previous request.'
|
||||
);
|
||||
}
|
||||
|
||||
openPrompt.resolve(promptResponse.response);
|
||||
openPrompts.delete(promptResponse.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const browserSessionData = await getBrowserSessionData();
|
||||
|
||||
if (!browserSessionData) {
|
||||
throw new Error('Gooti vault not unlocked by the user.');
|
||||
}
|
||||
|
||||
const currentIdentity = browserSessionData.identities.find(
|
||||
(x) => x.id === browserSessionData.selectedIdentityId
|
||||
);
|
||||
|
||||
if (!currentIdentity) {
|
||||
throw new Error('No Nostr identity available at endpoint.');
|
||||
}
|
||||
|
||||
const req = request as BackgroundRequestMessage;
|
||||
const permissionState = checkPermissions(
|
||||
browserSessionData,
|
||||
currentIdentity,
|
||||
req.host,
|
||||
req.method,
|
||||
req.params
|
||||
);
|
||||
|
||||
if (permissionState === false) {
|
||||
throw new Error('Permission denied');
|
||||
}
|
||||
|
||||
if (permissionState === undefined) {
|
||||
// Ask user for permission.
|
||||
const width = 375;
|
||||
const height = 600;
|
||||
const { top, left } = await getPosition(width, height);
|
||||
|
||||
const base64Event = Buffer.from(
|
||||
JSON.stringify(req.params ?? {}, undefined, 2)
|
||||
).toString('base64');
|
||||
|
||||
const response = await new Promise<PromptResponse>((resolve, reject) => {
|
||||
const id = crypto.randomUUID();
|
||||
openPrompts.set(id, { resolve, reject });
|
||||
browser.windows.create({
|
||||
type: 'popup',
|
||||
url: `prompt.html?method=${req.method}&host=${req.host}&id=${id}&nick=${currentIdentity.nick}&event=${base64Event}`,
|
||||
height,
|
||||
width,
|
||||
top,
|
||||
left,
|
||||
});
|
||||
});
|
||||
debug(response);
|
||||
if (response === 'approve' || response === 'reject') {
|
||||
await storePermission(
|
||||
browserSessionData,
|
||||
currentIdentity,
|
||||
req.host,
|
||||
req.method,
|
||||
response === 'approve' ? 'allow' : 'deny',
|
||||
req.params?.kind
|
||||
);
|
||||
}
|
||||
|
||||
if (['reject', 'reject-once'].includes(response)) {
|
||||
throw new Error('Permission denied');
|
||||
}
|
||||
} else {
|
||||
debug('Request allowed (via saved permission).');
|
||||
}
|
||||
|
||||
const relays: Relays = {};
|
||||
switch (req.method) {
|
||||
case 'getPublicKey':
|
||||
return NostrHelper.pubkeyFromPrivkey(currentIdentity.privkey);
|
||||
|
||||
case 'signEvent':
|
||||
return signEvent(req.params, currentIdentity.privkey);
|
||||
|
||||
case 'getRelays':
|
||||
browserSessionData.relays.forEach((x) => {
|
||||
relays[x.url] = { read: x.read, write: x.write };
|
||||
});
|
||||
return relays;
|
||||
|
||||
case 'nip04.encrypt':
|
||||
return await nip04Encrypt(
|
||||
currentIdentity.privkey,
|
||||
req.params.peerPubkey,
|
||||
req.params.plaintext
|
||||
);
|
||||
|
||||
case 'nip04.decrypt':
|
||||
return await nip04Decrypt(
|
||||
currentIdentity.privkey,
|
||||
req.params.peerPubkey,
|
||||
req.params.ciphertext
|
||||
);
|
||||
|
||||
default:
|
||||
throw new Error(`Not supported request method '${req.method}'.`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import browser from 'webextension-polyfill';
|
||||
import { BackgroundRequestMessage } from './background-common';
|
||||
|
||||
// Inject the script that will provide window.nostr
|
||||
// The script needs to run before any other scripts from the real
|
||||
// page run (and maybe check for window.nostr).
|
||||
const script = document.createElement('script');
|
||||
script.setAttribute('async', 'false');
|
||||
script.setAttribute('type', 'text/javascript');
|
||||
script.setAttribute('src', browser.runtime.getURL('gooti-extension.js'));
|
||||
(document.head || document.documentElement).appendChild(script);
|
||||
|
||||
// listen for messages from that script
|
||||
window.addEventListener('message', async (message) => {
|
||||
// We will also receive our own messages, that we sent.
|
||||
// We have to ignore them (they will not have a params field).
|
||||
|
||||
if (message.source !== window) return;
|
||||
if (!message.data) return;
|
||||
if (!message.data.params) return;
|
||||
if (message.data.ext !== 'gooti') return;
|
||||
|
||||
// pass on to background
|
||||
let response;
|
||||
try {
|
||||
const request: BackgroundRequestMessage = {
|
||||
method: message.data.method,
|
||||
params: message.data.params,
|
||||
host: location.host,
|
||||
};
|
||||
|
||||
response = await browser.runtime.sendMessage(request);
|
||||
} catch (error) {
|
||||
response = { error };
|
||||
}
|
||||
|
||||
// return response
|
||||
window.postMessage(
|
||||
{ id: message.data.id, ext: 'gooti', response },
|
||||
message.origin
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Event, EventTemplate } from 'nostr-tools';
|
||||
import { Nip07Method } from '@common';
|
||||
|
||||
type Relays = Record<string, { read: boolean; write: boolean }>;
|
||||
|
||||
class Messenger {
|
||||
#requests = new Map<
|
||||
string,
|
||||
{
|
||||
resolve: (value: unknown) => void;
|
||||
reject: (reason: any) => void;
|
||||
}
|
||||
>();
|
||||
|
||||
constructor() {
|
||||
window.addEventListener('message', this.#handleCallResponse.bind(this));
|
||||
}
|
||||
|
||||
async request(method: Nip07Method, params: any): Promise<any> {
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.#requests.set(id, { resolve, reject });
|
||||
window.postMessage(
|
||||
{
|
||||
id,
|
||||
ext: 'gooti',
|
||||
method,
|
||||
params,
|
||||
},
|
||||
'*'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#handleCallResponse(message: MessageEvent) {
|
||||
// We also will receive our own messages, that we sent.
|
||||
// We have to ignore them (they will not have a response field).
|
||||
if (
|
||||
!message.data ||
|
||||
message.data.response === null ||
|
||||
message.data.response === undefined ||
|
||||
message.data.ext !== 'gooti' ||
|
||||
!this.#requests.has(message.data.id)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.data.response.error) {
|
||||
this.#requests.get(message.data.id)?.reject(message.data.response.error);
|
||||
} else {
|
||||
this.#requests.get(message.data.id)?.resolve(message.data.response);
|
||||
}
|
||||
|
||||
this.#requests.delete(message.data.id);
|
||||
}
|
||||
}
|
||||
|
||||
const nostr = {
|
||||
messenger: new Messenger(),
|
||||
|
||||
async getPublicKey(): Promise<string> {
|
||||
debug('getPublicKey received');
|
||||
const pubkey = await this.messenger.request('getPublicKey', {});
|
||||
debug(`getPublicKey response:`);
|
||||
debug(pubkey);
|
||||
return pubkey;
|
||||
},
|
||||
|
||||
async signEvent(event: EventTemplate): Promise<Event> {
|
||||
debug('signEvent received');
|
||||
const signedEvent = await this.messenger.request('signEvent', event);
|
||||
debug('signEvent response:');
|
||||
debug(signedEvent);
|
||||
return signedEvent;
|
||||
},
|
||||
|
||||
async getRelays(): Promise<Relays> {
|
||||
debug('getRelays received');
|
||||
const relays = (await this.messenger.request('getRelays', {})) as Relays;
|
||||
debug('getRelays response:');
|
||||
debug(relays);
|
||||
return relays;
|
||||
},
|
||||
|
||||
nip04: {
|
||||
that: this,
|
||||
|
||||
async encrypt(peerPubkey: string, plaintext: string): Promise<string> {
|
||||
debug('nip04.encrypt received');
|
||||
const ciphertext = (await nostr.messenger.request('nip04.encrypt', {
|
||||
peerPubkey,
|
||||
plaintext,
|
||||
})) as string;
|
||||
debug('nip04.encrypt response:');
|
||||
debug(ciphertext);
|
||||
return ciphertext;
|
||||
},
|
||||
|
||||
async decrypt(peerPubkey: string, ciphertext: string): Promise<string> {
|
||||
debug('nip04.decrypt received');
|
||||
const plaintext = (await nostr.messenger.request('nip04.decrypt', {
|
||||
peerPubkey,
|
||||
ciphertext,
|
||||
})) as string;
|
||||
debug('nip04.decrypt response:');
|
||||
debug(plaintext);
|
||||
return plaintext;
|
||||
},
|
||||
},
|
||||
|
||||
// nip44: {
|
||||
// async encrypt(peer, plaintext) {
|
||||
// return window.nostr._call('nip44.encrypt', { peer, plaintext });
|
||||
// },
|
||||
|
||||
// async decrypt(peer, ciphertext) {
|
||||
// return window.nostr._call('nip44.decrypt', { peer, ciphertext });
|
||||
// },
|
||||
// },
|
||||
};
|
||||
|
||||
window.nostr = nostr as any;
|
||||
|
||||
const debug = function (value: any) {
|
||||
console.log(JSON.stringify(value));
|
||||
};
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
import browser from 'webextension-polyfill';
|
||||
import { Buffer } from 'buffer';
|
||||
import { Nip07Method } from '@common';
|
||||
import { PromptResponse, PromptResponseMessage } from './background-common';
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const id = params.get('id') as string;
|
||||
const method = params.get('method') as Nip07Method;
|
||||
const host = params.get('host') as string;
|
||||
const nick = params.get('nick') as string;
|
||||
const event = Buffer.from(params.get('event') as string, 'base64').toString();
|
||||
|
||||
let title = '';
|
||||
switch (method) {
|
||||
case 'getPublicKey':
|
||||
title = 'Get Public Key';
|
||||
break;
|
||||
|
||||
case 'signEvent':
|
||||
title = 'Sign Event';
|
||||
break;
|
||||
|
||||
case 'nip04.encrypt':
|
||||
title = 'Encrypt';
|
||||
break;
|
||||
|
||||
case 'nip04.decrypt':
|
||||
title = 'Decrypt';
|
||||
break;
|
||||
|
||||
case 'getRelays':
|
||||
title = 'Get Relays';
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const titleSpanElement = document.getElementById('titleSpan');
|
||||
if (titleSpanElement) {
|
||||
titleSpanElement.innerText = title;
|
||||
}
|
||||
|
||||
Array.from(document.getElementsByClassName('nick-INSERT')).forEach(
|
||||
(element) => {
|
||||
(element as HTMLElement).innerText = nick;
|
||||
}
|
||||
);
|
||||
|
||||
Array.from(document.getElementsByClassName('host-INSERT')).forEach(
|
||||
(element) => {
|
||||
(element as HTMLElement).innerText = host;
|
||||
}
|
||||
);
|
||||
|
||||
const kindSpanElement = document.getElementById('kindSpan');
|
||||
if (kindSpanElement) {
|
||||
kindSpanElement.innerText = JSON.parse(event).kind;
|
||||
}
|
||||
|
||||
const cardGetPublicKeyElement = document.getElementById('cardGetPublicKey');
|
||||
if (cardGetPublicKeyElement) {
|
||||
if (method === 'getPublicKey') {
|
||||
// Do nothing.
|
||||
} else {
|
||||
cardGetPublicKeyElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
const cardGetRelaysElement = document.getElementById('cardGetRelays');
|
||||
if (cardGetRelaysElement) {
|
||||
if (method === 'getRelays') {
|
||||
// Do nothing.
|
||||
} else {
|
||||
cardGetRelaysElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
const cardSignEventElement = document.getElementById('cardSignEvent');
|
||||
const card2SignEventElement = document.getElementById('card2SignEvent');
|
||||
if (cardSignEventElement && card2SignEventElement) {
|
||||
if (method === 'signEvent') {
|
||||
const card2SignEvent_jsonElement = document.getElementById(
|
||||
'card2SignEvent_json'
|
||||
);
|
||||
if (card2SignEvent_jsonElement) {
|
||||
card2SignEvent_jsonElement.innerText = event;
|
||||
}
|
||||
} else {
|
||||
cardSignEventElement.style.display = 'none';
|
||||
card2SignEventElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
const cardNip04EncryptElement = document.getElementById('cardNip04Encrypt');
|
||||
const card2Nip04EncryptElement = document.getElementById('card2Nip04Encrypt');
|
||||
if (cardNip04EncryptElement && card2Nip04EncryptElement) {
|
||||
if (method === 'nip04.encrypt') {
|
||||
const card2Nip04Encrypt_textElement = document.getElementById(
|
||||
'card2Nip04Encrypt_text'
|
||||
);
|
||||
if (card2Nip04Encrypt_textElement) {
|
||||
const eventObject: { peerPubkey: string; plaintext: string } =
|
||||
JSON.parse(event);
|
||||
card2Nip04Encrypt_textElement.innerText = eventObject.plaintext;
|
||||
}
|
||||
} else {
|
||||
cardNip04EncryptElement.style.display = 'none';
|
||||
card2Nip04EncryptElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
const cardNip04DecryptElement = document.getElementById('cardNip04Decrypt');
|
||||
const card2Nip04DecryptElement = document.getElementById('card2Nip04Decrypt');
|
||||
if (cardNip04DecryptElement && card2Nip04DecryptElement) {
|
||||
if (method === 'nip04.decrypt') {
|
||||
const card2Nip04Decrypt_textElement = document.getElementById(
|
||||
'card2Nip04Decrypt_text'
|
||||
);
|
||||
if (card2Nip04Decrypt_textElement) {
|
||||
const eventObject: { peerPubkey: string; ciphertext: string } =
|
||||
JSON.parse(event);
|
||||
card2Nip04Decrypt_textElement.innerText = eventObject.ciphertext;
|
||||
}
|
||||
} else {
|
||||
cardNip04DecryptElement.style.display = 'none';
|
||||
card2Nip04DecryptElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Functions
|
||||
//
|
||||
|
||||
function deliver(response: PromptResponse) {
|
||||
const message: PromptResponseMessage = {
|
||||
id,
|
||||
response,
|
||||
};
|
||||
|
||||
browser.runtime.sendMessage(message);
|
||||
window.close();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const rejectJustOnceButton = document.getElementById('rejectJustOnceButton');
|
||||
rejectJustOnceButton?.addEventListener('click', () => {
|
||||
deliver('reject-once');
|
||||
});
|
||||
|
||||
const rejectButton = document.getElementById('rejectButton');
|
||||
rejectButton?.addEventListener('click', () => {
|
||||
deliver('reject');
|
||||
});
|
||||
|
||||
const approveJustOnceButton = document.getElementById(
|
||||
'approveJustOnceButton'
|
||||
);
|
||||
approveJustOnceButton?.addEventListener('click', () => {
|
||||
deliver('approve-once');
|
||||
});
|
||||
|
||||
const approveButton = document.getElementById('approveButton');
|
||||
approveButton?.addEventListener('click', () => {
|
||||
deliver('approve');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user