From 1cb27c1881db09e4b5e4c3a9c3cc05c691196441 Mon Sep 17 00:00:00 2001
From: Kieran
Date: Mon, 10 Jul 2023 15:40:22 +0100
Subject: [PATCH] feat: nsecbunker
---
packages/app/src/Feed/EventPublisher.ts | 13 +--
packages/app/src/Hooks/useLoginHandler.tsx | 9 +-
packages/app/src/Login/LoginSession.ts | 24 ++++-
packages/app/src/Login/MultiAccountStore.ts | 57 +++++++++-
packages/app/src/Pages/LoginPage.tsx | 64 +++++++++---
packages/system/src/impl/nip46.ts | 109 ++++++++++++++------
6 files changed, 214 insertions(+), 62 deletions(-)
diff --git a/packages/app/src/Feed/EventPublisher.ts b/packages/app/src/Feed/EventPublisher.ts
index 16d30d83..f1076e8e 100644
--- a/packages/app/src/Feed/EventPublisher.ts
+++ b/packages/app/src/Feed/EventPublisher.ts
@@ -1,15 +1,6 @@
-import { useMemo } from "react";
import useLogin from "Hooks/useLogin";
-import { EventPublisher, Nip7Signer } from "@snort/system";
export default function useEventPublisher() {
- const { publicKey, privateKey } = useLogin();
- return useMemo(() => {
- if (privateKey) {
- return EventPublisher.privateKey(privateKey);
- }
- if (publicKey) {
- return new EventPublisher(new Nip7Signer(), publicKey);
- }
- }, [publicKey, privateKey]);
+ const { publisher } = useLogin();
+ return publisher;
}
diff --git a/packages/app/src/Hooks/useLoginHandler.tsx b/packages/app/src/Hooks/useLoginHandler.tsx
index 62af3f18..77870d0b 100644
--- a/packages/app/src/Hooks/useLoginHandler.tsx
+++ b/packages/app/src/Hooks/useLoginHandler.tsx
@@ -1,7 +1,7 @@
import { useIntl } from "react-intl";
import { EmailRegex, MnemonicRegex } from "Const";
-import { LoginStore } from "Login";
+import { LoginSessionType, LoginStore } from "Login";
import { generateBip39Entropy, entropyToPrivateKey } from "nip6";
import { getNip05PubKey } from "Pages/LoginPage";
import { bech32ToHex } from "SnortUtils";
@@ -28,10 +28,10 @@ export default function useLoginHandler() {
}
} else if (key.startsWith("npub")) {
const hexKey = bech32ToHex(key);
- LoginStore.loginWithPubkey(hexKey);
+ LoginStore.loginWithPubkey(hexKey, LoginSessionType.PublicKey);
} else if (key.match(EmailRegex)) {
const hexKey = await getNip05PubKey(key);
- LoginStore.loginWithPubkey(hexKey);
+ LoginStore.loginWithPubkey(hexKey, LoginSessionType.PublicKey);
} else if (key.match(MnemonicRegex)?.length === 24) {
if (!hasSubtleCrypto) {
throw new Error(insecureMsg);
@@ -50,7 +50,8 @@ export default function useLoginHandler() {
await nip46.init();
const loginPubkey = await nip46.getPubKey();
- LoginStore.loginWithPubkey(loginPubkey);
+ LoginStore.loginWithPubkey(loginPubkey, LoginSessionType.Nip46, undefined, nip46.relays);
+ nip46.close();
} else {
throw new Error("INVALID PRIVATE KEY");
}
diff --git a/packages/app/src/Login/LoginSession.ts b/packages/app/src/Login/LoginSession.ts
index 3a7fb288..a626de72 100644
--- a/packages/app/src/Login/LoginSession.ts
+++ b/packages/app/src/Login/LoginSession.ts
@@ -1,4 +1,4 @@
-import { HexKey, RelaySettings, u256 } from "@snort/system";
+import { HexKey, RelaySettings, u256, EventPublisher } from "@snort/system";
import { UserPreferences } from "Login";
import { SubscriptionEvent } from "Subscription";
@@ -10,7 +10,19 @@ interface Newest {
timestamp: number;
}
+export enum LoginSessionType {
+ PrivateKey = "private_key",
+ PublicKey = "public_key",
+ Nip7 = "nip7",
+ Nip46 = "nip46",
+}
+
export interface LoginSession {
+ /**
+ * Type of login session
+ */
+ type: LoginSessionType;
+
/**
* Current user private key
*/
@@ -80,4 +92,14 @@ export interface LoginSession {
* Snort subscriptions licences
*/
subscriptions: Array;
+
+ /**
+ * Remote signer relays (NIP-46)
+ */
+ remoteSignerRelays?: Array;
+
+ /**
+ * Instance event publisher
+ */
+ publisher?: EventPublisher;
}
diff --git a/packages/app/src/Login/MultiAccountStore.ts b/packages/app/src/Login/MultiAccountStore.ts
index aeccb629..e5f13d88 100644
--- a/packages/app/src/Login/MultiAccountStore.ts
+++ b/packages/app/src/Login/MultiAccountStore.ts
@@ -1,15 +1,16 @@
import * as secp from "@noble/curves/secp256k1";
import * as utils from "@noble/curves/abstract/utils";
-import { HexKey, RelaySettings } from "@snort/system";
+import { HexKey, RelaySettings, EventPublisher, Nip46Signer, Nip7Signer } from "@snort/system";
import { deepClone, sanitizeRelayUrl, unwrap, ExternalStore } from "@snort/shared";
import { DefaultRelays } from "Const";
-import { LoginSession } from "Login";
+import { LoginSession, LoginSessionType } from "Login";
import { DefaultPreferences, UserPreferences } from "./Preferences";
const AccountStoreKey = "sessions";
const LoggedOut = {
+ type: "public_key",
preferences: DefaultPreferences,
tags: {
item: [],
@@ -60,7 +61,8 @@ export class MultiAccountStore extends ExternalStore {
super();
const existing = window.localStorage.getItem(AccountStoreKey);
if (existing) {
- this.#accounts = new Map((JSON.parse(existing) as Array).map(a => [unwrap(a.publicKey), a]));
+ const logins = JSON.parse(existing);
+ this.#accounts = new Map((logins as Array).map(a => [unwrap(a.publicKey), a]));
} else {
this.#accounts = new Map();
}
@@ -68,6 +70,9 @@ export class MultiAccountStore extends ExternalStore {
if (!this.#activeAccount) {
this.#activeAccount = this.#accounts.keys().next().value;
}
+ for (const [, v] of this.#accounts) {
+ v.publisher = this.#createPublisher(v);
+ }
}
getSessions() {
@@ -85,20 +90,28 @@ export class MultiAccountStore extends ExternalStore {
}
}
- loginWithPubkey(key: HexKey, relays?: Record) {
+ loginWithPubkey(
+ key: HexKey,
+ type: LoginSessionType,
+ relays?: Record,
+ remoteSignerRelays?: Array
+ ) {
if (this.#accounts.has(key)) {
throw new Error("Already logged in with this pubkey");
}
const initRelays = this.decideInitRelays(relays);
const newSession = {
...LoggedOut,
+ type,
publicKey: key,
relays: {
item: initRelays,
timestamp: 1,
},
preferences: deepClone(DefaultPreferences),
+ remoteSignerRelays,
} as LoginSession;
+ newSession.publisher = this.#createPublisher(newSession);
this.#accounts.set(key, newSession);
this.#activeAccount = key;
@@ -121,6 +134,7 @@ export class MultiAccountStore extends ExternalStore {
const initRelays = relays ?? Object.fromEntries(DefaultRelays.entries());
const newSession = {
...LoggedOut,
+ type: LoginSessionType.PrivateKey,
privateKey: key,
publicKey: pubKey,
generatedEntropy: entropy,
@@ -130,6 +144,8 @@ export class MultiAccountStore extends ExternalStore {
},
preferences: deepClone(DefaultPreferences),
} as LoginSession;
+ newSession.publisher = this.#createPublisher(newSession);
+
this.#accounts.set(pubKey, newSession);
this.#activeAccount = pubKey;
this.#save();
@@ -157,7 +173,26 @@ export class MultiAccountStore extends ExternalStore {
const s = this.#activeAccount ? this.#accounts.get(this.#activeAccount) : undefined;
if (!s) return LoggedOut;
- return deepClone(s);
+ return s;
+ }
+
+ #createPublisher(l: LoginSession) {
+ switch (l.type) {
+ case LoginSessionType.PrivateKey: {
+ return EventPublisher.privateKey(unwrap(l.privateKey));
+ }
+ case LoginSessionType.Nip46: {
+ const relayArgs = (l.remoteSignerRelays ?? []).map(a => `relay=${encodeURIComponent(a)}`);
+ const inner = new Nip7Signer();
+ const nip46 = new Nip46Signer(`bunker://${unwrap(l.publicKey)}?${[...relayArgs].join("&")}`, inner);
+ return new EventPublisher(nip46, unwrap(l.publicKey));
+ }
+ default: {
+ if (l.publicKey) {
+ return new EventPublisher(new Nip7Signer(), l.publicKey);
+ }
+ }
+ }
}
#migrate() {
@@ -203,6 +238,18 @@ export class MultiAccountStore extends ExternalStore {
}
}
+ // update session types
+ for (const [, v] of this.#accounts) {
+ if (v.privateKey) {
+ v.type = LoginSessionType.PrivateKey;
+ didMigrate = true;
+ }
+ if (!v.type) {
+ v.type = LoginSessionType.Nip7;
+ didMigrate = true;
+ }
+ }
+
if (didMigrate) {
console.debug("Finished migration to MultiAccountStore");
this.#save();
diff --git a/packages/app/src/Pages/LoginPage.tsx b/packages/app/src/Pages/LoginPage.tsx
index be38fea4..9b3ecf84 100644
--- a/packages/app/src/Pages/LoginPage.tsx
+++ b/packages/app/src/Pages/LoginPage.tsx
@@ -3,16 +3,22 @@ import "./LoginPage.css";
import { CSSProperties, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useIntl, FormattedMessage } from "react-intl";
-import { HexKey } from "@snort/system";
+import { HexKey, Nip46Signer, PrivateKeySigner } from "@snort/system";
-import { bech32ToHex, unwrap } from "SnortUtils";
+import { bech32ToHex, getPublicKey, unwrap } from "SnortUtils";
import ZapButton from "Element/ZapButton";
import useImgProxy from "Hooks/useImgProxy";
import Icon from "Icons/Icon";
import useLogin from "Hooks/useLogin";
-import { generateNewLogin, LoginStore } from "Login";
+import { generateNewLogin, LoginSessionType, LoginStore } from "Login";
import AsyncButton from "Element/AsyncButton";
import useLoginHandler from "Hooks/useLoginHandler";
+import { secp256k1 } from "@noble/curves/secp256k1";
+import { bytesToHex } from "@noble/curves/abstract/utils";
+import Modal from "Element/Modal";
+import QrCode from "Element/QrCode";
+import Copy from "Element/Copy";
+import { delay } from "SnortUtils";
interface ArtworkEntry {
name: string;
@@ -73,6 +79,7 @@ export default function LoginPage() {
const loginHandler = useLoginHandler();
const hasNip7 = "nostr" in window;
const hasSubtleCrypto = window.crypto.subtle !== undefined;
+ const [nostrConnect, setNostrConnect] = useState("");
useEffect(() => {
if (login.publicKey) {
@@ -112,7 +119,27 @@ export default function LoginPage() {
const relays =
"getRelays" in unwrap(window.nostr) ? await unwrap(window.nostr?.getRelays).call(window.nostr) : undefined;
const pubKey = await unwrap(window.nostr).getPublicKey();
- LoginStore.loginWithPubkey(pubKey, relays);
+ LoginStore.loginWithPubkey(pubKey, LoginSessionType.Nip7, relays);
+ }
+
+ async function startNip46() {
+ const meta = {
+ name: "Snort",
+ url: window.location.href,
+ };
+
+ const newKey = bytesToHex(secp256k1.utils.randomPrivateKey());
+ const relays = ["wss://relay.damus.io"].map(a => `relay=${encodeURIComponent(a)}`);
+ const connectUrl = `nostrconnect://${getPublicKey(newKey)}?${[
+ ...relays,
+ `metadata=${encodeURIComponent(JSON.stringify(meta))}`,
+ ].join("&")}`;
+ setNostrConnect(connectUrl);
+
+ const signer = new Nip46Signer(connectUrl, new PrivateKeySigner(newKey));
+ await signer.init();
+ await delay(500);
+ await signer.describe();
}
function altLogins() {
@@ -121,12 +148,25 @@ export default function LoginPage() {
}
return (
-
+ <>
+
+
+
+
+
+
+ {nostrConnect && (
+ setNostrConnect("")}>
+
+
+
+
+
+ )}
+ >
);
}
@@ -227,9 +267,9 @@ export default function LoginPage() {
/>
-
+
makeRandomKey()}>
diff --git a/packages/system/src/impl/nip46.ts b/packages/system/src/impl/nip46.ts
index aaf0e8bb..b4cc50cc 100644
--- a/packages/system/src/impl/nip46.ts
+++ b/packages/system/src/impl/nip46.ts
@@ -27,64 +27,93 @@ interface Nip46Request {
interface Nip46Response {
id: string
result: any
- error?: string
+ error: string
}
interface QueueObj {
- resolve: (o: Nip46Response) => void;
+ resolve: (o: any) => void;
reject: (e: Error) => void;
}
export class Nip46Signer implements EventSigner {
#conn?: Connection;
#relay: string;
- #target: string;
+ #localPubkey: string;
+ #remotePubkey?: string;
#token?: string;
#insideSigner: EventSigner;
#commandQueue: Map
= new Map();
#log = debug("NIP-46");
+ #proto: string;
+ #didInit: boolean = false;
constructor(config: string, insideSigner?: EventSigner) {
const u = new URL(config);
- this.#target = u.pathname.substring(2);
+ this.#proto = u.protocol;
+ this.#localPubkey = 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);
+ if (this.#localPubkey.startsWith("npub")) {
+ this.#localPubkey = bech32ToHex(this.#localPubkey);
}
this.#relay = unwrap(u.searchParams.get("relay"));
this.#insideSigner = insideSigner ?? new PrivateKeySigner(secp256k1.utils.randomPrivateKey())
}
+ get relays() {
+ return [this.#relay];
+ }
+
async init() {
+ const isBunker = this.#proto === "bunker:";
+ if (isBunker) {
+ this.#remotePubkey = this.#localPubkey;
+ this.#localPubkey = await this.#insideSigner.getPubKey();
+ }
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]
+ "#p": [this.#localPubkey]
}], () => { });
- const rsp = await this.#connect(insidePubkey);
- if (rsp === "ack") {
+ if (isBunker) {
+ await this.#connect(unwrap(this.#remotePubkey));
resolve();
} else {
- reject();
+ this.#commandQueue.set("connect", {
+ reject,
+ resolve
+ })
}
}
this.#conn.Connect();
+ this.#didInit = true;
})
}
+ async close() {
+ if (this.#conn) {
+ await this.#disconnect();
+ this.#conn.CloseReq("reply");
+ this.#conn.Close();
+ this.#conn = undefined;
+ this.#didInit = false;
+ }
+ }
+
+ async describe() {
+ return await this.#rpc>("describe", []);
+ }
+
async getPubKey() {
return await this.#rpc("get_public_key", []);
}
@@ -98,7 +127,12 @@ export class Nip46Signer implements EventSigner {
}
async sign(ev: NostrEvent) {
- return await this.#rpc("nip04_decrypt", [ev]);
+ const evStr = await this.#rpc("sign_event", [JSON.stringify(ev)]);
+ return JSON.parse(evStr);
+ }
+
+ async #disconnect() {
+ return await this.#rpc("disconnect", []);
}
async #connect(pk: string) {
@@ -114,10 +148,21 @@ export class Nip46Signer implements EventSigner {
throw new Error("Unknown event kind");
}
- const decryptedContent = await this.#insideSigner.nip4Decrypt(e.content, this.#target);
- const reply = JSON.parse(decryptedContent) as Nip46Response;
+ const decryptedContent = await this.#insideSigner.nip4Decrypt(e.content, e.pubkey);
+ const reply = JSON.parse(decryptedContent) as Nip46Request | Nip46Response;
- const pending = this.#commandQueue.get(reply.id);
+ let id = reply.id;
+ this.#log("Recv: %O", reply);
+ if ("method" in reply && reply.method === "connect") {
+ this.#remotePubkey = reply.params[0];
+ await this.#sendCommand({
+ id: reply.id,
+ result: "ack",
+ error: ""
+ }, unwrap(this.#remotePubkey));
+ id = "connect";
+ }
+ const pending = this.#commandQueue.get(id);
if (!pending) {
throw new Error("No pending command found");
}
@@ -127,32 +172,38 @@ export class Nip46Signer implements EventSigner {
}
async #rpc(method: string, params: Array) {
+ if (!this.#didInit) {
+ await this.init();
+ }
if (!this.#conn) throw new Error("Connection error");
- const id = uuid();
const payload = {
- id,
+ id: uuid(),
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);
+ this.#sendCommand(payload, unwrap(this.#remotePubkey));
return await new Promise((resolve, reject) => {
- this.#commandQueue.set(id, {
+ this.#commandQueue.set(payload.id, {
resolve: async (o: Nip46Response) => {
- this.#log("Reply: %O", o);
resolve(o.result as T);
},
reject,
});
});
}
+
+ async #sendCommand(payload: Nip46Request | Nip46Response, target: string) {
+ if (!this.#conn) return;
+
+ const eb = new EventBuilder();
+ eb.kind(NIP46_KIND as EventKind)
+ .content(await this.#insideSigner.nip4Encrypt(JSON.stringify(payload), target))
+ .tag(["p", target]);
+
+ this.#log("Send: %O", payload);
+ const evCommand = await eb.buildAndSign(this.#insideSigner);
+ await this.#conn.SendAsync(evCommand);
+ }
}