Enable recovery with a mnemonic seed per nip06

This commit is contained in:
w3irdrobot 2023-03-10 14:49:54 -05:00
parent 710a7dd2de
commit ac444ed562
Signed by: w3irdrobot
GPG Key ID: 3E6DBBB622F3155C
6 changed files with 48 additions and 25 deletions

View File

@ -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
*/

View File

@ -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");
}

View File

@ -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" },
});

View File

@ -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
*/

View File

@ -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"
},

View File

@ -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",