From debc9566cce21f5cfb5c06df98aff63346135a5d Mon Sep 17 00:00:00 2001 From: w3irdrobot Date: Fri, 10 Mar 2023 14:02:43 -0500 Subject: [PATCH 1/5] Add nip06 private key generation --- packages/app/package.json | 2 ++ packages/app/src/Const.ts | 5 +++++ packages/app/src/Pages/Login.tsx | 19 +++++++++++++--- packages/app/src/Pages/new/NewUserFlow.tsx | 8 +++++-- packages/app/src/Pages/new/messages.ts | 1 + packages/app/src/State/Login.ts | 19 ++++++++++++---- packages/app/src/Util.ts | 10 +++++++++ packages/app/src/lang.json | 21 +++++++++++++++++ packages/app/src/translations/en.json | 7 ++++++ yarn.lock | 26 ++++++++++++++++++++-- 10 files changed, 107 insertions(+), 11 deletions(-) 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..9ac753e 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 */ diff --git a/packages/app/src/Pages/Login.tsx b/packages/app/src/Pages/Login.tsx index bc2548a..8f303f7 100644 --- a/packages/app/src/Pages/Login.tsx +++ b/packages/app/src/Pages/Login.tsx @@ -5,10 +5,13 @@ 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 } from "Const"; +import { DefaultRelays, EmailRegex, DerivationPath } from "Const"; import { bech32ToHex, unwrap } from "Util"; import { HexKey } from "@snort/nostr"; import ZapButton from "Element/ZapButton"; @@ -111,8 +114,18 @@ export default function LoginPage() { } async function makeRandomKey() { - const newKey = secp.utils.bytesToHex(secp.utils.randomPrivateKey()); - dispatch(setGeneratedPrivateKey(newKey)); + const mn = bip39.generateMnemonic(wordlist); + const ent = bip39.mnemonicToEntropy(mn, wordlist); + 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); + dispatch(setGeneratedPrivateKey({ key: newKeyHex, entropy: entHex })); navigate("/new"); } 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() { +

+ +

+
diff --git a/packages/app/src/State/Login.ts b/packages/app/src/State/Login.ts index 9ebd910..7892c46 100644 --- a/packages/app/src/State/Login.ts +++ b/packages/app/src/State/Login.ts @@ -333,6 +333,9 @@ const LoginSlice = createSlice({ window.localStorage.setItem(PrivateKeyItem, action.payload.key); state.publicKey = secp.utils.bytesToHex(secp.schnorr.getPublicKey(action.payload.key)); }, + clearEntropy: state => { + state.generatedEntropy = undefined; + }, setPublicKey: (state, action: PayloadAction) => { window.localStorage.setItem(PublicKeyItem, action.payload); state.loggedOut = false; @@ -479,6 +482,7 @@ export const { init, setPrivateKey, setGeneratedPrivateKey, + clearEntropy, setPublicKey, setRelays, removeRelay, From ac444ed5628c794b56cb16719f3c279652a42166 Mon Sep 17 00:00:00 2001 From: w3irdrobot Date: Fri, 10 Mar 2023 14:49:54 -0500 Subject: [PATCH 3/5] Enable recovery with a mnemonic seed per nip06 --- packages/app/src/Const.ts | 5 +++++ packages/app/src/Pages/Login.tsx | 31 ++++++++++----------------- packages/app/src/Pages/messages.ts | 2 +- packages/app/src/Util.ts | 27 +++++++++++++++++++++++ packages/app/src/lang.json | 6 +++--- packages/app/src/translations/en.json | 2 +- 6 files changed, 48 insertions(+), 25 deletions(-) diff --git a/packages/app/src/Const.ts b/packages/app/src/Const.ts index 9ac753e..cea0685 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 8f303f7..2690390 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 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/Util.ts b/packages/app/src/Util.ts index 2bdba39..25712b1 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 2112efd..9e9cc2b 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 467ed2b..d93c619 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", From 7d449cedb74bf042add56b958cf978140c9f00a9 Mon Sep 17 00:00:00 2001 From: w3irdrobot Date: Fri, 10 Mar 2023 14:54:25 -0500 Subject: [PATCH 4/5] Check off NIP06 support on the README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 9bb4abb38ae6f331dd3bbafd3065be730290f868 Mon Sep 17 00:00:00 2001 From: w3irdrobot Date: Fri, 10 Mar 2023 15:42:31 -0500 Subject: [PATCH 5/5] Updated generatedEntropy type to string --- packages/app/src/State/Login.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/State/Login.ts b/packages/app/src/State/Login.ts index 7892c46..31a28ae 100644 --- a/packages/app/src/State/Login.ts +++ b/packages/app/src/State/Login.ts @@ -99,9 +99,9 @@ export interface LoginStore { privateKey?: HexKey; /** - * BIP39 generated entropy + * BIP39-generated, hex-encoded entropy */ - generatedEntropy?: HexKey; + generatedEntropy?: string; /** * Current users public key