migrate background related things from chrome
This commit is contained in:
@@ -5,37 +5,26 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"homepage_url": "https://getgooti.com",
|
"homepage_url": "https://getgooti.com",
|
||||||
"options_page": "options.html",
|
"options_page": "options.html",
|
||||||
"permissions": [
|
"permissions": ["storage"],
|
||||||
"storage"
|
|
||||||
],
|
|
||||||
"action": {
|
"action": {
|
||||||
"default_popup": "index.html",
|
"default_popup": "index.html",
|
||||||
"default_icon": "gooti-with-bg.png"
|
"default_icon": "gooti-with-bg.png"
|
||||||
},
|
},
|
||||||
"background": {
|
"background": {
|
||||||
"scripts": [
|
"scripts": ["background.js"]
|
||||||
"background.js"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"run_at": "document_end",
|
"run_at": "document_start",
|
||||||
"matches": [
|
"matches": ["<all_urls>"],
|
||||||
"<all_urls>"
|
"js": ["gooti-content-script.js"],
|
||||||
],
|
"all_frames": true
|
||||||
"js": [
|
|
||||||
"gooti-content-script.js"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"web_accessible_resources": [
|
"web_accessible_resources": [
|
||||||
{
|
{
|
||||||
"resources": [
|
"resources": ["gooti-extension.js"],
|
||||||
"gooti-extension.js"
|
"matches": ["<all_urls>"]
|
||||||
],
|
|
||||||
"matches": [
|
|
||||||
"<all_urls>"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"browser_specific_settings": {
|
"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