Add ndk and nsecbunker login

This commit is contained in:
Jonathan Staab 2023-07-03 05:24:51 -07:00
parent 8b3de91408
commit a2157a392c
9 changed files with 177 additions and 11 deletions

BIN
package-lock.json generated

Binary file not shown.

View File

@ -38,6 +38,7 @@
"@capacitor/ios": "^4.7.3",
"@fortawesome/fontawesome-free": "^6.2.1",
"@noble/secp256k1": "^1.7.0",
"@nostr-dev-kit/ndk": "^0.7.0",
"@scure/base": "^1.1.1",
"@tsconfig/svelte": "^3.0.0",
"classnames": "^2.3.2",

View File

@ -1,4 +1,5 @@
import {nip19, nip04, getPublicKey, getEventHash, signEvent} from "nostr-tools"
import {nip19, nip04, getPublicKey, getEventHash, signEvent, generatePrivateKey} from "nostr-tools"
import NDK, {NDKEvent, NDKNip46Signer, NDKPrivateKeySigner} from "@nostr-dev-kit/ndk"
import {get} from "svelte/store"
import {error} from "src/util/logger"
import {synced} from "src/util/misc"
@ -6,22 +7,56 @@ import {synced} from "src/util/misc"
const method = synced("agent/keys/method")
const pubkey = synced("agent/keys/pubkey")
const privkey = synced("agent/keys/privkey")
const bunkerKey = synced("agent/keys/bunkerKey")
const getExtension = () => (window as {nostr?: any}).nostr
const canSign = () => ["privkey", "extension"].includes(get(method))
const canSign = () => ["bunker", "privkey", "extension"].includes(get(method))
// Validate the key before setting it to state by encoding it using bech32.
// This will error if invalid (this works whether it's a public or a private key)
const validate = key => nip19.npubEncode(key)
let ndk, remoteSigner
const prepareNdk = async (token?: string) => {
const localSigner = new NDKPrivateKeySigner(get(bunkerKey))
ndk = new NDK({
explicitRelayUrls: ["wss://relay.f7z.io", "wss://relay.damus.io", "wss://relay.nsecbunker.com"],
})
remoteSigner = new NDKNip46Signer(ndk, get(pubkey), localSigner)
remoteSigner.token = token
ndk.signer = remoteSigner
await ndk.connect(5000)
await remoteSigner.blockUntilReady()
}
const getNDK = async () => {
if (!ndk) {
await prepareNdk()
}
return ndk
}
const login = ($method, key) => {
method.set($method)
pubkey.set(null)
privkey.set(null)
bunkerKey.set(null)
if ($method === "privkey") {
privkey.set(key)
pubkey.set(getPublicKey(key))
} else {
privkey.set(null)
} else if (["pubkey", "extension"].includes($method)) {
pubkey.set(key)
} else if ($method === "bunker") {
pubkey.set(key.pubkey)
bunkerKey.set(generatePrivateKey())
prepareNdk(key.token)
}
}
@ -31,12 +66,20 @@ const clear = () => {
privkey.set(null)
}
const sign = event => {
const sign = async event => {
const $method = get(method)
event.pubkey = get(pubkey)
event.id = getEventHash(event)
if ($method === "bunker") {
const ndkEvent = new NDKEvent(await getNDK(), event)
await ndkEvent.sign(remoteSigner)
return ndkEvent.rawEvent()
}
if ($method === "privkey") {
return Object.assign(event, {
sig: signEvent(event, get(privkey)),
@ -53,6 +96,23 @@ const sign = event => {
const getCrypt = () => {
const $method = get(method)
if ($method === "bunker") {
return {
encrypt: async (pubkey, message) => {
const ndk = await getNDK()
const user = ndk.getUser({hexpubkey: pubkey})
return ndk.signer.encrypt(user, message)
},
decrypt: async (pubkey, message) => {
const ndk = await getNDK()
const user = ndk.getUser({hexpubkey: pubkey})
return ndk.signer.decrypt(user, message)
},
}
}
if ($method === "privkey") {
const $privkey = get(privkey)
@ -104,6 +164,7 @@ export default {
method,
pubkey,
privkey,
bunkerKey,
canSign,
validate,
login,

View File

@ -4,7 +4,9 @@
import ChatEdit from "src/app/views/ChatEdit.svelte"
import LoginConnect from "src/app/views/LoginConnect.svelte"
import LoginPrivKey from "src/app/views/LoginPrivKey.svelte"
import LoginAdvanced from "src/app/views/LoginAdvanced.svelte"
import LoginPubKey from "src/app/views/LoginPubKey.svelte"
import LoginBunker from "src/app/views/LoginBunker.svelte"
import Onboarding from "src/app/views/Onboarding.svelte"
import NoteCreate from "src/app/views/NoteCreate.svelte"
import NoteDelete from "src/app/views/NoteDelete.svelte"
@ -50,8 +52,12 @@
<ChatEdit {...m} />
{:else if m.type === "login/privkey"}
<LoginPrivKey />
{:else if m.type === "login/advanced"}
<LoginAdvanced />
{:else if m.type === "login/pubkey"}
<LoginPubKey />
{:else if m.type === "login/bunker"}
<LoginBunker />
{:else if m.type === "login/connect"}
<LoginConnect />
{:else if m.type === "person/feed"}

View File

@ -32,8 +32,6 @@
const links = getLinks(shortContent)
const extraLinks = without(links, getLinks(fullContent))
console.log(fullContent, shortContent)
export const isNewline = i =>
!shortContent[i] ||
shortContent[i].type === NEWLINE ||

View File

@ -25,8 +25,8 @@
modal.push({type: "onboarding", stage: "intro"})
}
const pubkeyLogIn = () => {
modal.push({type: "login/pubkey"})
const advancedLogIn = () => {
modal.push({type: "login/advanced"})
}
if (user.getPubkey()) {
@ -56,7 +56,7 @@
>Log In</Anchor>
<Anchor class="w-32 text-center" theme="button" on:click={signUp}>Sign Up</Anchor>
</div>
<Anchor on:click={pubkeyLogIn}>
<Anchor on:click={advancedLogIn}>
<i class="fa fa-cogs" /> Advanced Login
</Anchor>
</div>

View File

@ -0,0 +1,42 @@
<script lang="ts">
import {modal} from "src/partials/state"
import Anchor from "src/partials/Anchor.svelte"
import Card from "src/partials/Card.svelte"
import Content from "src/partials/Content.svelte"
import Heading from "src/partials/Heading.svelte"
const BUNKER_URL = "https://nsecbunker.com"
const pubkeyLogIn = () => {
modal.push({type: "login/pubkey"})
}
const bunkerLogIn = () => {
modal.push({type: "login/bunker"})
}
</script>
<Content size="lg">
<Content class="text-center">
<Heading>Advanced Login</Heading>
<p>Find more options for logging in to nostr below.</p>
</Content>
<Content gap="gap-4">
<Card interactive on:click={pubkeyLogIn}>
<Content gap="gap-4">
<h2 class="text-lg font-bold">Log in with public key</h2>
<p>Use this to access a read-only view of your account (or someone else's).</p>
</Content>
</Card>
<Card interactive on:click={bunkerLogIn}>
<Content gap="gap-4">
<h2 class="text-lg font-bold">Log in with Nsec Bunker</h2>
<p>
Keep your keys secure by storing them in a <Anchor external href={BUNKER_URL}
>bunker</Anchor
>.
</p>
</Content>
</Card>
</Content>
</Content>

View File

@ -0,0 +1,43 @@
<script lang="ts">
import {toHex} from "src/util/nostr"
import Input from "src/partials/Input.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte"
import Heading from "src/partials/Heading.svelte"
import keys from "src/agent/keys"
import {toast} from "src/partials/state"
import {login} from "src/app/state"
let input = ""
const logIn = () => {
const [npub, token] = input.split("#")
const pubkey = npub.startsWith("npub") ? toHex(npub) : npub
try {
keys.validate(pubkey)
} catch (e) {
toast.show("error", "Sorry, but that's an invalid public key.")
return
}
login("bunker", {pubkey, token})
}
</script>
<Content size="lg" class="text-center">
<Heading>Login with NsecBunker</Heading>
<p>
To log in remotely, enter your nsec bunker token or pubkey below. If you're not using a token,
you'll need to approve authorization requests in your bunker's admin interface.
</p>
<div class="flex gap-2">
<div class="flex-grow">
<Input bind:value={input} placeholder="npub...">
<i slot="before" class="fa fa-key" />
</Input>
</div>
<Anchor theme="button" on:click={logIn}>Log In</Anchor>
</div>
</Content>

View File

@ -12,7 +12,7 @@
import keys from "src/agent/keys"
import {toast} from "src/partials/state"
const {pubkey, privkey} = keys
const {pubkey, privkey, bunkerKey} = keys
const nip07 = "https://github.com/nostr-protocol/nips/blob/master/07.md"
const keypairUrl = "https://www.cloudflare.com/learning/ssl/how-does-public-key-encryption-work/"
@ -87,6 +87,21 @@
</p>
</div>
{/if}
{#if $bunkerKey}
<div class="flex flex-col gap-1">
<strong>Bunker Key</strong>
<Input disabled type="password" value={$bunkerKey}>
<button
slot="after"
class="fa-solid fa-copy cursor-pointer"
on:click={() => copyKey("bunker", $bunkerKey)} />
</Input>
<p class="text-sm text-gray-1">
Your bunker key is used to authorize Coracle with your nsec bunker to sign events on
your behalf. Save this if you would like to log in elsewhere without re-authorizing.
</p>
</div>
{/if}
</div>
</Content>
</div>