Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nip44 Encryption & Nip59 Gift Wrapping #233

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
56 changes: 56 additions & 0 deletions ndk/src/events/encryption.test.ts
Original file line number Diff line number Diff line change
@@ -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)
});

})
201 changes: 201 additions & 0 deletions ndk/src/events/encryption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/**
* Encryption and giftwrapping of events
* Implemnents Nip04, Nip44, Nip59
*/
import type { NDKSigner } from "../signers";
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'

// 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,
nip : EncryptionNip | undefined = defaultEncryption
): Promise<void> {
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");

if (pTags.length !== 1) {
throw new Error(
"No recipient could be determined and no explicit recipient was provided"
);
}

recipient = this.ndk.getUser({ pubkey: pTags[0][1] });
}

// support for encrypting events via legacy `nip04`. adapted from Coracle
if ((!nip || nip == 'nip04') && await isEncryptionEnabled(signer, 'nip04')) {
try{
encrypted = (await signer?.encrypt(recipient, this.content, 'nip04')) as string;
}catch{}
}
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.')
this.content = encrypted
}

export async function decrypt(this: NDKEvent, sender?: NDKUser, signer?: NDKSigner, nip: EncryptionNip | undefined = defaultEncryption): Promise<void> {
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') && await isEncryptionEnabled(signer, 'nip04') && this.content.search("?iv=")) {
try{
decrypted = (await signer?.decrypt(sender, this.content, 'nip04')) as string;
}catch{}
}
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 isEncryptionEnabled(signer : NDKSigner, nip? : EncryptionNip){
if(!signer.encryptionEnabled) return false;
if(!nip) return true;
return Boolean(await signer.encryptionEnabled(nip));
}


// NIP 59 - adapted from Coracle

export type GiftWrapParams = {
manimejia marked this conversation as resolved.
Show resolved Hide resolved
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<NDKEvent>{
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<NDKEvent | null>{
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<VerifiedEvent>{
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<VerifiedEvent>{
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))
}
4 changes: 3 additions & 1 deletion ndk/src/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, giftUnwrap, giftWrap } from "./encryption.js";
import { encode } from "./nip19.js";
import { repost } from "./repost.js";
import { fetchReplyEvent, fetchRootEvent, fetchTaggedEvent } from "./fetch-tagged-event.js";
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions ndk/src/events/kinds/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
41 changes: 0 additions & 41 deletions ndk/src/events/nip04.ts

This file was deleted.

23 changes: 19 additions & 4 deletions ndk/src/signers/index.ts
Original file line number Diff line number Diff line change
@@ -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.
*/
Expand Down Expand Up @@ -31,16 +33,29 @@ export interface NDKSigner {
*/
relays?(): Promise<NDKRelay[]>;

/**
* 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?(nip?:EncryptionNip): Promise<EncryptionNip[]>

/**
* 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<string>;
encrypt(recipient: NDKUser, value: string, nip?:EncryptionNip): Promise<string>;

/**
* 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<string>;
decrypt(sender: NDKUser, value: string, nip?:EncryptionNip): Promise<string>;
}
Loading