From 1691c4fcf7808bc2ea85bd6c81f38c9005ac86a9 Mon Sep 17 00:00:00 2001 From: manimejia Date: Tue, 4 Jun 2024 10:37:35 -0500 Subject: [PATCH 1/8] rename nip04.ts to encryption.ts --- ndk/src/events/{nip04.ts => encryption.ts} | 1 + ndk/src/events/index.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) rename ndk/src/events/{nip04.ts => encryption.ts} (99%) diff --git a/ndk/src/events/nip04.ts b/ndk/src/events/encryption.ts similarity index 99% rename from ndk/src/events/nip04.ts rename to ndk/src/events/encryption.ts index 849f7c97..fe699716 100644 --- a/ndk/src/events/nip04.ts +++ b/ndk/src/events/encryption.ts @@ -39,3 +39,4 @@ export async function decrypt(this: NDKEvent, sender?: NDKUser, signer?: NDKSign this.content = (await signer?.decrypt(sender, this.content)) as string; } + diff --git a/ndk/src/events/index.ts b/ndk/src/events/index.ts index eb2f61ae..984bcffe 100644 --- a/ndk/src/events/index.ts +++ b/ndk/src/events/index.ts @@ -10,7 +10,7 @@ import { type NDKUser } from "../user/index.js"; import { type ContentTag, generateContentTags, mergeTags } from "./content-tagger.js"; import { isEphemeral, isParamReplaceable, isReplaceable } from "./kind.js"; import { NDKKind } from "./kinds/index.js"; -import { decrypt, encrypt } from "./nip04.js"; +import { decrypt, encrypt } from "./encryption.js"; import { encode } from "./nip19.js"; import { repost } from "./repost.js"; import { fetchReplyEvent, fetchRootEvent, fetchTaggedEvent } from "./fetch-tagged-event.js"; From 0647703c7656571d81bb9ab60ccbd97bd6e93c88 Mon Sep 17 00:00:00 2001 From: manimejia Date: Tue, 4 Jun 2024 10:47:46 -0500 Subject: [PATCH 2/8] implement nip44 as optional encryption nip for signers --- ndk/src/events/encryption.ts | 57 ++++++++++++++++++++++++++++++++---- ndk/src/signers/index.ts | 22 +++++++++++--- 2 files changed, 70 insertions(+), 9 deletions(-) diff --git a/ndk/src/events/encryption.ts b/ndk/src/events/encryption.ts index fe699716..5bc9ef0d 100644 --- a/ndk/src/events/encryption.ts +++ b/ndk/src/events/encryption.ts @@ -1,12 +1,28 @@ +/** + * Encryption and giftwrapping of events + * Implemnents Nip04, Nip44, (TODO) Nip59 + */ import type { NDKSigner } from "../signers"; import type { NDKUser } from "../user"; import type { NDKEvent } from "./index.js"; +export type EncryptionNip = 'nip04' | 'nip44'; + +// some clients may wish to set a default for message encryption... +// TODO how should replies to 'nip04' encrypted messages be handled? +let defaultEncryption : EncryptionNip | undefined = undefined; +export function useEncryption(nip : EncryptionNip){ + defaultEncryption = nip; +} + + export async function encrypt( this: NDKEvent, recipient?: NDKUser, - signer?: NDKSigner + signer?: NDKSigner, + nip : EncryptionNip | undefined = defaultEncryption ): Promise { + let encrypted : string; if (!this.ndk) throw new Error("No NDK instance found!"); if (!signer) { await this.ndk.assertSigner(); @@ -24,10 +40,21 @@ export async function encrypt( recipient = this.ndk.getUser({ pubkey: pTags[0][1] }); } - this.content = (await signer?.encrypt(recipient, this.content)) as string; -} + // support for encrypting events via legacy `nip04`. adapted from Coracle + if ((!nip || nip == 'nip04') && isNip04Enabled(signer)) { + try{ + encrypted = (await signer?.encrypt(recipient, this.content, 'nip04')) as string; + }catch{} + } + if ((!encrypted || nip == "nip44") && isNip44Enabled(signer)) { + encrypted = (await signer?.encrypt(recipient, this.content, 'nip44')) as string; + } + if(!encrypted) throw new Error('Failed to encrypt event.') + this.content = encrypted + } -export async function decrypt(this: NDKEvent, sender?: NDKUser, signer?: NDKSigner): Promise { +export async function decrypt(this: NDKEvent, sender?: NDKUser, signer?: NDKSigner, nip: EncryptionNip | undefined = defaultEncryption): Promise { + let decrypted : string; if (!this.ndk) throw new Error("No NDK instance found!"); if (!signer) { await this.ndk.assertSigner(); @@ -36,7 +63,27 @@ export async function decrypt(this: NDKEvent, sender?: NDKUser, signer?: NDKSign if (!sender) { sender = this.author; } + // simple check for legacy `nip04` encrypted events. adapted from Coracle + if ((!nip || nip=='nip04') && isNip04Enabled(signer) && this.content.search("?iv=")) { + try{ + decrypted = (await signer?.decrypt(sender, this.content, 'nip04')) as string; + }catch{} + } + if (!decrypted && isNip44Enabled(signer)) { + decrypted = (await signer?.decrypt(sender, this.content, 'nip44')) as string; + } + if(!decrypted) throw new Error('Failed to decrypt event.') + this.content = decrypted +} - this.content = (await signer?.decrypt(sender, this.content)) as string; +async function isNip04Enabled(signer : NDKSigner){ + let enabled = await signer.encryptionEnabled(); + if(enabled.indexOf('nip04') != -1) return true; + return false; } +async function isNip44Enabled(signer : NDKSigner){ + let enabled = await signer.encryptionEnabled(); + if(enabled.indexOf('nip44') != -1) return true; + return false; +} \ No newline at end of file diff --git a/ndk/src/signers/index.ts b/ndk/src/signers/index.ts index 9e2e52aa..f3d9de37 100644 --- a/ndk/src/signers/index.ts +++ b/ndk/src/signers/index.ts @@ -1,7 +1,9 @@ +import { EncryptionNip } from "../events/encryption.js"; import type { NostrEvent } from "../events/index.js"; import { NDKRelay } from "../relay/index.js"; import type { NDKUser } from "../user"; + /** * Interface for NDK signers. */ @@ -31,16 +33,28 @@ export interface NDKSigner { */ relays?(): Promise; + /** + * Determine the types of encryption (by nip) that this signer can perform. + * Implementing classes SHOULD return a value even for legacy (only nip04) third party signers. + * @return A promised list of any (or none) of these strings ['nip04', 'nip44'] + */ + encryptionEnabled?(): Promise + /** * Encrypts the given Nostr event for the given recipient. + * Implementing classes SHOULD equate legacy (only nip04) to nip == `nip04` || undefined + * @param recipient - The recipient (pubkey or conversationKey) of the encrypted value. * @param value - The value to be encrypted. - * @param recipient - The recipient of the encrypted value. + * @param nip - which NIP is being implemented ('nip04', 'nip44') */ - encrypt(recipient: NDKUser, value: string): Promise; + encrypt(recipient: NDKUser, value: string, nip?:EncryptionNip): Promise; /** * Decrypts the given value. - * @param value + * Implementing classes SHOULD equate legacy (only nip04) to nip == `nip04` || undefined + * @param sender - The sender (pubkey or conversationKey) of the encrypted value + * @param value - The value to be decrypted + * @param nip - which NIP is being implemented ('nip04', 'nip44', 'nip49') */ - decrypt(sender: NDKUser, value: string): Promise; + decrypt(sender: NDKUser, value: string, nip?:EncryptionNip): Promise; } From 4bc3457e3f09700842c719a36d2a77971ea240b9 Mon Sep 17 00:00:00 2001 From: manimejia Date: Tue, 4 Jun 2024 10:49:16 -0500 Subject: [PATCH 3/8] UNTESTED implement nip44 as optional encryption for NDKPrivateSigner UNTESTED with a test written --- ndk/src/signers/private-key/index.test.ts | 25 ++++++++++++++++++++++- ndk/src/signers/private-key/index.ts | 25 ++++++++++++++++++----- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/ndk/src/signers/private-key/index.test.ts b/ndk/src/signers/private-key/index.test.ts index a9f0822f..e4ec10d3 100644 --- a/ndk/src/signers/private-key/index.test.ts +++ b/ndk/src/signers/private-key/index.test.ts @@ -1,5 +1,5 @@ import { generateSecretKey } from "nostr-tools"; -import type { NostrEvent } from "../../index.js"; +import NDK, { NDKEvent, type NostrEvent } from "../../index.js"; import { NDKPrivateKeySigner } from "./index"; import { bytesToHex, hexToBytes } from "@noble/hashes/utils"; import { nip19 } from "nostr-tools"; @@ -60,4 +60,27 @@ describe("NDKPrivateKeySigner", () => { expect(signature).toBeDefined(); expect(signature.length).toBe(128); }); + + it("encrypts and decrypts an NDKEvent using Nip44", async () => { + const senderSigner = new NDKPrivateKeySigner("0277cc53c89ca9c8a441987265276fafa55bf5bed8a55b16fd640e0d6a0c21e2"); + const senderUser = await senderSigner.user(); + const recipientSigner = new NDKPrivateKeySigner("f04855b5887e20132a70c129ab67587d08733232307d337d82028ba6c81a9b0f"); + const recipientUser = await recipientSigner.user(); + + const sendEvent: NDKEvent = new NDKEvent (new NDK(), { + pubkey: senderUser.pubkey, + created_at: Math.floor(Date.now() / 1000), + tags: [], + content: "Test content", + kind: 1, + }); + + const original = sendEvent.content + await sendEvent.encrypt(recipientUser, senderSigner,'nip44'); + const recieveEvent = new NDKEvent(new NDK(), sendEvent.rawEvent()) + await recieveEvent.decrypt(senderUser, recipientSigner,'nip44'); + const decrypted = recieveEvent.content + + expect(decrypted).toBe(original); + }); }); diff --git a/ndk/src/signers/private-key/index.ts b/ndk/src/signers/private-key/index.ts index 61428705..ac5cf506 100644 --- a/ndk/src/signers/private-key/index.ts +++ b/ndk/src/signers/private-key/index.ts @@ -1,12 +1,13 @@ import type { UnsignedEvent } from "nostr-tools"; -import { generateSecretKey, getPublicKey, finalizeEvent, nip04 } from "nostr-tools"; +import { generateSecretKey, getPublicKey, finalizeEvent, nip04, nip44 } from "nostr-tools"; import type { NostrEvent } from "../../events/index.js"; import { NDKUser } from "../../user"; -import type { NDKSigner } from "../index.js"; +import type { EncryptionNip, NDKSigner } from "../index.js"; import { bytesToHex, hexToBytes } from "@noble/hashes/utils"; import { nip19 } from "nostr-tools"; + export class NDKPrivateKeySigner implements NDKSigner { private _user: NDKUser | undefined; _privateKey?: Uint8Array; @@ -37,7 +38,6 @@ export class NDKPrivateKeySigner implements NDKSigner { } } } - get privateKey(): string | undefined { if (!this._privateKey) return undefined; return bytesToHex(this._privateKey); @@ -68,21 +68,36 @@ export class NDKPrivateKeySigner implements NDKSigner { return finalizeEvent(event as UnsignedEvent, this._privateKey).sig; } - public async encrypt(recipient: NDKUser, value: string): Promise { + public async encryptionEnabled(): Promise{ + let enabled : EncryptionNip[] = ['nip04', 'nip44'] + return enabled; + } + + public async encrypt(recipient: NDKUser, value: string, nip?: EncryptionNip): Promise { if (!this._privateKey) { throw Error("Attempted to encrypt without a private key"); } const recipientHexPubKey = recipient.pubkey; + if(nip == 'nip44'){ + // TODO Deriving shared secret is an expensive computation, should be cached. + let conversationKey = nip44.v2.utils.getConversationKey(this._privateKey, recipientHexPubKey); + return await nip44.v2.encrypt(value, conversationKey); + } return await nip04.encrypt(this._privateKey, recipientHexPubKey, value); } - public async decrypt(sender: NDKUser, value: string): Promise { + public async decrypt(sender: NDKUser, value: string, nip?: EncryptionNip): Promise { if (!this._privateKey) { throw Error("Attempted to decrypt without a private key"); } const senderHexPubKey = sender.pubkey; + if(nip == 'nip44'){ + // TODO Deriving shared secret is an expensive computation, should be cached. + let conversationKey = nip44.v2.utils.getConversationKey(this._privateKey, senderHexPubKey); + return await nip44.v2.decrypt(value, conversationKey); + } return await nip04.decrypt(this._privateKey, senderHexPubKey, value); } } From 8bbf1cdd4efd10b7a8fca817088a43e4ca05601b Mon Sep 17 00:00:00 2001 From: manimejia Date: Tue, 4 Jun 2024 11:35:04 -0500 Subject: [PATCH 4/8] BUGFIX wrong import and wrong privateKey params --- ndk/src/signers/private-key/index.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ndk/src/signers/private-key/index.ts b/ndk/src/signers/private-key/index.ts index ac5cf506..ab37127c 100644 --- a/ndk/src/signers/private-key/index.ts +++ b/ndk/src/signers/private-key/index.ts @@ -3,9 +3,10 @@ import { generateSecretKey, getPublicKey, finalizeEvent, nip04, nip44 } from "no import type { NostrEvent } from "../../events/index.js"; import { NDKUser } from "../../user"; -import type { EncryptionNip, NDKSigner } from "../index.js"; +import type { NDKSigner } from "../index.js"; import { bytesToHex, hexToBytes } from "@noble/hashes/utils"; import { nip19 } from "nostr-tools"; +import { EncryptionNip } from "../../events/encryption.js"; export class NDKPrivateKeySigner implements NDKSigner { @@ -74,28 +75,28 @@ export class NDKPrivateKeySigner implements NDKSigner { } public async encrypt(recipient: NDKUser, value: string, nip?: EncryptionNip): Promise { - if (!this._privateKey) { + if (!this._privateKey || !this.privateKey) { throw Error("Attempted to encrypt without a private key"); } const recipientHexPubKey = recipient.pubkey; if(nip == 'nip44'){ // TODO Deriving shared secret is an expensive computation, should be cached. - let conversationKey = nip44.v2.utils.getConversationKey(this._privateKey, recipientHexPubKey); + let conversationKey = nip44.v2.utils.getConversationKey(this.privateKey, recipientHexPubKey); return await nip44.v2.encrypt(value, conversationKey); } return await nip04.encrypt(this._privateKey, recipientHexPubKey, value); } public async decrypt(sender: NDKUser, value: string, nip?: EncryptionNip): Promise { - if (!this._privateKey) { + if (!this._privateKey || !this.privateKey) { throw Error("Attempted to decrypt without a private key"); } const senderHexPubKey = sender.pubkey; if(nip == 'nip44'){ // TODO Deriving shared secret is an expensive computation, should be cached. - let conversationKey = nip44.v2.utils.getConversationKey(this._privateKey, senderHexPubKey); + let conversationKey = nip44.v2.utils.getConversationKey(this.privateKey, senderHexPubKey); return await nip44.v2.decrypt(value, conversationKey); } return await nip04.decrypt(this._privateKey, senderHexPubKey, value); From e23795acc630b141ca8ee8485f83594e4f2c33ef Mon Sep 17 00:00:00 2001 From: manimejia Date: Wed, 5 Jun 2024 09:40:46 -0500 Subject: [PATCH 5/8] ADDED Nip44 support for NDKNip07Signer also some buigfiixes for encryptionEnabled() UNTESTED : this commit has not been tested yet. --- ndk/src/events/encryption.ts | 29 +++++------ ndk/src/signers/index.ts | 3 +- ndk/src/signers/nip07/index.ts | 74 ++++++++++++++++------------ ndk/src/signers/private-key/index.ts | 6 ++- 4 files changed, 61 insertions(+), 51 deletions(-) diff --git a/ndk/src/events/encryption.ts b/ndk/src/events/encryption.ts index 5bc9ef0d..01ade652 100644 --- a/ndk/src/events/encryption.ts +++ b/ndk/src/events/encryption.ts @@ -7,6 +7,7 @@ import type { NDKUser } from "../user"; import type { NDKEvent } from "./index.js"; export type EncryptionNip = 'nip04' | 'nip44'; +export type EncryptionMethod = 'encrypt' | 'decrypt' // some clients may wish to set a default for message encryption... // TODO how should replies to 'nip04' encrypted messages be handled? @@ -22,12 +23,13 @@ export async function encrypt( signer?: NDKSigner, nip : EncryptionNip | undefined = defaultEncryption ): Promise { - let encrypted : string; + let encrypted : string | undefined; if (!this.ndk) throw new Error("No NDK instance found!"); if (!signer) { await this.ndk.assertSigner(); signer = this.ndk.signer; } + if(!signer) throw new Error('no NDK signer'); if (!recipient) { const pTags = this.getMatchingTags("p"); @@ -41,12 +43,12 @@ export async function encrypt( } // support for encrypting events via legacy `nip04`. adapted from Coracle - if ((!nip || nip == 'nip04') && isNip04Enabled(signer)) { + if ((!nip || nip == 'nip04') && await isEncryptionEnabled(signer, 'nip04')) { try{ encrypted = (await signer?.encrypt(recipient, this.content, 'nip04')) as string; }catch{} } - if ((!encrypted || nip == "nip44") && isNip44Enabled(signer)) { + if ((!encrypted || nip == "nip44") && await isEncryptionEnabled(signer, 'nip44')) { encrypted = (await signer?.encrypt(recipient, this.content, 'nip44')) as string; } if(!encrypted) throw new Error('Failed to encrypt event.') @@ -54,36 +56,31 @@ export async function encrypt( } export async function decrypt(this: NDKEvent, sender?: NDKUser, signer?: NDKSigner, nip: EncryptionNip | undefined = defaultEncryption): Promise { - let decrypted : string; + let decrypted : string | undefined; if (!this.ndk) throw new Error("No NDK instance found!"); if (!signer) { await this.ndk.assertSigner(); signer = this.ndk.signer; } + if(!signer) throw new Error('no NDK signer'); if (!sender) { sender = this.author; } // simple check for legacy `nip04` encrypted events. adapted from Coracle - if ((!nip || nip=='nip04') && isNip04Enabled(signer) && this.content.search("?iv=")) { + if ((!nip || nip=='nip04') && await isEncryptionEnabled(signer, 'nip04') && this.content.search("?iv=")) { try{ decrypted = (await signer?.decrypt(sender, this.content, 'nip04')) as string; }catch{} } - if (!decrypted && isNip44Enabled(signer)) { + if (!decrypted && await isEncryptionEnabled(signer, 'nip44')) { decrypted = (await signer?.decrypt(sender, this.content, 'nip44')) as string; } if(!decrypted) throw new Error('Failed to decrypt event.') this.content = decrypted } -async function isNip04Enabled(signer : NDKSigner){ - let enabled = await signer.encryptionEnabled(); - if(enabled.indexOf('nip04') != -1) return true; - return false; +async function isEncryptionEnabled(signer : NDKSigner, nip? : EncryptionNip){ + if(!signer.encryptionEnabled) return false; + if(!nip) return true; + return Boolean(await signer.encryptionEnabled(nip)); } - -async function isNip44Enabled(signer : NDKSigner){ - let enabled = await signer.encryptionEnabled(); - if(enabled.indexOf('nip44') != -1) return true; - return false; -} \ No newline at end of file diff --git a/ndk/src/signers/index.ts b/ndk/src/signers/index.ts index f3d9de37..fda7971e 100644 --- a/ndk/src/signers/index.ts +++ b/ndk/src/signers/index.ts @@ -36,9 +36,10 @@ export interface NDKSigner { /** * Determine the types of encryption (by nip) that this signer can perform. * Implementing classes SHOULD return a value even for legacy (only nip04) third party signers. + * @nip Optionally returns an array with single supported nip or empty, to check for truthy or falsy. * @return A promised list of any (or none) of these strings ['nip04', 'nip44'] */ - encryptionEnabled?(): Promise + encryptionEnabled?(nip?:EncryptionNip): Promise /** * Encrypts the given Nostr event for the given recipient. diff --git a/ndk/src/signers/nip07/index.ts b/ndk/src/signers/nip07/index.ts index 0a6500f6..8d36f359 100644 --- a/ndk/src/signers/nip07/index.ts +++ b/ndk/src/signers/nip07/index.ts @@ -4,9 +4,11 @@ import type { NostrEvent } from "../../events/index.js"; import { NDKUser } from "../../user/index.js"; import type { NDKSigner } from "../index.js"; import { NDKRelay } from "../../relay/index.js"; +import { EncryptionMethod, EncryptionNip } from "../../events/encryption.js"; -type Nip04QueueItem = { - type: "encrypt" | "decrypt"; +type EncryptionQueueItem = { + nip : EncryptionNip; + method: EncryptionMethod; counterpartyHexpubkey: string; value: string; resolve: (value: string) => void; @@ -26,8 +28,8 @@ type Nip07RelayMap = { */ export class NDKNip07Signer implements NDKSigner { private _userPromise: Promise | undefined; - public nip04Queue: Nip04QueueItem[] = []; - private nip04Processing = false; + public encryptionQueue: EncryptionQueueItem[] = []; + private encryptionProcessing = false; private debug: debug.Debugger; private waitTimeout: number; @@ -92,65 +94,69 @@ export class NDKNip07Signer implements NDKSigner { return activeRelays.map((url) => new NDKRelay(url)); } - public async encrypt(recipient: NDKUser, value: string): Promise { + public async encryptionEnabled(nip?:EncryptionNip): Promise{ + let enabled : EncryptionNip[] = [] + if((!nip || nip == 'nip04') && Boolean((window as any).nostr!.nip04)) enabled.push('nip04') + if((!nip || nip == 'nip44') && Boolean((window as any).nostr!.nip44)) enabled.push('nip44') + return enabled; + } + + public async encrypt(recipient: NDKUser, value: string, nip:EncryptionNip = 'nip04'): Promise { + if( !(await this.encryptionEnabled(nip)) ) throw new Error(nip + 'encryption is not available from your browser extension') await this.waitForExtension(); const recipientHexPubKey = recipient.pubkey; - return this.queueNip04("encrypt", recipientHexPubKey, value); + return this.queueEncryption(nip, "encrypt", recipientHexPubKey, value); } - public async decrypt(sender: NDKUser, value: string): Promise { + public async decrypt(sender: NDKUser, value: string, nip:EncryptionNip = 'nip04'): Promise { + if( !(await this.encryptionEnabled(nip)) ) throw new Error(nip + 'encryption is not available from your browser extension') await this.waitForExtension(); const senderHexPubKey = sender.pubkey; - return this.queueNip04("decrypt", senderHexPubKey, value); + return this.queueEncryption(nip, "decrypt", senderHexPubKey, value); } - private async queueNip04( - type: "encrypt" | "decrypt", + private async queueEncryption( + nip : EncryptionNip, + method: EncryptionMethod, counterpartyHexpubkey: string, value: string ): Promise { return new Promise((resolve, reject) => { - this.nip04Queue.push({ - type, + this.encryptionQueue.push({ + nip, + method, counterpartyHexpubkey, value, resolve, reject, }); - if (!this.nip04Processing) { - this.processNip04Queue(); + if (!this.encryptionProcessing) { + this.processEncryptionQueue(); } }); } - private async processNip04Queue(item?: Nip04QueueItem, retries = 0): Promise { - if (!item && this.nip04Queue.length === 0) { - this.nip04Processing = false; + private async processEncryptionQueue(item?: EncryptionQueueItem, retries = 0): Promise { + if (!item && this.encryptionQueue.length === 0) { + this.encryptionProcessing = false; return; } - this.nip04Processing = true; - const { type, counterpartyHexpubkey, value, resolve, reject } = - item || this.nip04Queue.shift()!; + this.encryptionProcessing = true; + const {nip, method, counterpartyHexpubkey, value, resolve, reject } = + item || this.encryptionQueue.shift()!; this.debug("Processing encryption queue item", { - type, + method, counterpartyHexpubkey, value, }); try { - let result; - - if (type === "encrypt") { - result = await window.nostr!.nip04!.encrypt(counterpartyHexpubkey, value); - } else { - result = await window.nostr!.nip04!.decrypt(counterpartyHexpubkey, value); - } - + let result = await window.nostr![nip]![method](counterpartyHexpubkey, value) resolve(result); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { @@ -158,13 +164,13 @@ export class NDKNip07Signer implements NDKSigner { if (error.message && error.message.includes("call already executing")) { if (retries < 5) { this.debug("Retrying encryption queue item", { - type, + method, counterpartyHexpubkey, value, retries, }); setTimeout(() => { - this.processNip04Queue(item, retries + 1); + this.processEncryptionQueue(item, retries + 1); }, 50 * retries); return; @@ -173,7 +179,7 @@ export class NDKNip07Signer implements NDKSigner { reject(error); } - this.processNip04Queue(); + this.processEncryptionQueue(); } private waitForExtension(): Promise { @@ -213,6 +219,10 @@ declare global { encrypt(recipientHexPubKey: string, value: string): Promise; decrypt(senderHexPubKey: string, value: string): Promise; }; + nip44?: { + encrypt(recipientHexPubKey: string, value: string): Promise; + decrypt(senderHexPubKey: string, value: string): Promise; + }; }; } } diff --git a/ndk/src/signers/private-key/index.ts b/ndk/src/signers/private-key/index.ts index ab37127c..d5b2e559 100644 --- a/ndk/src/signers/private-key/index.ts +++ b/ndk/src/signers/private-key/index.ts @@ -69,8 +69,10 @@ export class NDKPrivateKeySigner implements NDKSigner { return finalizeEvent(event as UnsignedEvent, this._privateKey).sig; } - public async encryptionEnabled(): Promise{ - let enabled : EncryptionNip[] = ['nip04', 'nip44'] + public async encryptionEnabled(nip?:EncryptionNip): Promise{ + let enabled : EncryptionNip[] = [] + if((!nip || nip == 'nip04')) enabled.push('nip04') + if((!nip || nip == 'nip44')) enabled.push('nip44') return enabled; } From 1e7c9017e9c6c1fd926b8f517e4e50b81be3ecac Mon Sep 17 00:00:00 2001 From: manimejia Date: Sat, 8 Jun 2024 16:15:56 -0500 Subject: [PATCH 6/8] UNTESTED adds Nip59 giftWrap and giftUnwrap for NDKEvents --- ndk/src/events/encryption.ts | 123 +++++++++++++++++++++++++++++++++-- ndk/src/events/index.ts | 4 +- 2 files changed, 122 insertions(+), 5 deletions(-) diff --git a/ndk/src/events/encryption.ts b/ndk/src/events/encryption.ts index 01ade652..19f5059a 100644 --- a/ndk/src/events/encryption.ts +++ b/ndk/src/events/encryption.ts @@ -1,10 +1,15 @@ /** * Encryption and giftwrapping of events - * Implemnents Nip04, Nip44, (TODO) Nip59 + * Implemnents Nip04, Nip44, Nip59 */ import type { NDKSigner } from "../signers"; -import type { NDKUser } from "../user"; -import type { NDKEvent } from "./index.js"; +import { NDKUser } from "../user"; +import { NDKEvent, NostrEvent } from "./index.js"; +import { NDKPrivateKeySigner } from "../signers/private-key"; +import { VerifiedEvent, getEventHash, verifiedSymbol } from "nostr-tools"; + + +// NIP04 && NIP44 export type EncryptionNip = 'nip04' | 'nip44'; export type EncryptionMethod = 'encrypt' | 'decrypt' @@ -16,7 +21,6 @@ export function useEncryption(nip : EncryptionNip){ defaultEncryption = nip; } - export async function encrypt( this: NDKEvent, recipient?: NDKUser, @@ -84,3 +88,114 @@ async function isEncryptionEnabled(signer : NDKSigner, nip? : EncryptionNip){ if(!nip) return true; return Boolean(await signer.encryptionEnabled(nip)); } + + +// NIP 59 - adapted from Coracle + +export type GiftWrapParams = { + encryptionNip?: EncryptionNip + rumorKind?: number; + wrapKind?: 1059 | 1060 + wrapTags?: string[][] +} + +/** + * Instantiate a new (Nip59 gift wrapped) NDKEvent from any NDKevent + * @param this + * @param recipient + * @param signer + * @param params + * @returns + */ +export async function giftWrap(this:NDKEvent, recipient: NDKUser, signer?:NDKSigner, params:GiftWrapParams = {}) : Promise{ + params.encryptionNip = params.encryptionNip || 'nip44'; + if(!signer){ + if(!this.ndk) + throw new Error('no signer available for giftWrap') + signer = this.ndk.signer; + } + if(!signer) + throw new Error('no signer') + if(!signer.encryptionEnabled || !signer.encryptionEnabled(params.encryptionNip)) + throw new Error('signer is not able to giftWrap') + const rumor = getRumorEvent(this, params?.rumorKind) + const seal = await getSealEvent(rumor, recipient, signer, params.encryptionNip); + const wrap = await getWrapEvent(seal, recipient, params); + return new NDKEvent(this.ndk, wrap); +} + +/** + * Instantiate a new (Nip59 un-wrapped rumor) NDKEvent from any gift wrapped NDKevent + * @param this + */ +export async function giftUnwrap(this:NDKEvent, sender?:NDKUser, signer?:NDKSigner, nip:EncryptionNip = 'nip44') : Promise{ + sender = sender || new NDKUser({pubkey:this.pubkey}) + if(!signer){ + if(!this.ndk) + throw new Error('no signer available for giftUnwrap') + signer = this.ndk.signer; + } + if(!signer) + throw new Error('no signer') + try { + + const seal = JSON.parse(await signer.decrypt(sender, this.content, nip)); + if (!seal) throw new Error("Failed to decrypt wrapper") + + const rumor = JSON.parse(await signer.decrypt(sender, seal.content, nip)) + if (!rumor) throw new Error("Failed to decrypt seal") + + if (seal.pubkey === rumor.pubkey) { + return new NDKEvent(this.ndk, rumor as NostrEvent) + } + } catch (e) { + console.log(e) + } + return null + } + + + +function getRumorEvent(event:NDKEvent, kind?:number):NDKEvent{ + let rumor = event.rawEvent(); + rumor.kind = kind || rumor.kind || 1; + rumor.sig = undefined; + rumor.id = getEventHash(rumor as any); + return new NDKEvent(event.ndk, rumor) +} + +async function getSealEvent(rumor : NDKEvent, recipient : NDKUser, signer:NDKSigner, nip:EncryptionNip = 'nip44') : Promise{ + const content = await signer.encrypt(recipient, JSON.stringify(rumor), nip); + let seal : any = { + kind: 13, + created_at: aproximateNow(5), + tags: [], + content , + pubkey : rumor.pubkey + } + seal.id = getEventHash(seal), + seal.sig = await signer.sign(seal); + seal[verifiedSymbol] = true + return seal; +} + +async function getWrapEvent(sealed:VerifiedEvent, recipient:NDKUser, params? : GiftWrapParams) : Promise{ + const signer = NDKPrivateKeySigner.generate(); + const content = await signer.encrypt(recipient, JSON.stringify(sealed), params?.encryptionNip || 'nip44') + const pubkey = (await signer.user()).pubkey + let wrap : any = { + kind : params?.wrapKind || 1059, + created_at: aproximateNow(5), + tags: (params?.wrapTags || []).concat([["p", recipient.pubkey]]), + content, + pubkey, + } + wrap.id = getEventHash(wrap); + wrap.sig = await signer.sign(wrap); + wrap[verifiedSymbol] = true + return wrap; +} + +function aproximateNow(drift = 0){ + return Math.round(Date.now() / 1000 - Math.random() * Math.pow(10, drift)) +} diff --git a/ndk/src/events/index.ts b/ndk/src/events/index.ts index 984bcffe..c4e7e8de 100644 --- a/ndk/src/events/index.ts +++ b/ndk/src/events/index.ts @@ -10,7 +10,7 @@ import { type NDKUser } from "../user/index.js"; import { type ContentTag, generateContentTags, mergeTags } from "./content-tagger.js"; import { isEphemeral, isParamReplaceable, isReplaceable } from "./kind.js"; import { NDKKind } from "./kinds/index.js"; -import { decrypt, encrypt } from "./encryption.js"; +import { decrypt, encrypt, giftUnwrap, giftWrap } from "./encryption.js"; import { encode } from "./nip19.js"; import { repost } from "./repost.js"; import { fetchReplyEvent, fetchRootEvent, fetchTaggedEvent } from "./fetch-tagged-event.js"; @@ -231,6 +231,8 @@ export class NDKEvent extends EventEmitter { public encode = encode.bind(this); public encrypt = encrypt.bind(this); public decrypt = decrypt.bind(this); + public giftWrap = giftWrap.bind(this); + public giftUnwrap = giftUnwrap.bind(this); /** * Get all tags with the given name From 0a776a280a70c7a9bede4956fa961334b2eee578 Mon Sep 17 00:00:00 2001 From: manimejia Date: Wed, 12 Jun 2024 10:06:53 -0500 Subject: [PATCH 7/8] ADDED tests to encryption.test.ts coppied Nip44 test for encrypt and decrypt new Nip59 test for gift wrap and unwrap --- ndk/src/events/encryption.test.ts | 56 +++++++++++++++++++++++ ndk/src/signers/private-key/index.test.ts | 23 ---------- 2 files changed, 56 insertions(+), 23 deletions(-) create mode 100644 ndk/src/events/encryption.test.ts diff --git a/ndk/src/events/encryption.test.ts b/ndk/src/events/encryption.test.ts new file mode 100644 index 00000000..297089bf --- /dev/null +++ b/ndk/src/events/encryption.test.ts @@ -0,0 +1,56 @@ +import { NDKEvent } from "."; +import { NDK } from "../ndk"; +import { NDKPrivateKeySigner } from "../signers/private-key"; + +const PRIVATE_KEY_1_FOR_TESTING = '1fbc12b81e0b21f10fb219e88dd76fc80c7aa5369779e44e762fec6f460d6a89'; +const PRIVATE_KEY_2_FOR_TESTING = "d30b946562050e6ced827113da15208730879c46547061b404434edff63236fa"; + + +describe("NDKEvent encryption (Nip44 & Nip59)", ()=>{ + + it("encrypts and decrypts an NDKEvent using Nip44", async () => { + const senderSigner = new NDKPrivateKeySigner(PRIVATE_KEY_1_FOR_TESTING); + const senderUser = await senderSigner.user(); + const recipientSigner = new NDKPrivateKeySigner(PRIVATE_KEY_2_FOR_TESTING); + const recipientUser = await recipientSigner.user(); + + const sendEvent: NDKEvent = new NDKEvent (new NDK(), { + pubkey: senderUser.pubkey, + created_at: Math.floor(Date.now() / 1000), + tags: [], + content: "Test content", + kind: 1, + }); + + const original = sendEvent.content + await sendEvent.encrypt(recipientUser, senderSigner,'nip44'); + const recieveEvent = new NDKEvent(new NDK(), sendEvent.rawEvent()) + await recieveEvent.decrypt(senderUser, recipientSigner,'nip44'); + const decrypted = recieveEvent.content + + expect(decrypted).toBe(original); + }); + + it("gift wraps and unwraps an NDKEvent using Nip59", async () => { + const sendsigner = new NDKPrivateKeySigner(PRIVATE_KEY_1_FOR_TESTING) + const senduser = await sendsigner.user(); + const recievesigner = new NDKPrivateKeySigner(PRIVATE_KEY_2_FOR_TESTING) + const recieveuser = await recievesigner.user(); + + let message = new NDKEvent(new NDK(),{ + kind : 1, + pubkey : senduser.pubkey, + content : "hello world", + created_at : new Date().valueOf(), + tags : [] + }) + + // console.log('MESSAGE EVENT : '+ JSON.stringify(message.rawEvent())) + const wrapped = await message.giftWrap(recieveuser,sendsigner); + // console.log('MESSAGE EVENT WRAPPED : '+ JSON.stringify(wrapped.rawEvent())) + const unwrapped = await wrapped.giftUnwrap(senduser,recievesigner) + // console.log('MESSAGE EVENT UNWRAPPED : '+ JSON.stringify(unwrapped?.rawEvent())) + expect(unwrapped).toBe(message) + }); + +}) diff --git a/ndk/src/signers/private-key/index.test.ts b/ndk/src/signers/private-key/index.test.ts index e4ec10d3..d4778212 100644 --- a/ndk/src/signers/private-key/index.test.ts +++ b/ndk/src/signers/private-key/index.test.ts @@ -60,27 +60,4 @@ describe("NDKPrivateKeySigner", () => { expect(signature).toBeDefined(); expect(signature.length).toBe(128); }); - - it("encrypts and decrypts an NDKEvent using Nip44", async () => { - const senderSigner = new NDKPrivateKeySigner("0277cc53c89ca9c8a441987265276fafa55bf5bed8a55b16fd640e0d6a0c21e2"); - const senderUser = await senderSigner.user(); - const recipientSigner = new NDKPrivateKeySigner("f04855b5887e20132a70c129ab67587d08733232307d337d82028ba6c81a9b0f"); - const recipientUser = await recipientSigner.user(); - - const sendEvent: NDKEvent = new NDKEvent (new NDK(), { - pubkey: senderUser.pubkey, - created_at: Math.floor(Date.now() / 1000), - tags: [], - content: "Test content", - kind: 1, - }); - - const original = sendEvent.content - await sendEvent.encrypt(recipientUser, senderSigner,'nip44'); - const recieveEvent = new NDKEvent(new NDK(), sendEvent.rawEvent()) - await recieveEvent.decrypt(senderUser, recipientSigner,'nip44'); - const decrypted = recieveEvent.content - - expect(decrypted).toBe(original); - }); }); From a02e181f5e1a95ec176aebded04eb335297d2bdd Mon Sep 17 00:00:00 2001 From: manimejia Date: Mon, 17 Jun 2024 11:26:54 -0500 Subject: [PATCH 8/8] UPDATE add encryoption kinds to NDKKind enum --- ndk/src/events/kinds/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ndk/src/events/kinds/index.ts b/ndk/src/events/kinds/index.ts index 9c80eee4..95674280 100644 --- a/ndk/src/events/kinds/index.ts +++ b/ndk/src/events/kinds/index.ts @@ -14,6 +14,12 @@ export enum NDKKind { GroupNote = 11, GroupReply = 12, + // Nip 59 : Gift Wrap + GiftWrap = 1059, + GiftWrapSeal = 13, + // Gift Wrapped Rumors + PrivateDirectMessage = 14, + GenericRepost = 16, ChannelCreation = 40, ChannelMetadata = 41,