flotilla/src/app/components/KeyDownload.svelte

182 lines
5.9 KiB
Svelte
Raw Normal View History

2026-01-12 23:21:58 +00:00
<script lang="ts">
import cx from "classnames"
2026-01-12 23:21:58 +00:00
import {nsecEncode} from "nostr-tools/nip19"
import {encrypt} from "nostr-tools/nip49"
import {hexToBytes} from "@welshman/lib"
import {preventDefault, downloadText} from "@lib/html"
import {errorMessage} from "@lib/util"
2026-01-12 23:21:58 +00:00
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
import ArrowDown from "@assets/icons/arrow-down.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Field from "@lib/components/Field.svelte"
import Button from "@lib/components/Button.svelte"
2026-01-30 22:14:12 +00:00
import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte"
2026-01-12 23:21:58 +00:00
import ModalHeader from "@lib/components/ModalHeader.svelte"
2026-02-02 18:09:14 +00:00
import ModalTitle from "@lib/components/ModalTitle.svelte"
2026-01-12 23:21:58 +00:00
import ModalFooter from "@lib/components/ModalFooter.svelte"
import ProgressBar from "@app/components/ProgressBar.svelte"
2026-06-08 21:14:40 +00:00
import {pushToast} from "@app/toast"
import {PLATFORM_NAME} from "@app/env"
2026-01-12 23:21:58 +00:00
type Props = {
2026-01-13 22:28:21 +00:00
secret: string
2026-01-12 23:21:58 +00:00
next: () => unknown
submitText?: string
step?: number
totalSteps?: number
2026-01-12 23:21:58 +00:00
}
const {secret, next, submitText = "Continue", step, totalSteps}: Props = $props()
2026-01-12 23:21:58 +00:00
const back = () => history.back()
const cleanupCopy = (copy: string) =>
copy
.replace(/\n\s*\n\s*/g, "NEWLINE")
.replace(/\s+/g, " ")
.replace(/NEWLINE/g, "\n\n")
.trim()
2026-07-08 21:53:49 +00:00
const downloadKey = async () => {
2026-01-12 23:21:58 +00:00
const sharedCopy = `
Most online services keep track of users by giving them a username and password. This gives the
service total control over their users, allowing them to ban them at any time, or sell their activity.
On Nostr, you control your own identity and social data, through the magic of cryptography. The basic
idea is that you have a public key, which acts as your user ID, and a private key which allows you to
prove your identity.
It's very important to keep your private key secret because it grants permanent and complete access to your
account.
`
2026-07-08 21:53:49 +00:00
let instructions: string
2026-01-12 23:21:58 +00:00
if (usePassword) {
if (password.length < 12) {
return pushToast({
theme: "error",
message: "Your password must be at least 12 characters long.",
})
}
const ncryptsec = encrypt(hexToBytes(secret), password)
2026-07-08 21:53:49 +00:00
instructions = `
2026-01-12 23:21:58 +00:00
This file contains a backup of your Nostr secret key, downloaded from ${PLATFORM_NAME} and encrypted using
a password you chose when you signed up.
${sharedCopy}
Your encrypted private key is:
${ncryptsec}
To use it to log in to other Nostr apps, find a Nostr Signer app (https://nostrapps.com/#signers is a good
place to look), and import your key.
`
} else {
const nsec = nsecEncode(hexToBytes(secret))
2026-07-08 21:53:49 +00:00
instructions = `
2026-01-12 23:21:58 +00:00
This file contains a backup of your Nostr secret key, downloaded from ${PLATFORM_NAME}.
${sharedCopy}
Your private key is:
${nsec}
To use it to log in to other Nostr apps, find a Nostr Signer app (https://nostrapps.com/#signers is a good
place to look), and import your key.
`
}
2026-07-08 21:53:49 +00:00
try {
await downloadText("Nostr Secret Key.txt", cleanupCopy(instructions))
} catch (e) {
// Dismissing the native share sheet rejects with "Share canceled" — leave
// the flow gated so the user can try again rather than showing an error.
if (!errorMessage(e).toLowerCase().includes("cancel")) {
2026-07-08 21:53:49 +00:00
pushToast({theme: "error", message: "We couldn't save your key. Please try again."})
}
}
didDownload = true
2026-01-12 23:21:58 +00:00
}
const onPasswordChange = () => {
didDownload = false
}
const toggleUsePassword = () => {
usePassword = !usePassword
didDownload = false
}
let password = $state("")
let usePassword = $state(false)
let didDownload = $state(false)
</script>
2026-01-30 22:14:12 +00:00
<Modal tag="form" onsubmit={preventDefault(next)}>
<ModalBody>
<ModalHeader>
2026-02-02 18:09:14 +00:00
<ModalTitle>Your Keys are Ready!</ModalTitle>
2026-01-30 22:14:12 +00:00
</ModalHeader>
<p>
A cryptographic key pair has two parts: your <strong>public key</strong> identifies your
account, while your <strong>private key</strong> acts sort of like a master password.
</p>
<p>
Securing your private key is very important, so make sure to take the time to save your key in
a secure place (like a password manager).
</p>
{#if usePassword}
<Field>
{#snippet label()}
Password*
{/snippet}
{#snippet input()}
<label class="input flex w-full items-center gap-2">
2026-01-30 22:14:12 +00:00
<Icon icon={Key} />
<input bind:value={password} onchange={onPasswordChange} class="grow" type="password" />
</label>
{/snippet}
{#snippet info()}
<p>Passwords should be at least 12 characters long. Write this down!</p>
{/snippet}
</Field>
{/if}
<div class="flex flex-col">
<Button
class={cx(`button button-${didDownload ? "neutral" : "primary"}`)}
onclick={downloadKey}>
2026-01-30 22:14:12 +00:00
Download my key
<Icon icon={ArrowDown} />
</Button>
<Button class="button button-link no-underline" onclick={toggleUsePassword}>
2026-01-30 22:14:12 +00:00
{#if usePassword}
Nevermind, I want to download the plain version
{:else}
I want to download an encrypted version
{/if}
</Button>
</div>
</ModalBody>
{#if step && totalSteps}
<ProgressBar current={step} total={totalSteps} />
{/if}
2026-01-12 23:21:58 +00:00
<ModalFooter>
<Button class="button button-link" onclick={back}>
2026-01-12 23:21:58 +00:00
<Icon icon={AltArrowLeft} />
Go back
</Button>
<Button class="button button-primary" disabled={!didDownload} type="submit">
2026-01-12 23:21:58 +00:00
{submitText}
<Icon icon={AltArrowRight} />
</Button>
</ModalFooter>
2026-01-30 22:14:12 +00:00
</Modal>