diff --git a/src/App.tsx b/src/App.tsx
index 7ed255c..2f369ef 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -122,6 +122,15 @@ function App() {
})
}
+ async function importKey() {
+ call(async () => {
+ // @ts-ignore
+ const nsec = document.getElementById(`nsec`)?.value
+ await swicCall('importKey', nsec)
+ log('Key imported')
+ })
+ }
+
async function fetchNewKey() {
call(async () => {
// @ts-ignore
@@ -136,6 +145,7 @@ function App() {
// subscribe to updates from the service worker
swicOnRender(() => {
+ console.log("render")
setRender(r => r + 1)
})
@@ -161,9 +171,13 @@ function App() {
)
})}
-
+
+
+
+
+
diff --git a/src/backend.ts b/src/backend.ts
index e32b478..7b41bce 100644
--- a/src/backend.ts
+++ b/src/backend.ts
@@ -1,8 +1,11 @@
import { generatePrivateKey, getPublicKey, nip19 } from 'nostr-tools'
import { dbi, DbKey, DbPending, DbPerm } from './db'
import { Keys } from './keys'
-import NDK, { IEventHandlingStrategy, NDKEvent, NDKNip46Backend, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk'
+import NDK, { IEventHandlingStrategy, NDKEvent, NDKNip46Backend, NDKPrivateKeySigner, NDKSigner } from '@nostr-dev-kit/ndk'
import { NOAUTHD_URL, WEB_PUSH_PUBKEY, NIP46_RELAYS } from './consts'
+//import { PrivateKeySigner } from './signer'
+
+//const PERF_TEST = false
export interface KeyInfo {
npub: string
@@ -14,7 +17,7 @@ interface Key {
npub: string
ndk: NDK
backoff: number
- signer: NDKPrivateKeySigner
+ signer: NDKSigner
backend: NDKNip46Backend
}
@@ -56,7 +59,7 @@ class EventHandlingStrategyWrapper implements IEventHandlingStrategy {
remotePubkey: string,
params: string[]
): Promise {
- console.log("handle", { method: this.method, id, remotePubkey, params })
+ console.log(Date.now(), "handle", { method: this.method, id, remotePubkey, params })
const allow = await this.allowCb({
npub: this.npub,
id,
@@ -66,10 +69,10 @@ class EventHandlingStrategyWrapper implements IEventHandlingStrategy {
})
if (!allow) return undefined
return this.body.handle(backend, id, remotePubkey, params)
- .then(r => {
- console.log("req", id, "method", this.method, "result", r)
- return r
- })
+ .then(r => {
+ console.log(Date.now(), "req", id, "method", this.method, "result", r)
+ return r
+ })
}
}
@@ -79,6 +82,7 @@ export class NoauthBackend {
private enckeys: DbKey[] = []
private keys: Key[] = []
private perms: DbPerm[] = []
+ private doneReqIds: string[] = []
private confirmBuffer: Pending[] = []
private accessBuffer: DbPending[] = []
private notifCallback: (() => void) | null = null
@@ -199,7 +203,7 @@ export class NoauthBackend {
},
body,
})
- if (r.status !== 200 && r.status != 201) {
+ if (r.status !== 200 && r.status !== 201) {
console.log("Fetch error", url, method, r.status)
throw new Error("Failed to fetch" + url)
}
@@ -394,6 +398,13 @@ export class NoauthBackend {
params
}: IAllowCallbackParams): Promise {
+ // same reqs usually come on reconnects
+ if (this.doneReqIds.includes(id)) {
+ console.log("request already done", id)
+ // FIXME maybe repeat the reply, but without the Notification?
+ return false
+ }
+
const appNpub = nip19.npubEncode(remotePubkey)
const req: DbPending = {
id,
@@ -403,30 +414,34 @@ export class NoauthBackend {
params: JSON.stringify(params),
timestamp: Date.now()
}
- if (!await dbi.addPending(req)) {
- console.log("request already done", id)
- // FIXME maybe repeat the reply, but without the Notification?
- return false
- }
const self = this
- return new Promise((ok) => {
+ return new Promise(async (ok) => {
// called when it's decided whether to allow this or not
- const cb = async (allow: boolean, remember: boolean) => {
+ const onAllow = async (manual: boolean, allow: boolean, remember: boolean) => {
// confirm
- console.log(allow ? "allowed" : "disallowed", npub, method, params)
- await dbi.confirmPending(id, allow)
+ console.log(Date.now(), allow ? "allowed" : "disallowed", npub, method, params)
+ if (manual) {
+ await dbi.confirmPending(id, allow)
- if (!await dbi.getApp(req.appNpub)) {
- await dbi.addApp({
- appNpub: req.appNpub,
- npub: req.npub,
- timestamp: Date.now(),
- name: '',
- icon: '',
- url: ''
+ if (!await dbi.getApp(req.appNpub)) {
+ await dbi.addApp({
+ appNpub: req.appNpub,
+ npub: req.npub,
+ timestamp: Date.now(),
+ name: '',
+ icon: '',
+ url: ''
+ })
+ }
+ } else {
+ // just send to db w/o waiting for it
+ // if (!PERF_TEST)
+ dbi.addConfirmed({
+ ...req,
+ allowed: allow
})
}
@@ -459,6 +474,7 @@ export class NoauthBackend {
}
// notify UI that it was confirmed
+ // if (!PERF_TEST)
this.updateUI()
// return to let nip46 flow proceed
@@ -467,21 +483,24 @@ export class NoauthBackend {
// check perms
const perm = this.getPerm(req)
- console.log("perm", req.id, perm)
+ console.log(Date.now(), "perm", req.id, perm)
// have perm?
if (perm) {
// reply immediately
- cb(perm === '1', false)
+ onAllow(false, perm === '1', false)
} else {
+ // put pending req to db
+ await dbi.addPending(req)
+
// need manual confirmation
console.log("need confirm", req)
// put to a list of pending requests
this.confirmBuffer.push({
req,
- cb
+ cb: (allow, remember) => onAllow(true, allow, remember)
})
// show notifs
@@ -502,7 +521,7 @@ export class NoauthBackend {
// init relay objects but dont wait until we connect
ndk.connect()
- const signer = new NDKPrivateKeySigner(sk)
+ const signer = new NDKPrivateKeySigner(sk) // PrivateKeySigner
const backend = new NDKNip46Backend(ndk, sk, () => Promise.resolve(true))
this.keys.push({ npub, backend, signer, ndk, backoff })
@@ -575,6 +594,12 @@ export class NoauthBackend {
return k
}
+ private async importKey(nsec: string) {
+ const k = await this.addKey(nsec)
+ this.updateUI()
+ return k
+ }
+
private async saveKey(npub: string, passphrase: string) {
const info = this.enckeys.find(k => k.npub === npub)
if (!info) throw new Error(`Key ${npub} not found`)
@@ -661,6 +686,8 @@ export class NoauthBackend {
let result = undefined
if (method === 'generateKey') {
result = await this.generateKey()
+ } else if (method === 'importKey') {
+ result = await this.importKey(args[0])
} else if (method === 'saveKey') {
result = await this.saveKey(args[0], args[1])
} else if (method === 'fetchKey') {
diff --git a/src/db.ts b/src/db.ts
index d91bc22..bf38cf1 100644
--- a/src/db.ts
+++ b/src/db.ts
@@ -186,4 +186,12 @@ export const dbi = {
console.log(`db addPending error: ${error}`)
}
},
+ addConfirmed: async (r: DbHistory) => {
+ try {
+ await db.history.add(r)
+ } catch (error) {
+ console.log(`db addConfirmed error: ${error}`)
+ return false
+ }
+ },
}
diff --git a/src/ende.ts b/src/ende.ts
new file mode 100644
index 0000000..0a5783f
--- /dev/null
+++ b/src/ende.ts
@@ -0,0 +1,126 @@
+/*
+ende stands for encryption decryption
+*/
+import { secp256k1 as secp } from '@noble/curves/secp256k1'
+//import * as secp from "./vendor/secp256k1.js";
+
+export async function encrypt(
+ publicKey: string,
+ message: string,
+ privateKey: string,
+): Promise {
+ const key = secp.getSharedSecret(privateKey, "02" + publicKey);
+ const normalizedKey = getNormalizedX(key);
+ const encoder = new TextEncoder();
+ const iv = Uint8Array.from(randomBytes(16));
+ const plaintext = encoder.encode(message);
+ const cryptoKey = await crypto.subtle.importKey(
+ "raw",
+ normalizedKey,
+ { name: "AES-CBC" },
+ false,
+ ["encrypt"],
+ );
+ const ciphertext = await crypto.subtle.encrypt(
+ { name: "AES-CBC", iv },
+ cryptoKey,
+ plaintext,
+ );
+
+ const ctb64 = toBase64(new Uint8Array(ciphertext));
+ const ivb64 = toBase64(new Uint8Array(iv.buffer));
+ return `${ctb64}?iv=${ivb64}`;
+}
+
+export async function decrypt(
+ privateKey: string,
+ publicKey: string,
+ data: string,
+): Promise {
+ const key = secp.getSharedSecret(privateKey, "02" + publicKey); // this line is very slow
+ return decrypt_with_shared_secret(data, key);
+}
+
+export async function decrypt_with_shared_secret(
+ data: string,
+ sharedSecret: Uint8Array,
+): Promise {
+ const [ctb64, ivb64] = data.split("?iv=");
+ const normalizedKey = getNormalizedX(sharedSecret);
+
+ const cryptoKey = await crypto.subtle.importKey(
+ "raw",
+ normalizedKey,
+ { name: "AES-CBC" },
+ false,
+ ["decrypt"],
+ );
+ let ciphertext: BufferSource;
+ let iv: BufferSource;
+ try {
+ ciphertext = decodeBase64(ctb64);
+ iv = decodeBase64(ivb64);
+ } catch (e) {
+ return new Error(`failed to decode, ${e}`);
+ }
+
+ try {
+ const plaintext = await crypto.subtle.decrypt(
+ { name: "AES-CBC", iv },
+ cryptoKey,
+ ciphertext,
+ );
+ const text = utf8Decode(plaintext);
+ return text;
+ } catch (e) {
+ return new Error(`failed to decrypt, ${e}`);
+ }
+}
+
+export function utf8Encode(str: string) {
+ let encoder = new TextEncoder();
+ return encoder.encode(str);
+}
+
+export function utf8Decode(bin: Uint8Array | ArrayBuffer): string {
+ let decoder = new TextDecoder();
+ return decoder.decode(bin);
+}
+
+function toBase64(uInt8Array: Uint8Array) {
+ let strChunks = new Array(uInt8Array.length);
+ let i = 0;
+ // @ts-ignore
+ for (let byte of uInt8Array) {
+ strChunks[i] = String.fromCharCode(byte); // bytes to utf16 string
+ i++;
+ }
+ return btoa(strChunks.join(""));
+}
+
+function decodeBase64(base64String: string) {
+ const binaryString = atob(base64String);
+ const length = binaryString.length;
+ const bytes = new Uint8Array(length);
+
+ for (let i = 0; i < length; i++) {
+ bytes[i] = binaryString.charCodeAt(i);
+ }
+ return bytes;
+}
+
+function getNormalizedX(key: Uint8Array): Uint8Array {
+ return key.slice(1, 33);
+}
+
+function randomBytes(bytesLength: number = 32) {
+ return crypto.getRandomValues(new Uint8Array(bytesLength));
+}
+
+export function utf16Encode(str: string): number[] {
+ let array = new Array(str.length);
+ for (let i = 0; i < str.length; i++) {
+ array[i] = str.charCodeAt(i);
+ }
+ return array;
+}
\ No newline at end of file
diff --git a/src/nip04.ts b/src/nip04.ts
new file mode 100644
index 0000000..f121e94
--- /dev/null
+++ b/src/nip04.ts
@@ -0,0 +1,86 @@
+import { randomBytes } from '@noble/hashes/utils'
+import { secp256k1 } from '@noble/curves/secp256k1'
+import { base64 } from '@scure/base'
+import { getPublicKey } from 'nostr-tools'
+
+export const utf8Decoder = new TextDecoder('utf-8')
+export const utf8Encoder = new TextEncoder()
+
+function toBase64(uInt8Array: Uint8Array) {
+ let strChunks = new Array(uInt8Array.length);
+ let i = 0;
+ // @ts-ignore
+ for (let byte of uInt8Array) {
+ strChunks[i] = String.fromCharCode(byte); // bytes to utf16 string
+ i++;
+ }
+ return btoa(strChunks.join(""));
+}
+
+function fromBase64(base64String: string) {
+ const binaryString = atob(base64String);
+ const length = binaryString.length;
+ const bytes = new Uint8Array(length);
+
+ for (let i = 0; i < length; i++) {
+ bytes[i] = binaryString.charCodeAt(i);
+ }
+ return bytes;
+}
+
+function getNormalizedX(key: Uint8Array): Uint8Array {
+ return key.slice(1, 33)
+}
+
+export class Nip04 {
+ private cache = new Map()
+
+ private async getKey(privkey: string, pubkey: string) {
+ const id = getPublicKey(privkey) + pubkey
+ let cryptoKey = this.cache.get(id)
+ if (cryptoKey) return cryptoKey
+
+ const key = secp256k1.getSharedSecret(privkey, '02' + pubkey)
+ const normalizedKey = getNormalizedX(key)
+ cryptoKey = await crypto.subtle.importKey('raw', normalizedKey, { name: 'AES-CBC' }, false, ['encrypt', 'decrypt'])
+ this.cache.set(id, cryptoKey)
+ return cryptoKey
+ }
+
+ public async encrypt(privkey: string, pubkey: string, text: string): Promise {
+ const t1 = Date.now()
+ const cryptoKey = await this.getKey(privkey, pubkey)
+ const t2 = Date.now()
+ let iv = Uint8Array.from(randomBytes(16))
+ let plaintext = utf8Encoder.encode(text)
+ let ciphertext = await crypto.subtle.encrypt({ name: 'AES-CBC', iv }, cryptoKey, plaintext)
+ const t3 = Date.now()
+ let ctb64 = base64.encode(new Uint8Array(ciphertext))
+ let ivb64 = base64.encode(new Uint8Array(iv.buffer))
+ // let ctb64 = toBase64(new Uint8Array(ciphertext))
+ // let ivb64 = toBase64(new Uint8Array(iv.buffer))
+
+ console.log("nip04_encrypt", text, "t1", t2 - t1, "t2", t3 - t2, "t3", Date.now() - t3)
+
+ return `${ctb64}?iv=${ivb64}`
+ }
+
+ public async decrypt(privkey: string, pubkey: string, data: string): Promise {
+ let [ctb64, ivb64] = data.split('?iv=')
+
+ const cryptoKey = await this.getKey(privkey, pubkey)
+
+ let ciphertext = base64.decode(ctb64)
+ let iv = base64.decode(ivb64)
+ // let ciphertext = fromBase64(ctb64)
+ // let iv = fromBase64(ivb64)
+
+ let plaintext = await crypto.subtle.decrypt({ name: 'AES-CBC', iv }, cryptoKey, ciphertext)
+
+ let text = utf8Decoder.decode(plaintext)
+ return text
+ }
+
+}
+
+
diff --git a/src/signer.ts b/src/signer.ts
new file mode 100644
index 0000000..307a3a4
--- /dev/null
+++ b/src/signer.ts
@@ -0,0 +1,70 @@
+import type { UnsignedEvent } from "nostr-tools";
+import { generatePrivateKey, getPublicKey, getSignature } from "nostr-tools";
+
+import type { NostrEvent } from '@nostr-dev-kit/ndk' // "./ndk-dist";
+import { NDKUser } from '@nostr-dev-kit/ndk' // "./ndk-dist";
+import type { NDKSigner } from '@nostr-dev-kit/ndk' // "./ndk-dist";
+import { Nip04 } from "./nip04";
+//import { decrypt, encrypt } from "./ende";
+
+export class PrivateKeySigner implements NDKSigner {
+ private _user: NDKUser | undefined;
+ privateKey?: string;
+ private nip04: Nip04
+
+ public constructor(privateKey?: string) {
+ if (privateKey) {
+ this.privateKey = privateKey;
+ this._user = new NDKUser({
+ hexpubkey: getPublicKey(this.privateKey),
+ });
+ }
+ this.nip04 = new Nip04()
+ }
+
+ public static generate() {
+ const privateKey = generatePrivateKey();
+ return new PrivateKeySigner(privateKey);
+ }
+
+ public async blockUntilReady(): Promise {
+ if (!this._user) {
+ throw new Error("NDKUser not initialized");
+ }
+ return this._user;
+ }
+
+ public async user(): Promise {
+ await this.blockUntilReady();
+ return this._user as NDKUser;
+ }
+
+ public async sign(event: NostrEvent): Promise {
+ if (!this.privateKey) {
+ throw Error("Attempted to sign without a private key");
+ }
+
+ return getSignature(event as UnsignedEvent, this.privateKey);
+ }
+
+ public async encrypt(recipient: NDKUser, value: string): Promise {
+ if (!this.privateKey) {
+ throw Error("Attempted to encrypt without a private key");
+ }
+
+ const recipientHexPubKey = recipient.hexpubkey;
+ return await this.nip04.encrypt(this.privateKey, recipientHexPubKey, value);
+// return await encrypt(recipientHexPubKey, value, this.privateKey);
+ }
+
+ public async decrypt(sender: NDKUser, value: string): Promise {
+ if (!this.privateKey) {
+ throw Error("Attempted to decrypt without a private key");
+ }
+
+ const senderHexPubKey = sender.hexpubkey;
+// console.log("nip04_decrypt", value)
+ return await this.nip04.decrypt(this.privateKey, senderHexPubKey, value);
+// return await decrypt(this.privateKey, senderHexPubKey, value) as string;
+ }
+}
\ No newline at end of file