From 4342669896b1134277756918964dfe8f2251acfa Mon Sep 17 00:00:00 2001 From: Kieran Date: Thu, 5 Oct 2023 14:02:41 +0100 Subject: [PATCH] Make pin optional --- packages/app/src/Element/PinPrompt.tsx | 10 +-- packages/app/src/Hooks/useLoginHandler.tsx | 20 +++--- packages/app/src/Login/Functions.ts | 22 +++---- packages/app/src/Login/LoginSession.ts | 4 +- packages/app/src/Login/MultiAccountStore.ts | 15 +++-- packages/app/src/Pages/LoginPage.tsx | 68 +++++++++++++++------ packages/app/src/Pages/new/NewUserFlow.tsx | 13 +--- packages/app/src/Pages/settings/Keys.css | 25 ++++++++ packages/app/src/Pages/settings/Keys.tsx | 27 +++++--- packages/app/src/lang.json | 63 +++++++++++-------- packages/app/src/translations/en.json | 21 ++++--- packages/system/src/encrypted.ts | 64 ++++++++++++++++++- 12 files changed, 236 insertions(+), 116 deletions(-) diff --git a/packages/app/src/Element/PinPrompt.tsx b/packages/app/src/Element/PinPrompt.tsx index b7215f5a..a9412931 100644 --- a/packages/app/src/Element/PinPrompt.tsx +++ b/packages/app/src/Element/PinPrompt.tsx @@ -3,7 +3,7 @@ import "./PinPrompt.css"; import { ReactNode, useRef, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { unwrap } from "@snort/shared"; -import { EventPublisher, InvalidPinError, PinEncrypted, PinEncryptedPayload } from "@snort/system"; +import { EventPublisher, InvalidPinError, PinEncrypted } from "@snort/system"; import useEventPublisher from "Hooks/useEventPublisher"; import { LoginStore, createPublisher, sessionNeedsPin } from "Login"; @@ -67,7 +67,7 @@ export function PinPrompt({

- {subTitle} + {subTitle ?
{subTitle}
: null} setPin(e.target.value)} @@ -113,9 +113,9 @@ export function LoginUnlock() { } async function unlockSession(pin: string) { - const key = new PinEncrypted(unwrap(login.privateKeyData) as PinEncryptedPayload); - await key.decrypt(pin); - const pub = createPublisher(login, key); + const key = unwrap(login.privateKeyData); + await key.unlock(pin); + const pub = createPublisher(login); if (pub) { if (login.preferences.pow) { pub.pow(login.preferences.pow, new WasmPowWorker()); diff --git a/packages/app/src/Hooks/useLoginHandler.tsx b/packages/app/src/Hooks/useLoginHandler.tsx index 1127ffe7..532c4e69 100644 --- a/packages/app/src/Hooks/useLoginHandler.tsx +++ b/packages/app/src/Hooks/useLoginHandler.tsx @@ -1,5 +1,5 @@ import { useIntl } from "react-intl"; -import { Nip46Signer, PinEncrypted } from "@snort/system"; +import { Nip46Signer, KeyStorage } from "@snort/system"; import { EmailRegex, MnemonicRegex } from "Const"; import { LoginSessionType, LoginStore } from "Login"; @@ -8,13 +8,11 @@ import { getNip05PubKey } from "Pages/LoginPage"; import { bech32ToHex } from "SnortUtils"; import { unwrap } from "@snort/shared"; -export class PinRequiredError extends Error {} - export default function useLoginHandler() { const { formatMessage } = useIntl(); const hasSubtleCrypto = window.crypto.subtle !== undefined; - async function doLogin(key: string, pin?: string) { + async function doLogin(key: string, pin: (key: string) => Promise) { const insecureMsg = formatMessage({ defaultMessage: "Can't login with private key on an insecure connection, please use a Nostr key manager extension instead", @@ -26,8 +24,7 @@ export default function useLoginHandler() { } const hexKey = bech32ToHex(key); if (hexKey.length === 64) { - if (!pin) throw new PinRequiredError(); - LoginStore.loginWithPrivateKey(await PinEncrypted.create(hexKey, pin)); + LoginStore.loginWithPrivateKey(await pin(hexKey)); return; } else { throw new Error("INVALID PRIVATE KEY"); @@ -36,17 +33,15 @@ export default function useLoginHandler() { if (!hasSubtleCrypto) { throw new Error(insecureMsg); } - if (!pin) throw new PinRequiredError(); const ent = generateBip39Entropy(key); - const keyHex = entropyToPrivateKey(ent); - LoginStore.loginWithPrivateKey(await PinEncrypted.create(keyHex, pin)); + const hexKey = entropyToPrivateKey(ent); + LoginStore.loginWithPrivateKey(await pin(hexKey)); return; } else if (key.length === 64) { if (!hasSubtleCrypto) { throw new Error(insecureMsg); } - if (!pin) throw new PinRequiredError(); - LoginStore.loginWithPrivateKey(await PinEncrypted.create(key, pin)); + LoginStore.loginWithPrivateKey(await pin(key)); return; } @@ -58,7 +53,6 @@ export default function useLoginHandler() { const hexKey = await getNip05PubKey(key); LoginStore.loginWithPubkey(hexKey, LoginSessionType.PublicKey); } else if (key.startsWith("bunker://")) { - if (!pin) throw new PinRequiredError(); const nip46 = new Nip46Signer(key); await nip46.init(); @@ -68,7 +62,7 @@ export default function useLoginHandler() { LoginSessionType.Nip46, undefined, nip46.relays, - await PinEncrypted.create(unwrap(nip46.privateKey), pin), + await pin(unwrap(nip46.privateKey)), ); nip46.close(); } else { diff --git a/packages/app/src/Login/Functions.ts b/packages/app/src/Login/Functions.ts index 5413a77c..f6af4681 100644 --- a/packages/app/src/Login/Functions.ts +++ b/packages/app/src/Login/Functions.ts @@ -1,4 +1,4 @@ -import { RelaySettings, EventPublisher, PinEncrypted, Nip46Signer, Nip7Signer, PrivateKeySigner } from "@snort/system"; +import { RelaySettings, EventPublisher, Nip46Signer, Nip7Signer, PrivateKeySigner, KeyStorage } from "@snort/system"; import { unixNowMs } from "@snort/shared"; import * as secp from "@noble/curves/secp256k1"; import * as utils from "@noble/curves/abstract/utils"; @@ -10,7 +10,6 @@ import { bech32ToHex, dedupeById, randomSample, sanitizeRelayUrl, unwrap } from import { SubscriptionEvent } from "Subscription"; import { System } from "index"; import { Chats, FollowsFeed, GiftsCache, Notifications } from "Cache"; -import { PinRequiredError } from "Hooks/useLoginHandler"; import { Nip7OsSigner } from "./Nip7OsSigner"; export function setRelays(state: LoginSession, relays: Record, createdAt: number) { @@ -64,7 +63,7 @@ export function clearEntropy(state: LoginSession) { /** * Generate a new key and login with this generated key */ -export async function generateNewLogin(pin: string) { +export async function generateNewLogin(pin: (key: string) => Promise) { const ent = generateBip39Entropy(); const entropy = utils.bytesToHex(ent); const privateKey = entropyToPrivateKey(ent); @@ -89,9 +88,7 @@ export async function generateNewLogin(pin: string) { const publisher = EventPublisher.privateKey(privateKey); const ev = await publisher.contactList([bech32ToHex(SnortPubKey), publicKey], newRelays); System.BroadcastEvent(ev); - - const key = await PinEncrypted.create(privateKey, pin); - LoginStore.loginWithPrivateKey(key, entropy, newRelays); + LoginStore.loginWithPrivateKey(await pin(privateKey), entropy, newRelays); } export function generateRandomKey() { @@ -175,22 +172,17 @@ export function addSubscription(state: LoginSession, ...subs: SubscriptionEvent[ } export function sessionNeedsPin(l: LoginSession) { - return l.type === LoginSessionType.PrivateKey || l.type === LoginSessionType.Nip46; + return l.privateKeyData && l.privateKeyData.shouldUnlock(); } -export function createPublisher(l: LoginSession, pin?: PinEncrypted) { +export function createPublisher(l: LoginSession) { switch (l.type) { case LoginSessionType.PrivateKey: { - if (!pin) throw new PinRequiredError(); - l.privateKeyData = pin; - return EventPublisher.privateKey(pin.value); + return EventPublisher.privateKey(unwrap(l.privateKeyData as KeyStorage).value); } case LoginSessionType.Nip46: { - if (!pin) throw new PinRequiredError(); - l.privateKeyData = pin; - const relayArgs = (l.remoteSignerRelays ?? []).map(a => `relay=${encodeURIComponent(a)}`); - const inner = new PrivateKeySigner(pin.value); + const inner = new PrivateKeySigner(unwrap(l.privateKeyData as KeyStorage).value); const nip46 = new Nip46Signer(`bunker://${unwrap(l.publicKey)}?${[...relayArgs].join("&")}`, inner); return new EventPublisher(nip46, unwrap(l.publicKey)); } diff --git a/packages/app/src/Login/LoginSession.ts b/packages/app/src/Login/LoginSession.ts index 12df2aa0..80acad12 100644 --- a/packages/app/src/Login/LoginSession.ts +++ b/packages/app/src/Login/LoginSession.ts @@ -1,4 +1,4 @@ -import { HexKey, RelaySettings, u256, PinEncrypted, PinEncryptedPayload } from "@snort/system"; +import { HexKey, RelaySettings, u256, KeyStorage } from "@snort/system"; import { UserPreferences } from "Login"; import { SubscriptionEvent } from "Subscription"; @@ -47,7 +47,7 @@ export interface LoginSession { /** * Encrypted private key */ - privateKeyData?: PinEncrypted | PinEncryptedPayload; + privateKeyData?: KeyStorage; /** * BIP39-generated, hex-encoded entropy diff --git a/packages/app/src/Login/MultiAccountStore.ts b/packages/app/src/Login/MultiAccountStore.ts index a067696d..9496961a 100644 --- a/packages/app/src/Login/MultiAccountStore.ts +++ b/packages/app/src/Login/MultiAccountStore.ts @@ -2,7 +2,7 @@ import * as secp from "@noble/curves/secp256k1"; import * as utils from "@noble/curves/abstract/utils"; import { v4 as uuid } from "uuid"; -import { HexKey, RelaySettings, PinEncrypted, EventPublisher } from "@snort/system"; +import { HexKey, RelaySettings, EventPublisher, KeyStorage, NotEncrypted } from "@snort/system"; import { deepClone, sanitizeRelayUrl, unwrap, ExternalStore } from "@snort/shared"; import { DefaultRelays } from "Const"; @@ -85,6 +85,9 @@ export class MultiAccountStore extends ExternalStore { timestamp: 0, }; v.extraChats ??= []; + if (v.privateKeyData) { + v.privateKeyData = KeyStorage.fromPayload(v.privateKeyData as object); + } } this.#loadIrisKeyIfExists(); } @@ -121,7 +124,7 @@ export class MultiAccountStore extends ExternalStore { type: LoginSessionType, relays?: Record, remoteSignerRelays?: Array, - privateKey?: PinEncrypted, + privateKey?: KeyStorage, ) { if (this.#accounts.has(key)) { throw new Error("Already logged in with this pubkey"); @@ -159,7 +162,7 @@ export class MultiAccountStore extends ExternalStore { return Object.fromEntries(DefaultRelays.entries()); } - loginWithPrivateKey(key: PinEncrypted, entropy?: string, relays?: Record) { + loginWithPrivateKey(key: KeyStorage, entropy?: string, relays?: Record) { const pubKey = utils.bytesToHex(secp.schnorr.getPublicKey(key.value)); if (this.#accounts.has(pubKey)) { throw new Error("Already logged in with this pubkey"); @@ -218,13 +221,13 @@ export class MultiAccountStore extends ExternalStore { return { ...s }; } - async #loadIrisKeyIfExists() { + #loadIrisKeyIfExists() { try { const irisKeyJSON = window.localStorage.getItem("iris.myKey"); if (irisKeyJSON) { const irisKeyObj = JSON.parse(irisKeyJSON); if (irisKeyObj.priv) { - const privateKey = await PinEncrypted.create(irisKeyObj.priv, "1234"); + const privateKey = new NotEncrypted(irisKeyObj.priv); this.loginWithPrivateKey(privateKey); window.localStorage.removeItem("iris.myKey"); } @@ -286,7 +289,7 @@ export class MultiAccountStore extends ExternalStore { } const toSave = []; for (const v of this.#accounts.values()) { - if (v.privateKeyData instanceof PinEncrypted) { + if (v.privateKeyData instanceof KeyStorage) { toSave.push({ ...v, privateKeyData: v.privateKeyData.toPayload(), diff --git a/packages/app/src/Pages/LoginPage.tsx b/packages/app/src/Pages/LoginPage.tsx index 9cb6e3ae..ba068cc3 100644 --- a/packages/app/src/Pages/LoginPage.tsx +++ b/packages/app/src/Pages/LoginPage.tsx @@ -3,15 +3,15 @@ import "./LoginPage.css"; import { CSSProperties, useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { useIntl, FormattedMessage } from "react-intl"; -import { HexKey, Nip46Signer, PinEncrypted, PrivateKeySigner } from "@snort/system"; +import { HexKey, Nip46Signer, NotEncrypted, PinEncrypted, PrivateKeySigner } from "@snort/system"; -import { bech32ToHex, getPublicKey, unwrap } from "SnortUtils"; +import { bech32ToHex, getPublicKey, isHex, unwrap } from "SnortUtils"; import ZapButton from "Element/Event/ZapButton"; import useImgProxy from "Hooks/useImgProxy"; import Icon from "Icons/Icon"; import { generateNewLogin, LoginSessionType, LoginStore } from "Login"; import AsyncButton from "Element/AsyncButton"; -import useLoginHandler, { PinRequiredError } from "Hooks/useLoginHandler"; +import useLoginHandler from "Hooks/useLoginHandler"; import { secp256k1 } from "@noble/curves/secp256k1"; import { bytesToHex } from "@noble/curves/abstract/utils"; import Modal from "Element/Modal"; @@ -94,16 +94,20 @@ export default function LoginPage() { setArt({ ...ret, link: url }); }, []); + async function makeKeyStore(key: string, pin?: string) { + if (pin) { + return await PinEncrypted.create(key, pin); + } else { + return new NotEncrypted(key); + } + } + async function doLogin(pin?: string) { setError(""); try { - await loginHandler.doLogin(key, pin); + await loginHandler.doLogin(key, key => makeKeyStore(key, pin)); navigate("/"); } catch (e) { - if (e instanceof PinRequiredError) { - setPin(true); - return; - } if (e instanceof Error) { setError(e.message); } else { @@ -117,9 +121,9 @@ export default function LoginPage() { } } - async function makeRandomKey(pin: string) { + async function makeRandomKey(pin?: string) { try { - await generateNewLogin(pin); + await generateNewLogin(key => makeKeyStore(key, pin)); window.plausible?.("Generate Account"); navigate("/new"); } catch (e) { @@ -153,7 +157,7 @@ export default function LoginPage() { setNip46Key(newKey); } - async function startNip46(pin: string) { + async function startNip46(pin?: string) { if (!nostrConnect || !nip46Key) return; const signer = new Nip46Signer(nostrConnect, new PrivateKeySigner(nip46Key)); @@ -165,7 +169,7 @@ export default function LoginPage() { LoginSessionType.Nip46, undefined, ["wss://relay.damus.io"], - await PinEncrypted.create(nip46Key, pin), + await makeKeyStore(nip46Key, pin), ); navigate("/"); } @@ -316,7 +320,15 @@ export default function LoginPage() { />

- doLogin()}> + { + if (key.startsWith("nsec") || (key.length === 64 && isHex(key))) { + setPin(true); + } else { + await doLogin(); + } + }}> setPin(true)}> @@ -325,9 +337,22 @@ export default function LoginPage() { {pin && ( - -

+ <> +

+ +

+

+ +

+

+ +

+ } onResult={async pin => { setPin(false); @@ -339,7 +364,16 @@ export default function LoginPage() { await makeRandomKey(pin); } }} - onCancel={() => setPin(false)} + onCancel={async () => { + setPin(false); + if (key) { + await doLogin(); + } else if (nostrConnect) { + await startNip46(); + } else { + await makeRandomKey(); + } + }} /> )} {altLogins()} diff --git a/packages/app/src/Pages/new/NewUserFlow.tsx b/packages/app/src/Pages/new/NewUserFlow.tsx index a88e5ffc..e540f8b1 100644 --- a/packages/app/src/Pages/new/NewUserFlow.tsx +++ b/packages/app/src/Pages/new/NewUserFlow.tsx @@ -3,15 +3,13 @@ import { useNavigate } from "react-router-dom"; import Logo from "Element/Logo"; import { CollapsedSection } from "Element/Collapsed"; -import Copy from "Element/Copy"; -import { hexToBech32 } from "SnortUtils"; -import { hexToMnemonic } from "nip6"; import useLogin from "Hooks/useLogin"; import { PROFILE } from "."; import { DefaultPreferences, LoginStore, updatePreferences } from "Login"; import { AllLanguageCodes } from "Pages/settings/Preferences"; import messages from "./messages"; +import ExportKeys from "Pages/settings/Keys"; const WhatIsSnort = () => { return ( @@ -127,14 +125,7 @@ export default function NewUserFlow() {

-

- -

- -

- -

- +