diff options
| author | Adam Soltys <[email protected]> | 2025-01-07 14:42:03 -0800 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2025-01-07 14:42:03 -0800 |
| commit | 2358d1bf39c34a508db6c50f0f6312ee411a8a17 (patch) | |
| tree | c1a592710013c0db2f4d992f6884f4fd371f125d /src | |
| parent | 493cb3a3504fdc2791d47fd032f5ac9d2ccc66b7 (diff) | |
| download | coinos-ui-2358d1bf39c34a508db6c50f0f6312ee411a8a17.tar.gz coinos-ui-2358d1bf39c34a508db6c50f0f6312ee411a8a17.zip | |
nostr signin
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/Nostr.svelte | 260 | ||||
| -rw-r--r-- | src/lib/nostr.ts | 63 | ||||
| -rw-r--r-- | src/lib/store.ts | 1 | ||||
| -rw-r--r-- | src/routes/(app)/+layout.svelte | 6 | ||||
| -rw-r--r-- | src/routes/(app)/settings/+layout.svelte | 14 | ||||
| -rw-r--r-- | src/routes/(app)/settings/security/+page.svelte | 3 | ||||
| -rw-r--r-- | src/routes/(static)/login/+page.svelte | 2 | ||||
| -rw-r--r-- | src/routes/(static)/register/+page.server.ts | 7 | ||||
| -rw-r--r-- | src/routes/(static)/register/+page.svelte | 21 |
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} /> |
