From c5ee254c484f9b447d32f0c85e4a930d67eb1767 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 7 Feb 2024 10:46:11 -0800 Subject: nostr key import --- bun.lockb | Bin 119377 -> 120340 bytes package.json | 2 +- src/lib/nostr.js | 89 ++++++++++++--------- src/locales/en.json | 1 + .../[username]/settings/[...tab]/_security.svelte | 37 +++++++-- 5 files changed, 84 insertions(+), 45 deletions(-) diff --git a/bun.lockb b/bun.lockb index 3216c9fd..a4c6213a 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 1a3f9638..dbd537e2 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "leaflet": "^1.9.3", "leaflet.markercluster": "^1.5.3", "maplibre-gl": "^3.3.1", - "nostr-tools": "^1.14.2", + "nostr-tools": "^2.1.5", "postcss": "^8.4.20", "prettier": "2", "prettier-plugin-svelte": "^2.10.1", diff --git a/src/lib/nostr.js b/src/lib/nostr.js index 5ab87e51..66f3bd3a 100644 --- a/src/lib/nostr.js +++ b/src/lib/nostr.js @@ -11,22 +11,26 @@ import { import { goto, invalidate } from "$app/navigation"; import { getPublicKey, - signEvent, + finalizeEvent, getEventHash, nip04, - nip06, nip19, } from "nostr-tools"; + +import { + encrypt as nip49encrypt, + decrypt as nip49decrypt, +} from "nostr-tools/nip49"; + import { mnemonicToEntropy, entropyToMnemonic } from "@scure/bip39"; import wordlist from "$lib/english"; import { bech32m } from "@scure/base"; -const { generateSeedWords, privateKeyFromSeedWords } = nip06; +import { generateSeedWords, privateKeyFromSeedWords } from "nostr-tools/nip06"; const { encode, decode, toWords, fromWords } = bech32m; export let generate = async (user) => { if (!get(pin)?.length === 6) return; - let password = get(pw); let salt = crypto.getRandomValues(new Uint8Array(16)); let mnemonic = generateSeedWords(); @@ -35,7 +39,7 @@ export let generate = async (user) => { let bytes = new Uint8Array( await crypto.subtle.encrypt( { name: "AES-GCM", iv: new Uint8Array(16) }, - await stretch(password, salt), + await stretch(await getPassword(), salt), Uint8Array.from(Buffer.from(mnemonicToEntropy(mnemonic, wordlist), "hex")) ) ); @@ -78,29 +82,24 @@ function typedArrayToBuffer(array) { ); } -export let getMnemonic = async (user) => { - let { cipher, username, salt } = user; - let password = get(pw); - - if (!password) { - passwordPrompt.set(true); - } - - try { - await post("/password", { password }); - } catch (e) { - passwordPrompt.set(true); - } +export let getPrivateKey = async (user) => { + let { nsec, username, salt } = user; + let privkey = privateKeyFromSeedWords(await getMnemonic(user)); - await wait(() => !!get(pw)); - password = get(pw); + if (nsec) { + let decrypted = nip49decrypt(nsec, await getPassword()); + return decrypted; + } else return privkey; +}; +export let getMnemonic = async (user) => { + let { cipher, username, salt } = user; let mnemonic, key, seed, entropy, child, privkey; entropy = new Uint8Array( await crypto.subtle.decrypt( { name: "AES-GCM", iv: new Uint8Array(16) }, - await stretch(password, Buffer.from(salt, "hex")), + await stretch(await getPassword(), Buffer.from(salt, "hex")), Uint8Array.from(fromWords(decode(cipher, 180).words)) ) ); @@ -108,45 +107,57 @@ export let getMnemonic = async (user) => { return entropyToMnemonic(entropy, wordlist); }; -let getPrivateKey = async (user) => { - return privateKeyFromSeedWords(await getMnemonic(user)); -}; - export let getNsec = async (user) => { return nip19.nsecEncode(await getPrivateKey(user)); }; +export let setNsec = async (user, nsec) => { + let { data: sk } = nip19.decode(nsec); + user.pubkey = getPublicKey(sk); + user.nsec = await encryptNsec(nsec); +}; + +export let encryptNsec = async (nsec) => { + let { type, data } = nip19.decode(nsec); + if (type === "nsec" && data.length === 32) { + return nip49encrypt(data, await getPassword()); + } else { + fail("invalid nsec"); + } +}; + export let sign = async ({ event, user }) => { - event.id = getEventHash(event); - event.sig = signEvent(event, await getPrivateKey(user)); + event = finalizeEvent(event, await getPrivateKey(user)); }; export let send = (event) => { return post("/events", { event }); }; -export let reEncryptEntropy = async (user, newPassword) => { - let { cipher, username, salt } = user; +let getPassword = async () => { let password = get(pw); - if (!password) { - passwordPrompt.set(true); - } - - try { - await post("/password", { password }); - } catch (e) { - passwordPrompt.set(true); + if (!password) passwordPrompt.set(true); + else { + try { + await post("/password", { password }); + } catch (e) { + passwordPrompt.set(true); + } } await wait(() => !!get(pw)); - password = get(pw); + return get(pw); +}; + +export let reEncryptEntropy = async (user, newPassword) => { + let { cipher, username, salt } = user; let mnemonic, key, seed, entropy, child, privkey; entropy = Buffer.from( await crypto.subtle.decrypt( { name: "AES-GCM", iv: new Uint8Array(16) }, - await stretch(password, Buffer.from(salt, "hex")), + await stretch(await getPassword(), Buffer.from(salt, "hex")), Uint8Array.from(fromWords(decode(cipher, 180).words)) ), "hex" diff --git a/src/locales/en.json b/src/locales/en.json index 2d29b1e5..84f58ced 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -201,6 +201,7 @@ "placeholder": "Enter an address, invoice or username" }, "settings": { + "import": "Import key", "destination": "Autowithdrawal address", "destinationDesc": "The address that withdrawals will be sent to", "destinationPlaceholder": "Enter a Bitcoin, Liquid or Lightning address", diff --git a/src/routes/(app)/[username]/settings/[...tab]/_security.svelte b/src/routes/(app)/[username]/settings/[...tab]/_security.svelte index 129b57df..7151f619 100644 --- a/src/routes/(app)/[username]/settings/[...tab]/_security.svelte +++ b/src/routes/(app)/[username]/settings/[...tab]/_security.svelte @@ -1,5 +1,5 @@