aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xbun.lockbbin119377 -> 120340 bytes
-rw-r--r--package.json2
-rw-r--r--src/lib/nostr.js89
-rw-r--r--src/locales/en.json1
-rw-r--r--src/routes/(app)/[username]/settings/[...tab]/_security.svelte37
5 files changed, 84 insertions, 45 deletions
diff --git a/bun.lockb b/bun.lockb
index 3216c9fd..a4c6213a 100755
--- a/bun.lockb
+++ b/bun.lockb
Binary files 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 @@
<script>
- import { getMnemonic, getNsec } from "$lib/nostr";
+ import { getMnemonic, getNsec, setNsec } from "$lib/nostr";
import { tick } from "svelte";
import { t } from "$lib/translations";
import Pin from "$comp/Pin.svelte";
@@ -14,8 +14,10 @@
let confirming2fa,
disabling2fa,
+ importing,
locked,
mnemonic,
+ newNsec = "nsec13wekuqstlsyfrzm369yh6vjaphslq5pq9kze74acqqynjm8lvpms4qzsx9",
nsec,
old,
otp,
@@ -29,6 +31,10 @@
settingPin,
verify;
+ let importNsec = () => {
+ importing = true;
+ };
+
let toggleNsec = async () => {
try {
nsec = await getNsec(user);
@@ -301,10 +307,31 @@
{$t("user.settings.nostrDescription")}
</p>
- <button type="button" class="primary" on:click={toggleNsec}>
- <Icon icon="warning" style="mr-1 w-6 my-auto" />
- {revealNsec ? $t("user.settings.hideNsec") : $t("user.settings.revealNsec")}
- </button>
+ <div class="flex">
+ <button type="button" class="primary" on:click={toggleNsec}>
+ <Icon icon="warning" style="mr-1 w-6 my-auto" />
+ {revealNsec
+ ? $t("user.settings.hideNsec")
+ : $t("user.settings.revealNsec")}
+ </button>
+
+ <button type="button" class="primary" on:click={importNsec}>
+ <Icon icon="save" style="mr-1 w-6 my-auto" />
+ {$t("user.settings.import")}
+ </button>
+ </div>
+
+ {#if importing}
+ <input
+ name="nsec"
+ bind:value={newNsec}
+ placeholder="nsec..."
+ on:blur={() => setNsec(user, newNsec)}
+ />
+ {/if}
+
+ <input type="hidden" name="nsec" bind:value={user.nsec} />
+ <input type="hidden" name="pubkey" bind:value={user.pubkey} />
{#if revealNsec}
<div class="text-lg break-all">