From 68b9a89278a7e6fdb9fa714a31275e87a5c14646 Mon Sep 17 00:00:00 2001 From: Kieran Date: Wed, 5 Jul 2023 11:41:47 +0100 Subject: [PATCH] NIP-46 draft --- packages/app/src/Hooks/useLoginHandler.tsx | 8 ++ packages/system/src/event-builder.ts | 15 +- packages/system/src/event-ext.ts | 3 +- packages/system/src/event-publisher.ts | 73 ++-------- packages/system/src/impl/nip46.ts | 158 +++++++++++++++++++++ packages/system/src/impl/nip7.ts | 61 ++++++++ packages/system/src/index.ts | 2 + packages/system/src/query.ts | 1 - 8 files changed, 256 insertions(+), 65 deletions(-) create mode 100644 packages/system/src/impl/nip46.ts create mode 100644 packages/system/src/impl/nip7.ts diff --git a/packages/app/src/Hooks/useLoginHandler.tsx b/packages/app/src/Hooks/useLoginHandler.tsx index aa7a3b8f..62af3f18 100644 --- a/packages/app/src/Hooks/useLoginHandler.tsx +++ b/packages/app/src/Hooks/useLoginHandler.tsx @@ -5,6 +5,7 @@ import { LoginStore } from "Login"; import { generateBip39Entropy, entropyToPrivateKey } from "nip6"; import { getNip05PubKey } from "Pages/LoginPage"; import { bech32ToHex } from "SnortUtils"; +import { Nip7Signer, Nip46Signer } from "@snort/system"; export default function useLoginHandler() { const { formatMessage } = useIntl(); @@ -43,6 +44,13 @@ export default function useLoginHandler() { throw new Error(insecureMsg); } LoginStore.loginWithPrivateKey(key); + } else if (key.startsWith("bunker://")) { + const inner = new Nip7Signer(); + const nip46 = new Nip46Signer(key, inner); + await nip46.init(); + + const loginPubkey = await nip46.getPubKey(); + LoginStore.loginWithPubkey(loginPubkey); } else { throw new Error("INVALID PRIVATE KEY"); } diff --git a/packages/system/src/event-builder.ts b/packages/system/src/event-builder.ts index 54379fbf..ac4980dd 100644 --- a/packages/system/src/event-builder.ts +++ b/packages/system/src/event-builder.ts @@ -1,4 +1,4 @@ -import { EventKind, HexKey, NostrPrefix, NostrEvent } from "."; +import { EventKind, HexKey, NostrPrefix, NostrEvent, EventSigner } from "."; import { HashtagRegex } from "./const"; import { getPublicKey, unixNow } from "@snort/shared"; import { EventExt } from "./event-ext"; @@ -73,10 +73,15 @@ export class EventBuilder { * Build and sign event * @param pk Private key to sign event with */ - async buildAndSign(pk: HexKey) { - const ev = this.pubKey(getPublicKey(pk)).build(); - await EventExt.sign(ev, pk); - return ev; + async buildAndSign(pk: HexKey | EventSigner) { + if (typeof pk === "string") { + const ev = this.pubKey(getPublicKey(pk)).build(); + EventExt.sign(ev, pk); + return ev; + } else { + const ev = this.pubKey(await pk.getPubKey()).build(); + return await pk.sign(ev); + } } #validate() { diff --git a/packages/system/src/event-ext.ts b/packages/system/src/event-ext.ts index cad639db..6a631f45 100644 --- a/packages/system/src/event-ext.ts +++ b/packages/system/src/event-ext.ts @@ -1,6 +1,6 @@ import * as secp from "@noble/curves/secp256k1"; import * as utils from "@noble/curves/abstract/utils"; -import { sha256, unixNow } from "@snort/shared"; +import { getPublicKey, sha256, unixNow } from "@snort/shared"; import { EventKind, HexKey, NostrEvent } from "."; import { Nip4WebCryptoEncryptor } from "./impl/nip4"; @@ -36,6 +36,7 @@ export abstract class EventExt { * Sign this message with a private key */ static sign(e: NostrEvent, key: HexKey) { + e.pubkey = getPublicKey(key); e.id = this.createId(e); const sig = secp.schnorr.sign(e.id, key); diff --git a/packages/system/src/event-publisher.ts b/packages/system/src/event-publisher.ts index 4b46e2e5..edbcb8ff 100644 --- a/packages/system/src/event-publisher.ts +++ b/packages/system/src/event-publisher.ts @@ -1,6 +1,6 @@ import * as secp from "@noble/curves/secp256k1"; import * as utils from "@noble/curves/abstract/utils"; -import { unwrap, barrierQueue, processWorkQueue, WorkQueueItem, getPublicKey } from "@snort/shared"; +import { unwrap, getPublicKey } from "@snort/shared"; import { EventKind, @@ -18,76 +18,33 @@ import { import { EventBuilder } from "./event-builder"; import { EventExt } from "./event-ext"; import { findTag } from "./utils"; +import { Nip7Signer } from "./impl/nip7"; -const Nip7Queue: Array = []; -processWorkQueue(Nip7Queue); -export type EventBuilderHook = (ev: EventBuilder) => EventBuilder; - -declare global { - interface Window { - nostr?: { - getPublicKey: () => Promise; - signEvent: (event: T) => Promise; - - getRelays?: () => Promise>; - - nip04?: { - encrypt?: (pubkey: HexKey, plaintext: string) => Promise; - decrypt?: (pubkey: HexKey, ciphertext: string) => Promise; - }; - }; - } -} +type EventBuilderHook = (ev: EventBuilder) => EventBuilder; export interface EventSigner { + init(): Promise; getPubKey(): Promise | string; nip4Encrypt(content: string, key: string): Promise; nip4Decrypt(content: string, otherKey: string): Promise; sign(ev: NostrEvent): Promise; } -export class Nip7Signer implements EventSigner { - async getPubKey(): Promise { - if (!window.nostr) { - throw new Error("Cannot use NIP-07 signer, not found!"); - } - return await window.nostr.getPublicKey(); - } - - async nip4Encrypt(content: string, key: string): Promise { - if (!window.nostr) { - throw new Error("Cannot use NIP-07 signer, not found!"); - } - return await barrierQueue(Nip7Queue, () => - unwrap(window.nostr?.nip04?.encrypt).call(window.nostr?.nip04, key, content) - ); - } - - async nip4Decrypt(content: string, otherKey: string): Promise { - if (!window.nostr) { - throw new Error("Cannot use NIP-07 signer, not found!"); - } - return await barrierQueue(Nip7Queue, () => - unwrap(window.nostr?.nip04?.decrypt).call(window.nostr?.nip04, otherKey, content) - ); - } - - async sign(ev: NostrEvent): Promise { - if (!window.nostr) { - throw new Error("Cannot use NIP-07 signer, not found!"); - } - return await barrierQueue(Nip7Queue, () => unwrap(window.nostr).signEvent(ev)); - } - -} - export class PrivateKeySigner implements EventSigner { #publicKey: string; #privateKey: string; - constructor(privateKey: string) { - this.#privateKey = privateKey; - this.#publicKey = getPublicKey(privateKey); + constructor(privateKey: string | Uint8Array) { + if (typeof privateKey === "string") { + this.#privateKey = privateKey; + } else { + this.#privateKey = utils.bytesToHex(privateKey); + } + this.#publicKey = getPublicKey(this.#privateKey); + } + + init(): Promise { + return Promise.resolve(); } getPubKey(): string { diff --git a/packages/system/src/impl/nip46.ts b/packages/system/src/impl/nip46.ts new file mode 100644 index 00000000..aaf0e8bb --- /dev/null +++ b/packages/system/src/impl/nip46.ts @@ -0,0 +1,158 @@ +import { unwrap, bech32ToHex } from "@snort/shared"; +import { secp256k1 } from "@noble/curves/secp256k1"; +import { v4 as uuid } from "uuid"; +import debug from "debug"; + +import { Connection } from "../connection"; +import { EventSigner, PrivateKeySigner } from "../event-publisher"; +import { NostrEvent } from "../nostr"; +import { EventBuilder } from "../event-builder"; +import EventKind from "../event-kind"; + +const NIP46_KIND = 24_133; + +interface Nip46Metadata { + name: string + url?: string + description?: string + icons?: Array +} + +interface Nip46Request { + id: string + method: string + params: Array +} + +interface Nip46Response { + id: string + result: any + error?: string +} + +interface QueueObj { + resolve: (o: Nip46Response) => void; + reject: (e: Error) => void; +} + +export class Nip46Signer implements EventSigner { + #conn?: Connection; + #relay: string; + #target: string; + #token?: string; + #insideSigner: EventSigner; + #commandQueue: Map = new Map(); + #log = debug("NIP-46"); + + constructor(config: string, insideSigner?: EventSigner) { + const u = new URL(config); + this.#target = u.pathname.substring(2); + + if (u.hash.length > 1) { + this.#token = u.hash.substring(1); + } + if (this.#target.startsWith("npub")) { + this.#target = bech32ToHex(this.#target); + } + + this.#relay = unwrap(u.searchParams.get("relay")); + this.#insideSigner = insideSigner ?? new PrivateKeySigner(secp256k1.utils.randomPrivateKey()) + } + + async init() { + return await new Promise((resolve, reject) => { + this.#conn = new Connection(this.#relay, { read: true, write: true }); + this.#conn.OnEvent = async (sub, e) => { + await this.#onReply(e); + } + this.#conn.OnConnected = async () => { + const insidePubkey = await this.#insideSigner.getPubKey(); + this.#conn!.QueueReq(["REQ", "reply", { + kinds: [NIP46_KIND], + authors: [this.#target], + "#p": [insidePubkey] + }], () => { }); + + const rsp = await this.#connect(insidePubkey); + if (rsp === "ack") { + resolve(); + } else { + reject(); + } + } + this.#conn.Connect(); + }) + + } + + async getPubKey() { + return await this.#rpc("get_public_key", []); + } + + async nip4Encrypt(content: string, otherKey: string) { + return await this.#rpc("nip04_encrypt", [otherKey, content]); + } + + async nip4Decrypt(content: string, otherKey: string) { + return await this.#rpc("nip04_decrypt", [otherKey, content]); + } + + async sign(ev: NostrEvent) { + return await this.#rpc("nip04_decrypt", [ev]); + } + + async #connect(pk: string) { + const connectParams = [pk]; + if (this.#token) { + connectParams.push(this.#token); + } + return await this.#rpc("connect", connectParams); + } + + async #onReply(e: NostrEvent) { + if (e.kind !== NIP46_KIND) { + throw new Error("Unknown event kind"); + } + + const decryptedContent = await this.#insideSigner.nip4Decrypt(e.content, this.#target); + const reply = JSON.parse(decryptedContent) as Nip46Response; + + const pending = this.#commandQueue.get(reply.id); + if (!pending) { + throw new Error("No pending command found"); + } + + pending.resolve(reply); + this.#commandQueue.delete(reply.id); + } + + async #rpc(method: string, params: Array) { + if (!this.#conn) throw new Error("Connection error"); + + const id = uuid(); + const payload = { + id, + method, + params, + } as Nip46Request; + this.#log("Request: %O", payload); + + const eb = new EventBuilder(); + eb.kind(NIP46_KIND as EventKind) + .content(await this.#insideSigner.nip4Encrypt(JSON.stringify(payload), this.#target)) + .tag(["p", this.#target]); + + const evCommand = await eb.buildAndSign(this.#insideSigner); + await this.#conn.SendAsync(evCommand); + + return await new Promise((resolve, reject) => { + this.#commandQueue.set(id, { + resolve: async (o: Nip46Response) => { + this.#log("Reply: %O", o); + resolve(o.result as T); + }, + reject, + }); + }); + } +} diff --git a/packages/system/src/impl/nip7.ts b/packages/system/src/impl/nip7.ts new file mode 100644 index 00000000..fe10e819 --- /dev/null +++ b/packages/system/src/impl/nip7.ts @@ -0,0 +1,61 @@ +import { WorkQueueItem, processWorkQueue, barrierQueue, unwrap } from "@snort/shared"; +import { EventSigner } from "../event-publisher"; +import { HexKey, NostrEvent } from "../nostr"; + +const Nip7Queue: Array = []; +processWorkQueue(Nip7Queue); + +declare global { + interface Window { + nostr?: { + getPublicKey: () => Promise; + signEvent: (event: T) => Promise; + + getRelays?: () => Promise>; + + nip04?: { + encrypt?: (pubkey: HexKey, plaintext: string) => Promise; + decrypt?: (pubkey: HexKey, ciphertext: string) => Promise; + }; + }; + } +} + +export class Nip7Signer implements EventSigner { + init(): Promise { + return Promise.resolve(); + } + + async getPubKey(): Promise { + if (!window.nostr) { + throw new Error("Cannot use NIP-07 signer, not found!"); + } + return await window.nostr.getPublicKey(); + } + + async nip4Encrypt(content: string, key: string): Promise { + if (!window.nostr) { + throw new Error("Cannot use NIP-07 signer, not found!"); + } + return await barrierQueue(Nip7Queue, () => + unwrap(window.nostr?.nip04?.encrypt).call(window.nostr?.nip04, key, content) + ); + } + + async nip4Decrypt(content: string, otherKey: string): Promise { + if (!window.nostr) { + throw new Error("Cannot use NIP-07 signer, not found!"); + } + return await barrierQueue(Nip7Queue, () => + unwrap(window.nostr?.nip04?.decrypt).call(window.nostr?.nip04, otherKey, content) + ); + } + + async sign(ev: NostrEvent): Promise { + if (!window.nostr) { + throw new Error("Cannot use NIP-07 signer, not found!"); + } + return await barrierQueue(Nip7Queue, () => unwrap(window.nostr).signEvent(ev)); + } + +} \ No newline at end of file diff --git a/packages/system/src/index.ts b/packages/system/src/index.ts index dab15411..a4146048 100644 --- a/packages/system/src/index.ts +++ b/packages/system/src/index.ts @@ -22,6 +22,8 @@ export * from "./zaps"; export * from "./impl/nip4"; export * from "./impl/nip44"; +export * from "./impl/nip7"; +export * from "./impl/nip46"; export * from "./cache/index"; export * from "./cache/user-relays"; diff --git a/packages/system/src/query.ts b/packages/system/src/query.ts index 570135d3..90b0a114 100644 --- a/packages/system/src/query.ts +++ b/packages/system/src/query.ts @@ -220,7 +220,6 @@ export class Query implements QueryBase { if (this.isOpen()) { for (const qt of this.#tracing) { if (qt.relay === c.Address) { - debugger; c.QueueReq(["REQ", qt.id, ...qt.filters], () => qt.sentToRelay()); } }