From c51e4d951f0ee1ed33a619602c90c9da934ef33e Mon Sep 17 00:00:00 2001 From: DEV Sam Hayes Date: Sat, 15 Feb 2025 15:08:48 +0100 Subject: [PATCH] add support for NIP44 (chrome & firefox) --- README.md | 2 + package-lock.json | 4 +- package.json | 6 +-- projects/chrome/public/manifest.json | 6 +-- projects/chrome/public/prompt.html | 46 +++++++++++++++++++++++ projects/chrome/src/background-common.ts | 37 +++++++++++++++++- projects/chrome/src/background.ts | 16 ++++++++ projects/chrome/src/gooti-extension.ts | 30 +++++++++++---- projects/chrome/src/prompt.ts | 43 +++++++++++++++++++++ projects/common/src/lib/models/nostr.ts | 4 +- projects/firefox/public/manifest.json | 2 +- projects/firefox/public/prompt.html | 46 +++++++++++++++++++++++ projects/firefox/src/background-common.ts | 37 +++++++++++++++++- projects/firefox/src/background.ts | 16 ++++++++ projects/firefox/src/gooti-extension.ts | 30 +++++++++++---- projects/firefox/src/prompt.ts | 44 ++++++++++++++++++++++ 16 files changed, 341 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 49792bc..1ed3212 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ It also implements these optional methods: async window.nostr.getRelays(): { [url: string]: {read: boolean, write: boolean} } async window.nostr.nip04.encrypt(pubkey, plaintext): string async window.nostr.nip04.decrypt(pubkey, ciphertext): string +async window.nostr.nip44.encrypt(pubkey, plaintext): string +async window.nostr.nip44.decrypt(pubkey, ciphertext): string ``` The repository is configured as monorepo to hold the extensions for Chrome and Firefox. diff --git a/package-lock.json b/package-lock.json index fa3d101..8c46684 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gooti-extension", - "version": "0.0.3", + "version": "0.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gooti-extension", - "version": "0.0.3", + "version": "0.0.4", "dependencies": { "@angular/animations": "^19.0.0", "@angular/common": "^19.0.0", diff --git a/package.json b/package.json index dd5508d..e69ab48 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "gooti-extension", - "version": "0.0.3", + "version": "0.0.4", "custom": { "chrome": { - "version": "0.0.3" + "version": "0.0.4" }, "firefox": { - "version": "0.0.3" + "version": "0.0.4" } }, "scripts": { diff --git a/projects/chrome/public/manifest.json b/projects/chrome/public/manifest.json index 2095aef..6bb6513 100644 --- a/projects/chrome/public/manifest.json +++ b/projects/chrome/public/manifest.json @@ -1,8 +1,8 @@ { "manifest_version": 3, - "name": "Gooti", - "description": "Nostr Identity Manager & Signer", - "version": "0.0.3", + "name": "Gooti - Nostr Identity Manager & Signer", + "description": "Manage and switch between multiple identities while interacting with Nostr apps", + "version": "0.0.4", "homepage_url": "https://getgooti.com", "options_page": "options.html", "permissions": [ diff --git a/projects/chrome/public/prompt.html b/projects/chrome/public/prompt.html index 22256a9..8d7b4d7 100644 --- a/projects/chrome/public/prompt.html +++ b/projects/chrome/public/prompt.html @@ -145,6 +145,29 @@
+ +
+ + + is requesting permission to
+
+ encrypt a text (NIP44)
+
+ + for the selected identity + + +
+
+ + +
+
+
+
@@ -167,6 +190,29 @@
+ + +
+ + + is requesting permission to
+
+ decrypt a text (NIP44)
+
+ + for the selected identity + + +
+
+ + +
+
+
diff --git a/projects/chrome/src/background-common.ts b/projects/chrome/src/background-common.ts index cfe6dde..b89db27 100644 --- a/projects/chrome/src/background-common.ts +++ b/projects/chrome/src/background-common.ts @@ -13,7 +13,7 @@ import { Permission_ENCRYPTED, } from '@common'; import { ChromeMetaHandler } from './app/common/data/chrome-meta-handler'; -import { Event, EventTemplate, finalizeEvent, nip04 } from 'nostr-tools'; +import { Event, EventTemplate, finalizeEvent, nip04, nip44 } from 'nostr-tools'; export const debug = function (message: any) { const dateString = new Date().toISOString(); @@ -141,11 +141,21 @@ export const checkPermissions = function ( return permissions.every((x) => x.methodPolicy === 'allow'); } + if (method === 'nip44.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'); } + if (method === 'nip44.decrypt') { + // No evaluation of params required. + return permissions.every((x) => x.methodPolicy === 'allow'); + } + return undefined; }; @@ -238,6 +248,18 @@ export const nip04Encrypt = async function ( ); }; +export const nip44Encrypt = async function ( + privkey: string, + peerPubkey: string, + plaintext: string +): Promise { + const key = nip44.v2.utils.getConversationKey( + NostrHelper.hex2bytes(privkey), + peerPubkey + ); + return nip44.v2.encrypt(plaintext, key); +}; + export const nip04Decrypt = async function ( privkey: string, peerPubkey: string, @@ -250,6 +272,19 @@ export const nip04Decrypt = async function ( ); }; +export const nip44Decrypt = async function ( + privkey: string, + peerPubkey: string, + ciphertext: string +): Promise { + const key = nip44.v2.utils.getConversationKey( + NostrHelper.hex2bytes(privkey), + peerPubkey + ); + + return nip44.v2.decrypt(ciphertext, key); +}; + const encryptPermission = async function ( permission: Permission_DECRYPTED, iv: string, diff --git a/projects/chrome/src/background.ts b/projects/chrome/src/background.ts index 08113aa..6c33232 100644 --- a/projects/chrome/src/background.ts +++ b/projects/chrome/src/background.ts @@ -8,6 +8,8 @@ import { getPosition, nip04Decrypt, nip04Encrypt, + nip44Decrypt, + nip44Encrypt, PromptResponse, PromptResponseMessage, signEvent, @@ -135,6 +137,13 @@ browser.runtime.onMessage.addListener(async (message /*, sender*/) => { req.params.plaintext ); + case 'nip44.encrypt': + return await nip44Encrypt( + currentIdentity.privkey, + req.params.peerPubkey, + req.params.plaintext + ); + case 'nip04.decrypt': return await nip04Decrypt( currentIdentity.privkey, @@ -142,6 +151,13 @@ browser.runtime.onMessage.addListener(async (message /*, sender*/) => { req.params.ciphertext ); + case 'nip44.decrypt': + return await nip44Decrypt( + currentIdentity.privkey, + req.params.peerPubkey, + req.params.ciphertext + ); + default: throw new Error(`Not supported request method '${req.method}'.`); } diff --git a/projects/chrome/src/gooti-extension.ts b/projects/chrome/src/gooti-extension.ts index 0c19f55..35f90a5 100644 --- a/projects/chrome/src/gooti-extension.ts +++ b/projects/chrome/src/gooti-extension.ts @@ -110,15 +110,29 @@ const nostr = { }, }, - // nip44: { - // async encrypt(peer, plaintext) { - // return window.nostr._call('nip44.encrypt', { peer, plaintext }); - // }, + nip44: { + async encrypt(peerPubkey: string, plaintext: string): Promise { + debug('nip44.encrypt received'); + const ciphertext = (await nostr.messenger.request('nip44.encrypt', { + peerPubkey, + plaintext, + })) as string; + debug('nip44.encrypt response:'); + debug(ciphertext); + return ciphertext; + }, - // async decrypt(peer, ciphertext) { - // return window.nostr._call('nip44.decrypt', { peer, ciphertext }); - // }, - // }, + async decrypt(peerPubkey: string, ciphertext: string): Promise { + debug('nip44.decrypt received'); + const plaintext = (await nostr.messenger.request('nip44.decrypt', { + peerPubkey, + ciphertext, + })) as string; + debug('nip44.decrypt response:'); + debug(plaintext); + return plaintext; + }, + }, }; window.nostr = nostr as any; diff --git a/projects/chrome/src/prompt.ts b/projects/chrome/src/prompt.ts index 0032df3..95aa6fc 100644 --- a/projects/chrome/src/prompt.ts +++ b/projects/chrome/src/prompt.ts @@ -24,10 +24,18 @@ switch (method) { title = 'Encrypt'; break; + case 'nip44.encrypt': + title = 'Encrypt'; + break; + case 'nip04.decrypt': title = 'Decrypt'; break; + case 'nip44.decrypt': + title = 'Decrypt'; + break; + case 'getRelays': title = 'Get Relays'; break; @@ -110,6 +118,23 @@ if (cardNip04EncryptElement && card2Nip04EncryptElement) { } } +const cardNip44EncryptElement = document.getElementById('cardNip44Encrypt'); +const card2Nip44EncryptElement = document.getElementById('card2Nip44Encrypt'); +if (cardNip44EncryptElement && card2Nip44EncryptElement) { + if (method === 'nip44.encrypt') { + const card2Nip44Encrypt_textElement = document.getElementById( + 'card2Nip44Encrypt_text' + ); + if (card2Nip44Encrypt_textElement) { + const eventObject: { peerPubkey: string; plaintext: string } = + JSON.parse(event); + card2Nip44Encrypt_textElement.innerText = eventObject.plaintext; + } + } else { + cardNip44EncryptElement.style.display = 'none'; + card2Nip44EncryptElement.style.display = 'none'; + } +} const cardNip04DecryptElement = document.getElementById('cardNip04Decrypt'); const card2Nip04DecryptElement = document.getElementById('card2Nip04Decrypt'); if (cardNip04DecryptElement && card2Nip04DecryptElement) { @@ -128,6 +153,24 @@ if (cardNip04DecryptElement && card2Nip04DecryptElement) { } } +const cardNip44DecryptElement = document.getElementById('cardNip44Decrypt'); +const card2Nip44DecryptElement = document.getElementById('card2Nip44Decrypt'); +if (cardNip44DecryptElement && card2Nip44DecryptElement) { + if (method === 'nip44.decrypt') { + const card2Nip44Decrypt_textElement = document.getElementById( + 'card2Nip44Decrypt_text' + ); + if (card2Nip44Decrypt_textElement) { + const eventObject: { peerPubkey: string; ciphertext: string } = + JSON.parse(event); + card2Nip44Decrypt_textElement.innerText = eventObject.ciphertext; + } + } else { + cardNip44DecryptElement.style.display = 'none'; + card2Nip44DecryptElement.style.display = 'none'; + } +} + // // Functions // diff --git a/projects/common/src/lib/models/nostr.ts b/projects/common/src/lib/models/nostr.ts index 3168ca3..5fc4686 100644 --- a/projects/common/src/lib/models/nostr.ts +++ b/projects/common/src/lib/models/nostr.ts @@ -3,6 +3,8 @@ export type Nip07Method = | 'getPublicKey' | 'getRelays' | 'nip04.encrypt' - | 'nip04.decrypt'; + | 'nip04.decrypt' + | 'nip44.encrypt' + | 'nip44.decrypt'; export type Nip07MethodPolicy = 'allow' | 'deny'; diff --git a/projects/firefox/public/manifest.json b/projects/firefox/public/manifest.json index a9bc34a..1b4faa1 100644 --- a/projects/firefox/public/manifest.json +++ b/projects/firefox/public/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Gooti", "description": "Nostr Identity Manager & Signer", - "version": "0.0.3", + "version": "0.0.4", "homepage_url": "https://getgooti.com", "options_page": "options.html", "permissions": [ diff --git a/projects/firefox/public/prompt.html b/projects/firefox/public/prompt.html index 22256a9..8d7b4d7 100644 --- a/projects/firefox/public/prompt.html +++ b/projects/firefox/public/prompt.html @@ -145,6 +145,29 @@
+ +
+ + + is requesting permission to
+
+ encrypt a text (NIP44)
+
+ + for the selected identity + + +
+
+ + +
+
+
+
@@ -167,6 +190,29 @@
+ + +
+ + + is requesting permission to
+
+ decrypt a text (NIP44)
+
+ + for the selected identity + + +
+
+ + +
+
+
diff --git a/projects/firefox/src/background-common.ts b/projects/firefox/src/background-common.ts index d942174..bad95e6 100644 --- a/projects/firefox/src/background-common.ts +++ b/projects/firefox/src/background-common.ts @@ -12,7 +12,7 @@ import { Permission_DECRYPTED, Permission_ENCRYPTED, } from '@common'; -import { Event, EventTemplate, finalizeEvent, nip04 } from 'nostr-tools'; +import { Event, EventTemplate, finalizeEvent, nip04, nip44 } from 'nostr-tools'; import { FirefoxMetaHandler } from './app/common/data/firefox-meta-handler'; import browser from 'webextension-polyfill'; @@ -146,11 +146,21 @@ export const checkPermissions = function ( return permissions.every((x) => x.methodPolicy === 'allow'); } + if (method === 'nip44.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'); } + if (method === 'nip44.decrypt') { + // No evaluation of params required. + return permissions.every((x) => x.methodPolicy === 'allow'); + } + return undefined; }; @@ -243,6 +253,18 @@ export const nip04Encrypt = async function ( ); }; +export const nip44Encrypt = async function ( + privkey: string, + peerPubkey: string, + plaintext: string +): Promise { + const key = nip44.v2.utils.getConversationKey( + NostrHelper.hex2bytes(privkey), + peerPubkey + ); + return nip44.v2.encrypt(plaintext, key); +}; + export const nip04Decrypt = async function ( privkey: string, peerPubkey: string, @@ -255,6 +277,19 @@ export const nip04Decrypt = async function ( ); }; +export const nip44Decrypt = async function ( + privkey: string, + peerPubkey: string, + ciphertext: string +): Promise { + const key = nip44.v2.utils.getConversationKey( + NostrHelper.hex2bytes(privkey), + peerPubkey + ); + + return nip44.v2.decrypt(ciphertext, key); +}; + const encryptPermission = async function ( permission: Permission_DECRYPTED, iv: string, diff --git a/projects/firefox/src/background.ts b/projects/firefox/src/background.ts index 08113aa..6c33232 100644 --- a/projects/firefox/src/background.ts +++ b/projects/firefox/src/background.ts @@ -8,6 +8,8 @@ import { getPosition, nip04Decrypt, nip04Encrypt, + nip44Decrypt, + nip44Encrypt, PromptResponse, PromptResponseMessage, signEvent, @@ -135,6 +137,13 @@ browser.runtime.onMessage.addListener(async (message /*, sender*/) => { req.params.plaintext ); + case 'nip44.encrypt': + return await nip44Encrypt( + currentIdentity.privkey, + req.params.peerPubkey, + req.params.plaintext + ); + case 'nip04.decrypt': return await nip04Decrypt( currentIdentity.privkey, @@ -142,6 +151,13 @@ browser.runtime.onMessage.addListener(async (message /*, sender*/) => { req.params.ciphertext ); + case 'nip44.decrypt': + return await nip44Decrypt( + currentIdentity.privkey, + req.params.peerPubkey, + req.params.ciphertext + ); + default: throw new Error(`Not supported request method '${req.method}'.`); } diff --git a/projects/firefox/src/gooti-extension.ts b/projects/firefox/src/gooti-extension.ts index 0c19f55..35f90a5 100644 --- a/projects/firefox/src/gooti-extension.ts +++ b/projects/firefox/src/gooti-extension.ts @@ -110,15 +110,29 @@ const nostr = { }, }, - // nip44: { - // async encrypt(peer, plaintext) { - // return window.nostr._call('nip44.encrypt', { peer, plaintext }); - // }, + nip44: { + async encrypt(peerPubkey: string, plaintext: string): Promise { + debug('nip44.encrypt received'); + const ciphertext = (await nostr.messenger.request('nip44.encrypt', { + peerPubkey, + plaintext, + })) as string; + debug('nip44.encrypt response:'); + debug(ciphertext); + return ciphertext; + }, - // async decrypt(peer, ciphertext) { - // return window.nostr._call('nip44.decrypt', { peer, ciphertext }); - // }, - // }, + async decrypt(peerPubkey: string, ciphertext: string): Promise { + debug('nip44.decrypt received'); + const plaintext = (await nostr.messenger.request('nip44.decrypt', { + peerPubkey, + ciphertext, + })) as string; + debug('nip44.decrypt response:'); + debug(plaintext); + return plaintext; + }, + }, }; window.nostr = nostr as any; diff --git a/projects/firefox/src/prompt.ts b/projects/firefox/src/prompt.ts index 0032df3..279c880 100644 --- a/projects/firefox/src/prompt.ts +++ b/projects/firefox/src/prompt.ts @@ -24,10 +24,18 @@ switch (method) { title = 'Encrypt'; break; + case 'nip44.encrypt': + title = 'Encrypt'; + break; + case 'nip04.decrypt': title = 'Decrypt'; break; + case 'nip44.decrypt': + title = 'Decrypt'; + break; + case 'getRelays': title = 'Get Relays'; break; @@ -110,6 +118,24 @@ if (cardNip04EncryptElement && card2Nip04EncryptElement) { } } +const cardNip44EncryptElement = document.getElementById('cardNip44Encrypt'); +const card2Nip44EncryptElement = document.getElementById('card2Nip44Encrypt'); +if (cardNip44EncryptElement && card2Nip44EncryptElement) { + if (method === 'nip44.encrypt') { + const card2Nip44Encrypt_textElement = document.getElementById( + 'card2Nip44Encrypt_text' + ); + if (card2Nip44Encrypt_textElement) { + const eventObject: { peerPubkey: string; plaintext: string } = + JSON.parse(event); + card2Nip44Encrypt_textElement.innerText = eventObject.plaintext; + } + } else { + cardNip44EncryptElement.style.display = 'none'; + card2Nip44EncryptElement.style.display = 'none'; + } +} + const cardNip04DecryptElement = document.getElementById('cardNip04Decrypt'); const card2Nip04DecryptElement = document.getElementById('card2Nip04Decrypt'); if (cardNip04DecryptElement && card2Nip04DecryptElement) { @@ -128,6 +154,24 @@ if (cardNip04DecryptElement && card2Nip04DecryptElement) { } } +const cardNip44DecryptElement = document.getElementById('cardNip44Decrypt'); +const card2Nip44DecryptElement = document.getElementById('card2Nip44Decrypt'); +if (cardNip44DecryptElement && card2Nip44DecryptElement) { + if (method === 'nip44.decrypt') { + const card2Nip44Decrypt_textElement = document.getElementById( + 'card2Nip44Decrypt_text' + ); + if (card2Nip44Decrypt_textElement) { + const eventObject: { peerPubkey: string; ciphertext: string } = + JSON.parse(event); + card2Nip44Decrypt_textElement.innerText = eventObject.ciphertext; + } + } else { + cardNip44DecryptElement.style.display = 'none'; + card2Nip44DecryptElement.style.display = 'none'; + } +} + // // Functions //