diff --git a/README.md b/README.md index 4ffdb56..60ae83d 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Snort supports the following NIP's: - [ ] NIP-03: OpenTimestamps Attestations for Events - [x] NIP-04: Encrypted Direct Message - [x] NIP-05: Mapping Nostr keys to DNS-based internet identifiers -- [ ] NIP-06: Basic key derivation from mnemonic seed phrase +- [x] NIP-06: Basic key derivation from mnemonic seed phrase - [x] NIP-07: `window.nostr` capability for web browsers - [x] NIP-08: Handling Mentions - [x] NIP-09: Event Deletion diff --git a/packages/app/package.json b/packages/app/package.json index 5c2f67b..2606fa5 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -12,6 +12,8 @@ "@noble/secp256k1": "^1.7.0", "@protobufjs/base64": "^1.1.2", "@reduxjs/toolkit": "^1.9.1", + "@scure/bip32": "^1.1.5", + "@scure/bip39": "^1.1.1", "@snort/nostr": "^1.0.0", "@szhsin/react-menu": "^3.3.1", "base32-decode": "^1.0.0", diff --git a/packages/app/src/Const.ts b/packages/app/src/Const.ts index 738f8fb..cea0685 100644 --- a/packages/app/src/Const.ts +++ b/packages/app/src/Const.ts @@ -85,6 +85,11 @@ export const ZapperSpam = [ "e1ff3bfdd4e40315959b08b4fcc8245eaa514637e1d4ec2ae166b743341be1af", // benthecarman ]; +/** + * NIP06-defined derivation path for private keys + */ +export const DerivationPath = "m/44'/1237'/0'/0/0"; + /** * Regex to match email address */ @@ -92,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 bc2548a..2690390 100644 --- a/packages/app/src/Pages/Login.tsx +++ b/packages/app/src/Pages/Login.tsx @@ -8,8 +8,8 @@ import { useIntl, FormattedMessage } from "react-intl"; import { RootState } from "State/Store"; import { setPrivateKey, setPublicKey, setRelays, setGeneratedPrivateKey } from "State/Login"; -import { DefaultRelays, EmailRegex } 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"; @@ -97,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})`); @@ -111,8 +113,10 @@ export default function LoginPage() { } async function makeRandomKey() { - const newKey = secp.utils.bytesToHex(secp.utils.randomPrivateKey()); - dispatch(setGeneratedPrivateKey(newKey)); + const ent = generateBip39Entropy(); + const entHex = secp.utils.bytesToHex(ent); + 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 cbc12dd..60955b6 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/Pages/new/DiscoverFollows.tsx b/packages/app/src/Pages/new/DiscoverFollows.tsx index 8998998..ad20eeb 100644 --- a/packages/app/src/Pages/new/DiscoverFollows.tsx +++ b/packages/app/src/Pages/new/DiscoverFollows.tsx @@ -1,19 +1,27 @@ import { useIntl, FormattedMessage } from "react-intl"; +import { useDispatch } from "react-redux"; import { useNavigate, Link } from "react-router-dom"; import { RecommendedFollows } from "Const"; import Logo from "Element/Logo"; import FollowListBase from "Element/FollowListBase"; import { useMemo } from "react"; +import { clearEntropy } from "State/Login"; import messages from "./messages"; export default function DiscoverFollows() { const { formatMessage } = useIntl(); + const dispatch = useDispatch(); const navigate = useNavigate(); const sortedReccomends = useMemo(() => { return RecommendedFollows.sort(() => (Math.random() >= 0.5 ? -1 : 1)); }, []); + async function clearEntropyAndGo() { + dispatch(clearEntropy()); + navigate("/"); + } + return (
@@ -27,7 +35,7 @@ export default function DiscoverFollows() { {formatMessage(messages.World)} }} />

-
diff --git a/packages/app/src/Pages/new/NewUserFlow.tsx b/packages/app/src/Pages/new/NewUserFlow.tsx index 96f21ea..6e5b3e8 100644 --- a/packages/app/src/Pages/new/NewUserFlow.tsx +++ b/packages/app/src/Pages/new/NewUserFlow.tsx @@ -6,7 +6,7 @@ import Logo from "Element/Logo"; import { CollapsedSection } from "Element/Collapsed"; import Copy from "Element/Copy"; import { RootState } from "State/Store"; -import { hexToBech32 } from "Util"; +import { hexToBech32, hexToMnemonic } from "Util"; import messages from "./messages"; @@ -68,7 +68,7 @@ const Extensions = () => { }; export default function NewUserFlow() { - const { publicKey, privateKey } = useSelector((s: RootState) => s.login); + const { publicKey, privateKey, generatedEntropy } = useSelector((s: RootState) => s.login); const navigate = useNavigate(); return ( @@ -91,6 +91,10 @@ export default function NewUserFlow() { +

+ +

+