diff --git a/packages/app/src/Const.ts b/packages/app/src/Const.ts index 9ac753e7..cea06851 100644 --- a/packages/app/src/Const.ts +++ b/packages/app/src/Const.ts @@ -97,6 +97,11 @@ export const EmailRegex = // eslint-disable-next-line no-useless-escape /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; +/** + * Regex to match a mnemonic seed + */ +export const MnemonicRegex = /^([^\s]+\s){11}[^\s]+$/; + /** * Extract file extensions regex */ diff --git a/packages/app/src/Pages/Login.tsx b/packages/app/src/Pages/Login.tsx index 8f303f7a..26903901 100644 --- a/packages/app/src/Pages/Login.tsx +++ b/packages/app/src/Pages/Login.tsx @@ -5,14 +5,11 @@ import { useDispatch, useSelector } from "react-redux"; import { useNavigate } from "react-router-dom"; import * as secp from "@noble/secp256k1"; import { useIntl, FormattedMessage } from "react-intl"; -import { HDKey } from "@scure/bip32"; -import { wordlist } from "@scure/bip39/wordlists/english"; -import * as bip39 from "@scure/bip39"; import { RootState } from "State/Store"; import { setPrivateKey, setPublicKey, setRelays, setGeneratedPrivateKey } from "State/Login"; -import { DefaultRelays, EmailRegex, DerivationPath } from "Const"; -import { bech32ToHex, unwrap } from "Util"; +import { DefaultRelays, EmailRegex, MnemonicRegex } from "Const"; +import { bech32ToHex, generateBip39Entropy, entropyToDerivedKey, unwrap } from "Util"; import { HexKey } from "@snort/nostr"; import ZapButton from "Element/ZapButton"; // import useImgProxy from "Feed/ImgProxy"; @@ -100,12 +97,14 @@ export default function LoginPage() { } else if (key.match(EmailRegex)) { const hexKey = await getNip05PubKey(key); dispatch(setPublicKey(hexKey)); + } else if (key.match(MnemonicRegex)) { + const ent = generateBip39Entropy(key); + const keyHex = entropyToDerivedKey(ent); + dispatch(setPrivateKey(keyHex)); + } else if (secp.utils.isValidPrivateKey(key)) { + dispatch(setPrivateKey(key)); } else { - if (secp.utils.isValidPrivateKey(key)) { - dispatch(setPrivateKey(key)); - } else { - throw new Error("INVALID PRIVATE KEY"); - } + throw new Error("INVALID PRIVATE KEY"); } } catch (e) { setError(`Failed to load NIP-05 pub key (${e})`); @@ -114,17 +113,9 @@ export default function LoginPage() { } async function makeRandomKey() { - const mn = bip39.generateMnemonic(wordlist); - const ent = bip39.mnemonicToEntropy(mn, wordlist); + const ent = generateBip39Entropy(); const entHex = secp.utils.bytesToHex(ent); - const masterKey = HDKey.fromMasterSeed(ent); - const newKey = masterKey.derive(DerivationPath); - - if (!newKey.privateKey) { - throw new Error("INVALID PRIVATE KEY DERIVATION"); - } - - const newKeyHex = secp.utils.bytesToHex(newKey.privateKey); + const newKeyHex = entropyToDerivedKey(ent); dispatch(setGeneratedPrivateKey({ key: newKeyHex, entropy: entHex })); navigate("/new"); } diff --git a/packages/app/src/Pages/messages.ts b/packages/app/src/Pages/messages.ts index cbc12dde..60955b6b 100644 --- a/packages/app/src/Pages/messages.ts +++ b/packages/app/src/Pages/messages.ts @@ -46,5 +46,5 @@ export default defineMessages({ }, Bookmarks: { defaultMessage: "Bookmarks" }, BookmarksCount: { defaultMessage: "{n} Bookmarks" }, - KeyPlaceholder: { defaultMessage: "nsec, npub, nip-05, hex" }, + KeyPlaceholder: { defaultMessage: "nsec, npub, nip-05, hex, mnemonic" }, }); diff --git a/packages/app/src/Util.ts b/packages/app/src/Util.ts index 2bdba396..25712b1a 100644 --- a/packages/app/src/Util.ts +++ b/packages/app/src/Util.ts @@ -7,7 +7,9 @@ import base32Decode from "base32-decode"; import { HexKey, TaggedRawEvent, u256, EventKind, encodeTLV, NostrPrefix } from "@snort/nostr"; import * as bip39 from "@scure/bip39"; import { wordlist } from "@scure/bip39/wordlists/english"; +import { HDKey } from "@scure/bip32"; +import { DerivationPath } from "Const"; import { MetadataCache } from "State/Users"; export const sha256 = (str: string) => { @@ -102,6 +104,15 @@ export function hexToBech32(hrp: string, hex?: string) { } } +export function generateBip39Entropy(mnemonic?: string): Uint8Array { + try { + const mn = mnemonic ?? bip39.generateMnemonic(wordlist); + return bip39.mnemonicToEntropy(mn, wordlist); + } catch (e) { + throw new Error("INVALID MNEMONIC PHRASE"); + } +} + /** * Convert hex-encoded entropy into mnemonic phrase */ @@ -110,6 +121,22 @@ export function hexToMnemonic(hex: string): string { return bip39.entropyToMnemonic(bytes, wordlist); } +/** + * Convert mnemonic phrase into hex-encoded private key + * using the derivation path specified in NIP06 + * @param mnemonic the mnemonic-encoded entropy + */ +export function entropyToDerivedKey(entropy: Uint8Array): string { + const masterKey = HDKey.fromMasterSeed(entropy); + const newKey = masterKey.derive(DerivationPath); + + if (!newKey.privateKey) { + throw new Error("INVALID KEY DERIVATION"); + } + + return secp.utils.bytesToHex(newKey.privateKey); +} + /** * Convert hex pubkey to bech32 link url */ diff --git a/packages/app/src/lang.json b/packages/app/src/lang.json index 2112efde..9e9cc2bf 100644 --- a/packages/app/src/lang.json +++ b/packages/app/src/lang.json @@ -197,9 +197,6 @@ "B6+XJy": { "defaultMessage": "zapped" }, - "B6H7eJ": { - "defaultMessage": "nsec, npub, nip-05, hex" - }, "BOUMjw": { "defaultMessage": "No nostr users found for {twitterUsername}" }, @@ -507,6 +504,9 @@ "WxthCV": { "defaultMessage": "e.g. Jack" }, + "X7xU8J": { + "defaultMessage": "nsec, npub, nip-05, hex, mnemonic" + }, "XgWvGA": { "defaultMessage": "Reactions" }, diff --git a/packages/app/src/translations/en.json b/packages/app/src/translations/en.json index 467ed2bf..d93c619d 100644 --- a/packages/app/src/translations/en.json +++ b/packages/app/src/translations/en.json @@ -63,7 +63,6 @@ "AyGauy": "Login", "B4C47Y": "name too short", "B6+XJy": "zapped", - "B6H7eJ": "nsec, npub, nip-05, hex", "BOUMjw": "No nostr users found for {twitterUsername}", "BOr9z/": "Snort is an open source project built by passionate people in their free time", "BcGMo+": "Notes hold text content, the most popular usage of these notes is to store \"tweet like\" messages.", @@ -164,6 +163,7 @@ "W9355R": "Unmute", "WONP5O": "Find your twitter follows on nostr (Data provided by {provider})", "WxthCV": "e.g. Jack", + "X7xU8J": "nsec, npub, nip-05, hex, mnemonic", "XgWvGA": "Reactions", "Y31HTH": "Help fund the development of Snort", "YDURw6": "Service URL",