Add importKey, add custom signer for backend, remove db from critical path of RPC with given perms

This commit is contained in:
artur 2023-12-05 18:50:14 +03:00
parent 3552447383
commit d6bfdb345e
6 changed files with 361 additions and 30 deletions

View File

@ -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' />

View File

@ -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') {

View File

@ -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
View 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
View 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
View 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;
}
}