aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2025-01-07 14:42:03 -0800
committerAdam Soltys <[email protected]>2025-01-07 14:42:03 -0800
commit2358d1bf39c34a508db6c50f0f6312ee411a8a17 (patch)
treec1a592710013c0db2f4d992f6884f4fd371f125d /src
parent493cb3a3504fdc2791d47fd032f5ac9d2ccc66b7 (diff)
downloadcoinos-ui-2358d1bf39c34a508db6c50f0f6312ee411a8a17.tar.gz
coinos-ui-2358d1bf39c34a508db6c50f0f6312ee411a8a17.zip
nostr signin
Diffstat (limited to 'src')
-rw-r--r--src/components/Nostr.svelte260
-rw-r--r--src/lib/nostr.ts63
-rw-r--r--src/lib/store.ts1
-rw-r--r--src/routes/(app)/+layout.svelte6
-rw-r--r--src/routes/(app)/settings/+layout.svelte14
-rw-r--r--src/routes/(app)/settings/security/+page.svelte3
-rw-r--r--src/routes/(static)/login/+page.svelte2
-rw-r--r--src/routes/(static)/register/+page.server.ts7
-rw-r--r--src/routes/(static)/register/+page.svelte21
9 files changed, 133 insertions, 244 deletions
diff --git a/src/components/Nostr.svelte b/src/components/Nostr.svelte
index 6445c4c1..0765f440 100644
--- a/src/components/Nostr.svelte
+++ b/src/components/Nostr.svelte
@@ -8,6 +8,7 @@
import { encrypt, decrypt, getConversationKey } from "nostr-tools/nip44";
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
import {
+ nip04,
nip19,
getEventHash,
finalizeEvent,
@@ -16,22 +17,29 @@
} from "nostr-tools";
import { AmberClipboardSigner } from "applesauce-signer";
- import { focus, fail, post } from "$lib/utils";
+ import { copy, focus, fail, post } from "$lib/utils";
import { password as pw, passwordPrompt } from "$lib/store";
+ // import { PUBLIC_COINOS_RELAY as relayUrl } from "$env/static/public";
+ const relayUrl = "wss://relay.nsec.app";
- let { id, redirect, token, nostrSignin } = $props();
+ let { id, redirect, token, nostrSignin = $bindable() } = $props();
let extension = $state();
onMount(() => {
if (browser && window.nostr) extension = true;
});
- let cancel = () => (nostrSignin = false);
+ let cancel = () => {
+ nostrSignin = false;
+ showNsec = false;
+ pubkey = undefined;
+ connectUrl = undefined;
+ };
let sk = $state();
let ck = $state();
let pk = $state();
- let relay = $state();
+ let pubkey = $state();
let signer = $derived(
browser &&
@@ -41,137 +49,65 @@
);
let connectUrl = $state();
- // let bunkerConnect = async () => {
- // sk = generateSecretKey();
- // console.log("SK", bytesToHex(sk));
- // let pubkey = getPublicKey(sk);
- // console.log("PUBKEY", pubkey);
- //
- // const url = new URL(bunker);
- // const secret = url.searchParams.get("secret");
- // console.log("URL", url);
- // pk = url.host || url.pathname.replace("//", "");
- // console.log("PK", pk);
- // const relayUrl = url.searchParams.get("relay");
- // relay = await Relay.connect(relayUrl);
- // console.log("connecting to", relayUrl);
- //
- // ck = getConversationKey(sk, pk);
- //
- // console.log("PK", pk);
- // console.log("CK", bytesToHex(ck));
- //
- // let connect = {
- // id: crypto.randomUUID(),
- // method: "connect",
- // params: [pk, secret, "get_public_key,sign_event"],
- // };
- //
- // let event = {
- // created_at: Math.round(Date.now() / 1000),
- // kind: 24133,
- // pubkey,
- // content: encrypt(JSON.stringify(connect), ck),
- // tags: [["p", pk]],
- // };
- //
- // let signedEvent = finalizeEvent(event, sk);
- // console.log("publishing connection event", signedEvent);
- // await relay.publish(signedEvent);
- //
- // const sub = relay.subscribe([{ kinds: [24133], "#p": [pubkey] }], {
- // async onevent(ev) {
- // console.log("got event:", ev);
- // try {
- // console.log(decrypt(ev.content, ck));
- // } catch (e) {
- // console.log(e);
- // }
- // },
- // });
- // };
-
- let getPubkey = async () => {
- let pubkey = getPublicKey(sk);
- let getPubkey = {
- id: crypto.randomUUID(),
- method: "get_public_key",
- params: [],
- };
-
- let testEvent = {
- created_at: Math.round(Date.now() / 1000),
- kind: 1,
- content: "hi mom",
- tags: [],
- };
-
- let signEvent = {
- id: crypto.randomUUID(),
- method: "sign_event",
- params: [JSON.stringify(testEvent)],
- };
+ let nostrConnect = async () => {
+ let secret = crypto.randomUUID();
- let event = {
- created_at: Math.round(Date.now() / 1000),
- kind: 24133,
- pubkey,
- content: encrypt(JSON.stringify(signEvent), ck),
- tags: [["p", pk]],
- };
+ sk = generateSecretKey();
+ pubkey = getPublicKey(sk);
- let signedEvent = finalizeEvent(event, sk);
- await relay.publish(signedEvent);
- };
+ connectUrl = `nostrconnect://${pubkey}?relay=${encodeURIComponent(relayUrl)}&perms=sign_event%3A1&name=Coinos&secret=${secret}`;
- let nostrConnect = async () => {
- let secret = crypto.randomUUID();
- let sk = generateSecretKey();
- console.log("SK", bytesToHex(sk));
- let pubkey = getPublicKey(sk);
- console.log("PUBKEY", pubkey);
- connectUrl = `nostrconnect://${pubkey}?relay=wss%3A%2F%2Frelay.coinos.io&perms=nip44_encrypt%2Cnip44_decrypt%2Csign_event%3A13%2Csign_event%3A14%2Csign_event%3A1059&name=Coinos&secret=${secret}`;
+ let relay = await Relay.connect(relayUrl);
- const relay = await Relay.connect("wss://relay.coinos.io");
- console.log(`connected to ${relay.url}`);
- let finished, remoteSignerPubkey;
+ let signatureRequested;
const sub = relay.subscribe([{ kinds: [24133], "#p": [pubkey] }], {
async onevent(event) {
- console.log("got event:", event);
- remoteSignerPubkey = event.pubkey;
- // console.log("decrypted", decrypt(event.content, sk));
- if (!finished) {
- finished = true;
- let event = {
- created_at: Math.round(Date.now() / 1000),
- kind: 24133,
- pubkey,
- content: encrypt(
- JSON.stringify({
- id: "1234",
- method: "get_public_key",
- params: [],
- }),
- sk,
- hexToBytes(remoteSignerPubkey),
- ),
- tags: [["p", remoteSignerPubkey]],
+ pk = event.pubkey;
+ ck = getConversationKey(sk, pk);
+ let decrypted;
+ console.log("EVENT", event);
+ try {
+ decrypted = JSON.parse(decrypt(event.content, ck));
+ } catch (e) {
+ try {
+ decrypted = JSON.parse(await nip04.decrypt(sk, pk, event.content));
+ } catch (e) {
+ console.log("ERROR", e);
+ }
+ }
+
+ if (decrypted?.result?.includes("sig")) {
+ await nostrLogin(JSON.parse(decrypted.result));
+ } else if (!signatureRequested) {
+ signatureRequested = true;
+ let signEvent = {
+ id: crypto.randomUUID(),
+ method: "sign_event",
+ params: [JSON.stringify({ ...ev, pubkey })],
};
- console.log("asking for public key", event);
- console.log("DECRYPTED", decrypt(event.content, sk));
- let signedEvent = finalizeEvent(event, sk);
- console.log("sleeping");
- await new Promise((r) => setTimeout(r, 5000));
- console.log("sending");
- await relay.publish(signedEvent);
+ // let content = encrypt(JSON.stringify(signEvent), ck);
+ let content = await nip04.encrypt(sk, pk, JSON.stringify(signEvent));
+ console.log("CONTENT", content);
+ let signedSignEvent = finalizeEvent(
+ {
+ created_at: Math.round(Date.now() / 1000),
+ kind: 24133,
+ pubkey,
+ content,
+ tags: [["p", pk]],
+ },
+ sk,
+ );
+
+ await new Promise((r) => setTimeout(r, 1000));
+ console.log("PUBLISHING", signedSignEvent);
+ relay.publish(signedSignEvent);
}
},
});
- console.log("subbed");
};
- let pubkey = $state();
let ev = $derived({
kind: 1,
created_at: Date.now(),
@@ -200,14 +136,9 @@
ev.pubkey = pubkey;
console.log("PUBKEY", ev.pubkey);
};
- //
- // let signerSign = async () => {
- // const signer = new AmberClipboardSigner();
- // ev.sig = await signer.signEvent(ev);
- // nostrLogin(await signer.signEvent(ev));
- // };
- //
+ let signerSigning;
let signerSign = async () => {
+ signerSigning = true;
document.addEventListener("focus", signerSigned, true);
};
@@ -223,8 +154,6 @@
// let pubkey = await window.nostr.getPublicKey();
const formData = new FormData();
- console.log("SIGNED", signedEvent);
-
try {
formData.append("loginRedirect", redirect);
formData.append("token", token);
@@ -249,6 +178,8 @@
};
</script>
+hi
+{nostrSignin}
{#if nostrSignin}
<div
class="fixed inset-0 bg-base-100 bg-opacity-90 overflow-y-auto h-full w-full z-50"
@@ -260,10 +191,25 @@
{$t("login.nostr")}
</h1>
- <!-- <div class="break-all"> -->
- <!-- {JSON.stringify(ev)} -->
- <!-- </div> -->
- {#if pubkey}
+ {#if showNsec}
+ <input bind:value={nsec} placeholder="nsec..." />
+ <button type="button" class="btn" onclick={nsecSign}>
+ <iconify-icon icon="ph:key-bold" width="32"></iconify-icon>
+ <div class="my-auto">Login</div>
+ </button>
+ {:else if connectUrl}
+ <a href={connectUrl}>
+ <img
+ src={`/qr/${encodeURIComponent(connectUrl)}/raw`}
+ class="z-10 border-4 border-white"
+ alt="QR"
+ />
+ <div class="break-all">
+ {connectUrl}
+ </div>
+ </a>
+ <button class="btn" onclick={() => copy(connectUrl)}>Copy</button>
+ {:else if signerSigning && pubkey}
<a
href={`nostrsigner:${encodeURIComponent(JSON.stringify(ev))}?compressionType=none&returnType=signature&type=sign_event&appName=Coinos`}
type="button"
@@ -288,25 +234,10 @@
<div class="my-auto">NIP-10 Extension</div>
</button>
{/if}
- <!-- {#if connectUrl} -->
- <!-- <a href={connectUrl}> -->
- <!-- connect -->
- <!-- <img -->
- <!-- src={`/qr/${encodeURIComponent(connectUrl)}/raw`} -->
- <!-- class="z-10 border-4 border-white" -->
- <!-- alt="QR" -->
- <!-- /> -->
- <!-- <div class="break-all"> -->
- <!-- {connectUrl} -->
- <!-- </div> -->
- <!-- </a> -->
- <!-- <button class="btn" onclick={() => copy(connectUrl)}>Copy</button> -->
- <!-- {:else} -->
- <!-- <button type="button" class="btn" onclick={nostrConnect}> -->
- <!-- <img src="/images/nostr.png" class="w-8" /> -->
- <!-- <div class="my-auto">NIP-46 Connect</div> -->
- <!-- </button> -->
- <!-- {/if} -->
+ <button type="button" class="btn" onclick={nostrConnect}>
+ <img src="/images/nostr.png" class="w-8" />
+ <div class="my-auto">NIP-46 Connect</div>
+ </button>
{#if signer}
<a type="button" class="btn" onclick={signerGetPubkey}>
<iconify-icon
@@ -318,19 +249,16 @@
</a>
{/if}
- {#if showNsec}
- <input bind:value={nsec} placeholder="nsec..." />
- <button type="button" class="btn" onclick={nsecSign}>
- <iconify-icon icon="ph:key-bold" width="32"></iconify-icon>
- <div class="my-auto">Nsec login</div>
- </button>
- {:else}
- <button type="button" class="btn" onclick={toggleNsec}>
- <iconify-icon icon="ph:key-bold" width="32"></iconify-icon>
- <div class="my-auto">Nsec login</div>
- </button>
- {/if}
+ <button type="button" class="btn" onclick={toggleNsec}>
+ <iconify-icon icon="ph:key-bold" width="32"></iconify-icon>
+ <div class="my-auto">Nsec Login</div>
+ </button>
{/if}
+
+ <button type="button" class="btn" onclick={cancel}>
+ <iconify-icon icon="ph:x-bold" width="32"></iconify-icon>
+ <div class="my-auto">Cancel</div>
+ </button>
</div>
</div>
{/if}
diff --git a/src/lib/nostr.ts b/src/lib/nostr.ts
index e45ce634..77c1d60a 100644
--- a/src/lib/nostr.ts
+++ b/src/lib/nostr.ts
@@ -1,7 +1,12 @@
import { Buffer } from "buffer";
import { browser } from "$app/environment";
-import { decrypted, password as pw, passwordPrompt, pin } from "$lib/store";
-import { post, stretch, wait } from "$lib/utils";
+import {
+ decrypted,
+ password as pw,
+ passwordPrompt,
+ signaturePrompt,
+} from "$lib/store";
+import { post, wait } from "$lib/utils";
import {
type EventTemplate,
finalizeEvent,
@@ -16,11 +21,7 @@ import {
encrypt as nip49encrypt,
} from "nostr-tools/nip49";
-import { bech32m, hex } from "@scure/base";
-import { entropyToMnemonic } from "@scure/bip39";
-import { wordlist } from "@scure/bip39/wordlists/english";
-
-import { privateKeyFromSeedWords } from "nostr-tools/nip06";
+import { bech32m } from "@scure/base";
type User = {
[key: string]: any;
@@ -32,8 +33,6 @@ type EncryptParams = {
user: User;
};
-const { encode, decode, toWords, fromWords } = bech32m;
-
export const encrypt = async ({ message, recipient, user }: EncryptParams) => {
const sk = await getPrivateKey(user);
return nip04.encrypt(sk, recipient, message);
@@ -74,25 +73,14 @@ export const getPrivateKey = async (user: User): Promise<Uint8Array> => {
if (nsec) {
k = nip49decrypt(nsec, await getPassword());
- } else k = privateKeyFromSeedWords(await getMnemonic(user));
+ } else {
+ throw new Error("nsec not available");
+ }
localStorage.setItem("nsec", nip19.nsecEncode(k));
return k;
};
-export const getMnemonic = async (user: User) => {
- const { cipher, salt } = user;
- const entropy = new Uint8Array(
- await crypto.subtle.decrypt(
- { name: "AES-GCM", iv: new Uint8Array(16) },
- await stretch(await getPassword(), Buffer.from(salt, "hex")),
- Uint8Array.from(fromWords(decode(cipher, 180).words)),
- ),
- );
-
- return entropyToMnemonic(entropy, wordlist);
-};
-
const decodeNsec = (nsec: string): Uint8Array => {
const { type, data } = nip19.decode(nsec);
if (type === "nsec" && data instanceof Uint8Array) return data;
@@ -119,8 +107,13 @@ type SignParams = {
};
export const sign = async ({ event, user }: SignParams) => {
- const sk = await getPrivateKey(user);
- event = finalizeEvent(event, sk);
+ let sk;
+ if (user.nsec) {
+ sk = await getPrivateKey(user);
+ event = finalizeEvent(event, sk);
+ } else {
+ signaturePrompt.set(event);
+ }
};
export const send = (event: EventTemplate) => {
@@ -132,23 +125,3 @@ const getPassword = async (): Promise<string> => {
await wait(() => !!get(pw));
return get(pw) || "";
};
-
-export const reEncryptEntropy = async (user: User, newPassword: string) => {
- const { cipher, salt } = user;
-
- const entropy = await crypto.subtle.decrypt(
- { name: "AES-GCM", iv: new Uint8Array(16) },
- await stretch(await getPassword(), hex.decode(salt)),
- Uint8Array.from(fromWords(decode(cipher, 180).words)),
- );
-
- const bytes = new Uint8Array(
- await crypto.subtle.encrypt(
- { name: "AES-GCM", iv: new Uint8Array(16) },
- await stretch(newPassword, hex.decode(salt)),
- entropy,
- ),
- );
-
- return encode("en", toWords(bytes), 180);
-};
diff --git a/src/lib/store.ts b/src/lib/store.ts
index 2532018d..0aba3d46 100644
--- a/src/lib/store.ts
+++ b/src/lib/store.ts
@@ -54,6 +54,7 @@ export const mnemonic = persistSession("mnemonic");
export const newPayment = persistLocal("newPayment");
export const password = writable<string | undefined>();
export const passwordPrompt = writable();
+export const signaturePrompt = writable();
export const pin = persistLocal("pin");
export const rate = writable();
export const request = writable();
diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte
index 31238a87..e179005f 100644
--- a/src/routes/(app)/+layout.svelte
+++ b/src/routes/(app)/+layout.svelte
@@ -8,6 +8,7 @@
request,
ndef,
passwordPrompt,
+ signaturePrompt,
password,
pin,
theme as themeStore,
@@ -16,6 +17,7 @@
import { browser } from "$app/environment";
import LoadingSplash from "$comp/LoadingSplash.svelte";
import AppHeader from "$comp/AppHeader.svelte";
+ import Nostr from "$comp/Nostr.svelte";
import Password from "$comp/Password.svelte";
import { getCookie, warning } from "$lib/utils";
import { t, locale, loading } from "$lib/translations";
@@ -89,6 +91,10 @@
<Password {user} />
{/if}
+{#if browser && $signaturePrompt}
+ <Nostr event={$signaturePrompt} />
+{/if}
+
<svelte:head>
{#if subject}
<title>coinos - {subject.username}</title>
diff --git a/src/routes/(app)/settings/+layout.svelte b/src/routes/(app)/settings/+layout.svelte
index 134b8933..85e5b0e6 100644
--- a/src/routes/(app)/settings/+layout.svelte
+++ b/src/routes/(app)/settings/+layout.svelte
@@ -12,7 +12,7 @@
import { avatar, banner, password, pin, save } from "$lib/store";
import { upload } from "$lib/upload";
import { page } from "$app/stores";
- import { sign, send, reEncryptEntropy, setNsec } from "$lib/nostr";
+ import { sign, send, setNsec } from "$lib/nostr";
import { invalidateAll } from "$app/navigation";
let { children, data, form } = $props();
@@ -59,18 +59,6 @@
data.set("pubkey", user.pubkey);
}
- if (data.get("password")) {
- try {
- data.set(
- "cipher",
- await reEncryptEntropy(prev, data.get("password")),
- );
- } catch (e) {
- console.log("Failed to encrypt keys with new password");
- throw e;
- }
- }
-
if ($avatar) {
try {
let { hash } = JSON.parse(
diff --git a/src/routes/(app)/settings/security/+page.svelte b/src/routes/(app)/settings/security/+page.svelte
index 3a427546..5d4b492f 100644
--- a/src/routes/(app)/settings/security/+page.svelte
+++ b/src/routes/(app)/settings/security/+page.svelte
@@ -1,6 +1,5 @@
<script>
import { getContext } from "svelte";
- import { getMnemonic } from "$lib/nostr";
import { tick } from "svelte";
import { t } from "$lib/translations";
import Pin from "$comp/Pin.svelte";
@@ -19,7 +18,6 @@
let confirming2fa = $state(),
disabling2fa = $state(),
importing = $state(),
- mnemonic = $state(),
newNsec = $state(),
nsec = $state(),
otp = $state(),
@@ -50,7 +48,6 @@
let toggleSeed = async () => {
try {
- mnemonic = await getMnemonic(user);
revealSeed = !revealSeed;
} catch (e) {
console.log(e);
diff --git a/src/routes/(static)/login/+page.svelte b/src/routes/(static)/login/+page.svelte
index ca6ebf73..7e8234ee 100644
--- a/src/routes/(static)/login/+page.svelte
+++ b/src/routes/(static)/login/+page.svelte
@@ -164,4 +164,4 @@
</div>
{/if}
-<Nostr {id} {nostrSignin} {redirect} {token} />
+<Nostr {id} bind:nostrSignin {redirect} {token} />
diff --git a/src/routes/(static)/register/+page.server.ts b/src/routes/(static)/register/+page.server.ts
index 561b34a0..bf908c54 100644
--- a/src/routes/(static)/register/+page.server.ts
+++ b/src/routes/(static)/register/+page.server.ts
@@ -1,4 +1,4 @@
-import { get, fd, login, post } from "$lib/utils";
+import { fd, get, login, post } from "$lib/utils";
import { fail, redirect } from "@sveltejs/kit";
export const load = async ({ parent }) => {
@@ -16,9 +16,8 @@ export const actions = {
const ip = request.headers.get("cf-connecting-ip");
const form = await fd(request);
- let { picture, username, password, cipher, salt, pubkey, loginRedirect } =
- form;
- const user = { picture, username, password, cipher, salt, pubkey };
+ let { picture, username, password, pubkey, loginRedirect } = form;
+ const user = { picture, username, password, pubkey };
let error;
if (loginRedirect === "undefined") loginRedirect = undefined;
diff --git a/src/routes/(static)/register/+page.svelte b/src/routes/(static)/register/+page.svelte
index 69c6a588..77394e1a 100644
--- a/src/routes/(static)/register/+page.svelte
+++ b/src/routes/(static)/register/+page.svelte
@@ -1,6 +1,5 @@
<script>
import { browser } from "$app/environment";
- import { onMount } from "svelte";
import punks from "$lib/punks";
import { upload } from "$lib/upload";
import { afterNavigate, invalidateAll } from "$app/navigation";
@@ -9,6 +8,7 @@
import { fly } from "svelte/transition";
import { enhance } from "$app/forms";
+ import Nostr from "$comp/Nostr.svelte";
import Pin from "$comp/Pin.svelte";
import Spinner from "$comp/Spinner.svelte";
import PasswordInput from "$comp/PasswordInput.svelte";
@@ -26,12 +26,9 @@
} from "unique-names-generator";
let { form, data } = $props();
- let nostr = $state();
let { id } = $derived(data);
- onMount(() => {
- if (browser && window.nostr) nostr = true;
- });
+ let nostrSignin = $state();
let username = $state();
let { index } = $state(data);
@@ -113,7 +110,7 @@
} catch (e) {
console.log("problem uploading avatar", e);
}
- }
+ }
const response = await fetch("/register", {
method: "POST",
@@ -312,12 +309,10 @@
{/if}
</button>
- {#if nostr}
- <button type="button" class="btn" onclick={nostrLogin}>
- <img src="/images/nostr.png" class="w-8" />
- <div class="my-auto">{$t("login.nostr")}</div>
- </button>
- {/if}
+ <button type="button" class="btn" onclick={() => (nostrSignin = true)}>
+ <img src="/images/nostr.png" class="w-8" />
+ <div class="my-auto">{$t("login.nostr")}</div>
+ </button>
<p class="text-secondary text-center font-medium">
{$t("login.haveAccount")}
@@ -330,3 +325,5 @@
</p>
</form>
</div>
+
+<Nostr {id} {nostrSignin} {redirect} {token} />