Add importKey, add custom signer for backend, remove db from critical path of RPC with given perms
This commit is contained in:
parent
3552447383
commit
d6bfdb345e
16
src/App.tsx
16
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() {
|
||||
)
|
||||
})}
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<button onClick={generateKey}>generate key</button>
|
||||
</div>
|
||||
<div>
|
||||
<input id='nsec' placeholder='nsec' />
|
||||
<button onClick={importKey}>import key (DANGER!)</button>
|
||||
</div>
|
||||
<div>
|
||||
<input id='npub' placeholder='npub' />
|
||||
<input id='passphrase' placeholder='password' />
|
||||
|
@ -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<string | undefined> {
|
||||
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<boolean> {
|
||||
|
||||
// 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') {
|
||||
|
@ -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
|
||||
}
|
||||
},
|
||||
}
|
||||
|
126
src/ende.ts
Normal file
126
src/ende.ts
Normal file
@ -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<string> {
|
||||
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<string | Error> {
|
||||
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<string | Error> {
|
||||
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;
|
||||
}
|
86
src/nip04.ts
Normal file
86
src/nip04.ts
Normal file
@ -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<string, CryptoKey>()
|
||||
|
||||
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<string> {
|
||||
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<string> {
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
70
src/signer.ts
Normal file
70
src/signer.ts
Normal file
@ -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<NDKUser> {
|
||||
if (!this._user) {
|
||||
throw new Error("NDKUser not initialized");
|
||||
}
|
||||
return this._user;
|
||||
}
|
||||
|
||||
public async user(): Promise<NDKUser> {
|
||||
await this.blockUntilReady();
|
||||
return this._user as NDKUser;
|
||||
}
|
||||
|
||||
public async sign(event: NostrEvent): Promise<string> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
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;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user