forked from Kieran/snort
NIP-46 draft
This commit is contained in:
parent
9640a7fa57
commit
68b9a89278
@ -5,6 +5,7 @@ import { LoginStore } from "Login";
|
|||||||
import { generateBip39Entropy, entropyToPrivateKey } from "nip6";
|
import { generateBip39Entropy, entropyToPrivateKey } from "nip6";
|
||||||
import { getNip05PubKey } from "Pages/LoginPage";
|
import { getNip05PubKey } from "Pages/LoginPage";
|
||||||
import { bech32ToHex } from "SnortUtils";
|
import { bech32ToHex } from "SnortUtils";
|
||||||
|
import { Nip7Signer, Nip46Signer } from "@snort/system";
|
||||||
|
|
||||||
export default function useLoginHandler() {
|
export default function useLoginHandler() {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
@ -43,6 +44,13 @@ export default function useLoginHandler() {
|
|||||||
throw new Error(insecureMsg);
|
throw new Error(insecureMsg);
|
||||||
}
|
}
|
||||||
LoginStore.loginWithPrivateKey(key);
|
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 {
|
} else {
|
||||||
throw new Error("INVALID PRIVATE KEY");
|
throw new Error("INVALID PRIVATE KEY");
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { EventKind, HexKey, NostrPrefix, NostrEvent } from ".";
|
import { EventKind, HexKey, NostrPrefix, NostrEvent, EventSigner } from ".";
|
||||||
import { HashtagRegex } from "./const";
|
import { HashtagRegex } from "./const";
|
||||||
import { getPublicKey, unixNow } from "@snort/shared";
|
import { getPublicKey, unixNow } from "@snort/shared";
|
||||||
import { EventExt } from "./event-ext";
|
import { EventExt } from "./event-ext";
|
||||||
@ -73,10 +73,15 @@ export class EventBuilder {
|
|||||||
* Build and sign event
|
* Build and sign event
|
||||||
* @param pk Private key to sign event with
|
* @param pk Private key to sign event with
|
||||||
*/
|
*/
|
||||||
async buildAndSign(pk: HexKey) {
|
async buildAndSign(pk: HexKey | EventSigner) {
|
||||||
const ev = this.pubKey(getPublicKey(pk)).build();
|
if (typeof pk === "string") {
|
||||||
await EventExt.sign(ev, pk);
|
const ev = this.pubKey(getPublicKey(pk)).build();
|
||||||
return ev;
|
EventExt.sign(ev, pk);
|
||||||
|
return ev;
|
||||||
|
} else {
|
||||||
|
const ev = this.pubKey(await pk.getPubKey()).build();
|
||||||
|
return await pk.sign(ev);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#validate() {
|
#validate() {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as secp from "@noble/curves/secp256k1";
|
import * as secp from "@noble/curves/secp256k1";
|
||||||
import * as utils from "@noble/curves/abstract/utils";
|
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 { EventKind, HexKey, NostrEvent } from ".";
|
||||||
import { Nip4WebCryptoEncryptor } from "./impl/nip4";
|
import { Nip4WebCryptoEncryptor } from "./impl/nip4";
|
||||||
@ -36,6 +36,7 @@ export abstract class EventExt {
|
|||||||
* Sign this message with a private key
|
* Sign this message with a private key
|
||||||
*/
|
*/
|
||||||
static sign(e: NostrEvent, key: HexKey) {
|
static sign(e: NostrEvent, key: HexKey) {
|
||||||
|
e.pubkey = getPublicKey(key);
|
||||||
e.id = this.createId(e);
|
e.id = this.createId(e);
|
||||||
|
|
||||||
const sig = secp.schnorr.sign(e.id, key);
|
const sig = secp.schnorr.sign(e.id, key);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as secp from "@noble/curves/secp256k1";
|
import * as secp from "@noble/curves/secp256k1";
|
||||||
import * as utils from "@noble/curves/abstract/utils";
|
import * as utils from "@noble/curves/abstract/utils";
|
||||||
import { unwrap, barrierQueue, processWorkQueue, WorkQueueItem, getPublicKey } from "@snort/shared";
|
import { unwrap, getPublicKey } from "@snort/shared";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EventKind,
|
EventKind,
|
||||||
@ -18,76 +18,33 @@ import {
|
|||||||
import { EventBuilder } from "./event-builder";
|
import { EventBuilder } from "./event-builder";
|
||||||
import { EventExt } from "./event-ext";
|
import { EventExt } from "./event-ext";
|
||||||
import { findTag } from "./utils";
|
import { findTag } from "./utils";
|
||||||
|
import { Nip7Signer } from "./impl/nip7";
|
||||||
|
|
||||||
const Nip7Queue: Array<WorkQueueItem> = [];
|
type EventBuilderHook = (ev: EventBuilder) => EventBuilder;
|
||||||
processWorkQueue(Nip7Queue);
|
|
||||||
export type EventBuilderHook = (ev: EventBuilder) => EventBuilder;
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
nostr?: {
|
|
||||||
getPublicKey: () => Promise<HexKey>;
|
|
||||||
signEvent: <T extends NostrEvent>(event: T) => Promise<T>;
|
|
||||||
|
|
||||||
getRelays?: () => Promise<Record<string, { read: boolean; write: boolean }>>;
|
|
||||||
|
|
||||||
nip04?: {
|
|
||||||
encrypt?: (pubkey: HexKey, plaintext: string) => Promise<string>;
|
|
||||||
decrypt?: (pubkey: HexKey, ciphertext: string) => Promise<string>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EventSigner {
|
export interface EventSigner {
|
||||||
|
init(): Promise<void>;
|
||||||
getPubKey(): Promise<string> | string;
|
getPubKey(): Promise<string> | string;
|
||||||
nip4Encrypt(content: string, key: string): Promise<string>;
|
nip4Encrypt(content: string, key: string): Promise<string>;
|
||||||
nip4Decrypt(content: string, otherKey: string): Promise<string>;
|
nip4Decrypt(content: string, otherKey: string): Promise<string>;
|
||||||
sign(ev: NostrEvent): Promise<NostrEvent>;
|
sign(ev: NostrEvent): Promise<NostrEvent>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Nip7Signer implements EventSigner {
|
|
||||||
async getPubKey(): Promise<string> {
|
|
||||||
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<string> {
|
|
||||||
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<string> {
|
|
||||||
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<NostrEvent> {
|
|
||||||
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 {
|
export class PrivateKeySigner implements EventSigner {
|
||||||
#publicKey: string;
|
#publicKey: string;
|
||||||
#privateKey: string;
|
#privateKey: string;
|
||||||
|
|
||||||
constructor(privateKey: string) {
|
constructor(privateKey: string | Uint8Array) {
|
||||||
this.#privateKey = privateKey;
|
if (typeof privateKey === "string") {
|
||||||
this.#publicKey = getPublicKey(privateKey);
|
this.#privateKey = privateKey;
|
||||||
|
} else {
|
||||||
|
this.#privateKey = utils.bytesToHex(privateKey);
|
||||||
|
}
|
||||||
|
this.#publicKey = getPublicKey(this.#privateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
init(): Promise<void> {
|
||||||
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
getPubKey(): string {
|
getPubKey(): string {
|
||||||
|
158
packages/system/src/impl/nip46.ts
Normal file
158
packages/system/src/impl/nip46.ts
Normal file
@ -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<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Nip46Request {
|
||||||
|
id: string
|
||||||
|
method: string
|
||||||
|
params: Array<any>
|
||||||
|
}
|
||||||
|
|
||||||
|
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<string, QueueObj> = 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<void>((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<string>("get_public_key", []);
|
||||||
|
}
|
||||||
|
|
||||||
|
async nip4Encrypt(content: string, otherKey: string) {
|
||||||
|
return await this.#rpc<string>("nip04_encrypt", [otherKey, content]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async nip4Decrypt(content: string, otherKey: string) {
|
||||||
|
return await this.#rpc<string>("nip04_decrypt", [otherKey, content]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sign(ev: NostrEvent) {
|
||||||
|
return await this.#rpc<NostrEvent>("nip04_decrypt", [ev]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async #connect(pk: string) {
|
||||||
|
const connectParams = [pk];
|
||||||
|
if (this.#token) {
|
||||||
|
connectParams.push(this.#token);
|
||||||
|
}
|
||||||
|
return await this.#rpc<string>("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<T>(method: string, params: Array<any>) {
|
||||||
|
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<T>((resolve, reject) => {
|
||||||
|
this.#commandQueue.set(id, {
|
||||||
|
resolve: async (o: Nip46Response) => {
|
||||||
|
this.#log("Reply: %O", o);
|
||||||
|
resolve(o.result as T);
|
||||||
|
},
|
||||||
|
reject,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
61
packages/system/src/impl/nip7.ts
Normal file
61
packages/system/src/impl/nip7.ts
Normal file
@ -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<WorkQueueItem> = [];
|
||||||
|
processWorkQueue(Nip7Queue);
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
nostr?: {
|
||||||
|
getPublicKey: () => Promise<HexKey>;
|
||||||
|
signEvent: <T extends NostrEvent>(event: T) => Promise<T>;
|
||||||
|
|
||||||
|
getRelays?: () => Promise<Record<string, { read: boolean; write: boolean }>>;
|
||||||
|
|
||||||
|
nip04?: {
|
||||||
|
encrypt?: (pubkey: HexKey, plaintext: string) => Promise<string>;
|
||||||
|
decrypt?: (pubkey: HexKey, ciphertext: string) => Promise<string>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Nip7Signer implements EventSigner {
|
||||||
|
init(): Promise<void> {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPubKey(): Promise<string> {
|
||||||
|
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<string> {
|
||||||
|
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<string> {
|
||||||
|
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<NostrEvent> {
|
||||||
|
if (!window.nostr) {
|
||||||
|
throw new Error("Cannot use NIP-07 signer, not found!");
|
||||||
|
}
|
||||||
|
return await barrierQueue(Nip7Queue, () => unwrap(window.nostr).signEvent(ev));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -22,6 +22,8 @@ export * from "./zaps";
|
|||||||
|
|
||||||
export * from "./impl/nip4";
|
export * from "./impl/nip4";
|
||||||
export * from "./impl/nip44";
|
export * from "./impl/nip44";
|
||||||
|
export * from "./impl/nip7";
|
||||||
|
export * from "./impl/nip46";
|
||||||
|
|
||||||
export * from "./cache/index";
|
export * from "./cache/index";
|
||||||
export * from "./cache/user-relays";
|
export * from "./cache/user-relays";
|
||||||
|
@ -220,7 +220,6 @@ export class Query implements QueryBase {
|
|||||||
if (this.isOpen()) {
|
if (this.isOpen()) {
|
||||||
for (const qt of this.#tracing) {
|
for (const qt of this.#tracing) {
|
||||||
if (qt.relay === c.Address) {
|
if (qt.relay === c.Address) {
|
||||||
debugger;
|
|
||||||
c.QueueReq(["REQ", qt.id, ...qt.filters], () => qt.sentToRelay());
|
c.QueueReq(["REQ", qt.id, ...qt.filters], () => qt.sentToRelay());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user