added seedphrase feature

This commit is contained in:
PhilReact 2024-11-22 20:45:46 +02:00
parent 78e85f9e79
commit ba7d81dbf8
9 changed files with 500 additions and 10 deletions

45
package-lock.json generated
View File

@ -57,6 +57,7 @@
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",
"jssha": "3.3.1", "jssha": "3.3.1",
"lit": "^3.2.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mime": "^4.0.4", "mime": "^4.0.4",
"moment": "^2.30.1", "moment": "^2.30.1",
@ -2523,6 +2524,19 @@
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==" "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
}, },
"node_modules/@lit-labs/ssr-dom-shim": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.1.tgz",
"integrity": "sha512-wx4aBmgeGvFmOKucFKY+8VFJSYZxs9poN3SDNQFF6lT6NrQUnHiPB2PWz2sc4ieEcAaYYzN+1uWahEeTq2aRIQ=="
},
"node_modules/@lit/reactive-element": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.4.tgz",
"integrity": "sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==",
"dependencies": {
"@lit-labs/ssr-dom-shim": "^1.2.0"
}
},
"node_modules/@malept/cross-spawn-promise": { "node_modules/@malept/cross-spawn-promise": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz",
@ -4438,8 +4452,7 @@
"node_modules/@types/trusted-types": { "node_modules/@types/trusted-types": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
"dev": true
}, },
"node_modules/@types/use-sync-external-store": { "node_modules/@types/use-sync-external-store": {
"version": "0.0.6", "version": "0.0.6",
@ -10011,6 +10024,34 @@
"uc.micro": "^2.0.0" "uc.micro": "^2.0.0"
} }
}, },
"node_modules/lit": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/lit/-/lit-3.2.1.tgz",
"integrity": "sha512-1BBa1E/z0O9ye5fZprPtdqnc0BFzxIxTTOO/tQFmyC/hj1O3jL4TfmLBw0WEwjAokdLwpclkvGgDJwTIh0/22w==",
"dependencies": {
"@lit/reactive-element": "^2.0.4",
"lit-element": "^4.1.0",
"lit-html": "^3.2.0"
}
},
"node_modules/lit-element": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.1.1.tgz",
"integrity": "sha512-HO9Tkkh34QkTeUmEdNYhMT8hzLid7YlMlATSi1q4q17HE5d9mrrEHJ/o8O2D0cMi182zK1F3v7x0PWFjrhXFew==",
"dependencies": {
"@lit-labs/ssr-dom-shim": "^1.2.0",
"@lit/reactive-element": "^2.0.4",
"lit-html": "^3.2.0"
}
},
"node_modules/lit-html": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.2.1.tgz",
"integrity": "sha512-qI/3lziaPMSKsrwlxH/xMgikhQ0EGOX2ICU73Bi/YHFvz2j/yMCIrw4+puF2IpQ4+upd3EWbvnHM9+PnJn48YA==",
"dependencies": {
"@types/trusted-types": "^2.0.2"
}
},
"node_modules/load-json-file": { "node_modules/load-json-file": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",

View File

@ -61,6 +61,7 @@
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",
"jssha": "3.3.1", "jssha": "3.3.1",
"lit": "^3.2.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mime": "^4.0.4", "mime": "^4.0.4",
"moment": "^2.30.1", "moment": "^2.30.1",

View File

@ -43,11 +43,13 @@ import Return from "./assets/svgs/Return.svg";
import Success from "./assets/svgs/Success.svg"; import Success from "./assets/svgs/Success.svg";
import Info from "./assets/svgs/Info.svg"; import Info from "./assets/svgs/Info.svg";
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import './utils/seedPhrase/RandomSentenceGenerator';
import { import {
createAccount, createAccount,
generateRandomSentence, generateRandomSentence,
saveFileToDisk, saveFileToDisk,
saveSeedPhraseToDisk,
} from "./utils/generateWallet/generateWallet"; } from "./utils/generateWallet/generateWallet";
import { kdf } from "./deps/kdf"; import { kdf } from "./deps/kdf";
import { generateSaveWalletData } from "./utils/generateWallet/storeWallet"; import { generateSaveWalletData } from "./utils/generateWallet/storeWallet";
@ -123,6 +125,7 @@ import { fileToBase64 } from "./utils/fileReading";
import { handleGetFileFromIndexedDB } from "./utils/indexedDB"; import { handleGetFileFromIndexedDB } from "./utils/indexedDB";
import { CoreSyncStatus } from "./components/CoreSyncStatus"; import { CoreSyncStatus } from "./components/CoreSyncStatus";
import { Wallets } from "./Wallets"; import { Wallets } from "./Wallets";
import { RandomSentenceGenerator } from "./utils/seedPhrase/RandomSentenceGenerator";
type extStates = type extStates =
| "not-authenticated" | "not-authenticated"
@ -210,6 +213,7 @@ const controlAllQueues = (action) => {
}); });
}; };
export const clearAllQueues = () => { export const clearAllQueues = () => {
Object.keys(allQueues).forEach((key) => { Object.keys(allQueues).forEach((key) => {
const val = allQueues[key]; const val = allQueues[key];
@ -384,7 +388,12 @@ function App() {
useRecoilState(enabledDevModeAtom); useRecoilState(enabledDevModeAtom);
const { toggleFullScreen } = useAppFullScreen(setFullScreen); const { toggleFullScreen } = useAppFullScreen(setFullScreen);
const generatorRef = useRef(null)
const exportSeedphrase = ()=> {
console.log('hello', generatorRef.current.parsedString)
const seedPhrase = generatorRef.current.parsedString
saveSeedPhraseToDisk(seedPhrase)
}
useEffect(() => { useEffect(() => {
const isDevModeFromStorage = localStorage.getItem("isEnabledDevMode"); const isDevModeFromStorage = localStorage.getItem("isEnabledDevMode");
if (isDevModeFromStorage) { if (isDevModeFromStorage) {
@ -899,7 +908,7 @@ function App() {
res(); res();
}, 250); }, 250);
}); });
const res = await createAccount(); const res = await createAccount(generatorRef.current.parsedString);
const wallet = await res.generateSaveWalletData( const wallet = await res.generateSaveWalletData(
walletToBeDownloadedPassword, walletToBeDownloadedPassword,
crypto.kdfThreads, crypto.kdfThreads,
@ -2430,6 +2439,40 @@ function App() {
Set up your Qortal account Set up your Qortal account
</TextP> </TextP>
<Spacer height="14px" /> <Spacer height="14px" />
<Box sx={{
display: 'flex',
maxWidth: '100%',
justifyContent: 'center',
padding: '10px'
}}>
<Box sx={{
display: 'flex',
flexDirection: 'column',
maxWidth: '400px',
alignItems: 'center',
gap: '10px'
}}>
<Typography sx={{
fontSize: '14px'
}}>Your seedphrase</Typography>
<Typography sx={{
fontSize: '12px'
}}>Only shown once! Please copy and keep safe!</Typography>
<random-sentence-generator
ref={generatorRef}
template="adverb verb noun adjective noun adverb verb noun adjective noun adjective verbed adjective noun"
></random-sentence-generator>
</Box>
</Box>
<CustomButton sx={{
padding: '7px',
fontSize: '12px'
}} onClick={exportSeedphrase}>
Export Seedphrase
</CustomButton>
<Spacer height="14px" />
<CustomLabel htmlFor="standard-adornment-password"> <CustomLabel htmlFor="standard-adornment-password">
Wallet Password Wallet Password
</CustomLabel> </CustomLabel>

View File

@ -6,13 +6,19 @@ import ListItemText from "@mui/material/ListItemText";
import ListItemAvatar from "@mui/material/ListItemAvatar"; import ListItemAvatar from "@mui/material/ListItemAvatar";
import Avatar from "@mui/material/Avatar"; import Avatar from "@mui/material/Avatar";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { Box, Button, ButtonBase, IconButton, Input } from "@mui/material"; import { Box, Button, ButtonBase, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Input } from "@mui/material";
import { CustomButton } from "./App-styles"; import { CustomButton } from "./App-styles";
import { useDropzone } from "react-dropzone"; import { useDropzone } from "react-dropzone";
import EditIcon from "@mui/icons-material/Edit"; import EditIcon from "@mui/icons-material/Edit";
import { Label } from "./components/Group/AddGroup"; import { Label } from "./components/Group/AddGroup";
import { Spacer } from "./common/Spacer"; import { Spacer } from "./common/Spacer";
import { getWallets, storeWallets } from "./background"; import { getWallets, storeWallets, walletVersion } from "./background";
import { useModal } from "./common/useModal";
import PhraseWallet from "./utils/generateWallet/phrase-wallet";
import { decryptStoredWalletFromSeedPhrase } from "./utils/decryptWallet";
import { crypto } from "./constants/decryptWallet";
import { LoadingButton } from "@mui/lab";
import { PasswordField } from "./components";
const parsefilenameQortal = (filename)=> { const parsefilenameQortal = (filename)=> {
return filename.startsWith("qortal_backup_") ? filename.slice(14) : filename; return filename.startsWith("qortal_backup_") ? filename.slice(14) : filename;
@ -21,6 +27,15 @@ const parsefilenameQortal = (filename)=> {
export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => { export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => {
const [wallets, setWallets] = useState([]); const [wallets, setWallets] = useState([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [seedValue, setSeedValue] = useState("");
const [seedName, setSeedName] = useState("");
const [seedError, setSeedError] = useState("");
const [password, setPassword] = useState("");
const [isOpenSeedModal, setIsOpenSeedModal] = useState(false);
const [isLoadingEncryptSeed, setIsLoadingEncryptSeed] = useState(false);
const { isShow, onCancel, onOk, show, } = useModal();
const { getRootProps, getInputProps } = useDropzone({ const { getRootProps, getInputProps } = useDropzone({
accept: { accept: {
@ -81,7 +96,6 @@ export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => {
setWallets((prev) => { setWallets((prev) => {
let copyPrev = [...prev]; let copyPrev = [...prev];
if (wallet === null) { if (wallet === null) {
console.log("entered");
copyPrev.splice(idx, 1); // Use splice to remove the item copyPrev.splice(idx, 1); // Use splice to remove the item
return copyPrev; return copyPrev;
} else { } else {
@ -91,6 +105,42 @@ export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => {
}); });
}; };
const handleSetSeedValue = async ()=> {
try {
setIsOpenSeedModal(true)
const {seedValue, seedName, password} = await show({
message: "",
publishFee: "",
});
setIsLoadingEncryptSeed(true)
const res = await decryptStoredWalletFromSeedPhrase(seedValue)
const wallet2 = new PhraseWallet(res, walletVersion);
const wallet = await wallet2.generateSaveWalletData(
password,
crypto.kdfThreads,
() => {}
);
if(wallet?.address0){
setWallets([...wallets, {
...wallet,
name: seedName
}]);
setIsOpenSeedModal(false)
setSeedValue('')
setSeedName('')
setPassword('')
setSeedError('')
} else {
setSeedError('Could not create wallet.')
}
} catch (error) {
setSeedError(error?.message || 'Could not create wallet.')
} finally {
setIsLoadingEncryptSeed(false)
}
}
const selectedWalletFunc = (wallet) => { const selectedWalletFunc = (wallet) => {
setRawWallet(wallet); setRawWallet(wallet);
setExtState("wallet-dropped"); setExtState("wallet-dropped");
@ -179,12 +229,98 @@ export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => {
right: '20px' right: '20px'
}} }}
> >
<CustomButton {...getRootProps()}> <CustomButton onClick={handleSetSeedValue} sx={{
padding: '10px'
}} >
Add seed-phrase
</CustomButton>
<CustomButton sx={{
padding: '10px'
}} {...getRootProps()}>
<input {...getInputProps()} /> <input {...getInputProps()} />
Add wallets Add wallets
</CustomButton> </CustomButton>
</Box> </Box>
<Dialog
open={isOpenSeedModal}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
onKeyDown={(e) => {
if (e.key === 'Enter' && seedValue && seedName && password) {
onOk({seedValue, seedName, password});
}
}}
>
<DialogTitle id="alert-dialog-title">
Type or paste in your seed-phrase
</DialogTitle>
<DialogContent>
<Box
sx={{
display: "flex",
flexDirection: "column",
}}
>
<Label>Name</Label>
<Input
placeholder="Name"
value={seedName}
onChange={(e) => setSeedName(e.target.value)}
/>
<Spacer height="7px" />
<Label>Seed-phrase</Label>
<Input
placeholder="Seed-phrase"
value={seedValue}
onChange={(e) => setSeedValue(e.target.value)}
/>
<Spacer height="7px" />
<Label>Choose new password</Label>
<PasswordField
id="standard-adornment-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
autoComplete="off"
/>
</Box>
</DialogContent>
<DialogActions>
<Button disabled={isLoadingEncryptSeed} variant="contained" onClick={()=> {
setIsOpenSeedModal(false)
setSeedValue('')
setSeedName('')
setPassword('')
setSeedError('')
}}>
Close
</Button>
<LoadingButton
loading={isLoadingEncryptSeed}
disabled={!seedValue || !seedName || !password}
variant="contained"
onClick={() => {
if(!seedValue || !seedName || !password) return
onOk({seedValue, seedName, password});
}}
autoFocus
>
Add
</LoadingButton>
<Typography sx={{
fontSize: '14px',
visibility: seedError ? 'visible' : 'hidden'
}}>{seedError}</Typography>
</DialogActions>
</Dialog>
</div> </div>
); );
}; };
@ -338,6 +474,9 @@ const WalletItem = ({ wallet, updateWalletItem, idx, setSelectedWallet }) => {
</Box> </Box>
</Box> </Box>
)} )}
</> </>
); );
}; };

View File

@ -21,4 +21,14 @@ export const decryptStoredWallet = async (password, wallet) => {
} }
const decryptedBytes = AES_CBC.decrypt(encryptedSeedBytes, encryptionKey, false, iv) const decryptedBytes = AES_CBC.decrypt(encryptedSeedBytes, encryptionKey, false, iv)
return decryptedBytes return decryptedBytes
}
export const decryptStoredWalletFromSeedPhrase = async (password) => {
console.log('p')
const threads = doInitWorkers(crypto.kdfThreads)
const salt = new Uint8Array(void 0)
const seed = await kdf(password, salt, threads)
return seed
} }

View File

@ -75,8 +75,8 @@ export function generateRandomSentence(template = 'adverb verb noun adjective no
return parse(template); return parse(template);
} }
export const createAccount = async()=> { export const createAccount = async(generatedSeedPhrase)=> {
const generatedSeedPhrase = generateRandomSentence() if(!generatedSeedPhrase) throw new Error('No generated seed-phrase')
const threads = doInitWorkers(crypto.kdfThreads) const threads = doInitWorkers(crypto.kdfThreads)
const seed = await kdf(generatedSeedPhrase, void 0, threads) const seed = await kdf(generatedSeedPhrase, void 0, threads)
@ -93,4 +93,13 @@ export const createAccount = async()=> {
await FileSaver.saveAs(blob, fileName); await FileSaver.saveAs(blob, fileName);
}
export const saveSeedPhraseToDisk = async (data) => {
const blob = new Blob([data], { type: 'text/plain;charset=utf-8' })
const fileName = "qortal_seedphrase.txt"
await FileSaver.saveAs(blob, fileName);
} }

View File

@ -0,0 +1,175 @@
// Author: irontiga <irontiga@gmail.com>
import { html, LitElement, css } from 'lit'
import * as WORDLISTS from './wordList'
class RandomSentenceGenerator extends LitElement {
static get properties() {
return {
template: { type: String, attribute: 'template' },
parsedString: { type: String },
fetchedWordlistCount: { type: Number, value: 0 },
capitalize: { type: Boolean },
partsOfSpeechMap: { type: Object },
templateEntropy: { type: Number, reflect: true, attribute: 'template-entropy' },
maxWordLength: { type: Number, attribute: 'max-word-length' }
}
}
constructor() {
super()
this.template = 'adjective noun verb adverb.'
this.maxWordLength = 0
this.parsedString = ''
this.fetchedWordlistCount = 0
this.capitalize = true
this.partsOfSpeechMap = {
'noun': 'nouns',
'adverb': 'adverbs',
'adv': 'adverbs',
'verb': 'verbs',
'interjection': 'interjections',
'adjective': 'adjectives',
'adj': 'adjectives',
'verbed': 'verbed'
}
this.partsOfSpeech = Object.keys(this.partsOfSpeechMap)
this._wordlists = WORDLISTS
}
static styles = css`
div {
text-align: center;
width: 100%;
background-color: #1f2023;
border-radius: 5px;
padding: 10px;
}
`;
render() {
return html`
<div>${this.parsedString}</div>
`
}
firstUpdated() {
console.log('this.template', this.template)
// ...
}
updated(changedProperties) {
let regen = false
if (changedProperties.has('template')) {
regen = true
}
if (changedProperties.has('maxWordLength')) {
console.dir(this.maxWordLength)
if (this.maxWordLength) {
const wl = { ...this._wordlists }
for (const partOfSpeech in this._wordlists) {
console.log(this._wordlists[partOfSpeech])
if (Array.isArray(this._wordlists[partOfSpeech])) {
wl[partOfSpeech] = this._wordlists[partOfSpeech].filter(word => word.length <= this.maxWordLength)
}
}
this._wordlists = wl
}
regen = true
}
if (regen) this.generate()
}
_RNG(entropy) {
if (entropy > 1074) {
throw new Error('Javascript can not handle that much entropy!')
}
let randNum = 0
const crypto = window.crypto || window.msCrypto
if (crypto) {
const entropy256 = Math.ceil(entropy / 8)
let buffer = new Uint8Array(entropy256)
crypto.getRandomValues(buffer)
randNum = buffer.reduce((num, value) => {
return num * value
}, 1) / Math.pow(256, entropy256)
} else {
console.warn('Secure RNG not found. Using Math.random')
randNum = Math.random()
}
return randNum
}
setRNG(fn) {
this._RNG = fn
}
_captitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
getWord(partOfSpeech) {
const words = this._wordlists[this.partsOfSpeechMap[partOfSpeech]]
const requiredEntropy = Math.log(words.length) / Math.log(2)
const index = this._RNG(requiredEntropy) * words.length
return {
word: words[Math.round(index)],
entropy: words.length
}
}
generate() {
this.parsedString = this.parse(this.template)
}
parse(template) {
const split = template.split(/[\s]/g)
let entropy = 1
const final = split.map(word => {
const lower = word.toLowerCase()
this.partsOfSpeech.some(partOfSpeech => {
const partOfSpeechIndex = lower.indexOf(partOfSpeech) // Check it exists
const nextChar = word.charAt(partOfSpeech.length)
if (partOfSpeechIndex === 0 && !(nextChar && (nextChar.match(/[a-zA-Z]/g) != null))) {
const replacement = this.getWord(partOfSpeech)
word = replacement.word + word.slice(partOfSpeech.length) // Append the rest of the "word" (punctuation)
entropy = entropy * replacement.entropy
return true
}
})
return word
})
this.templateEntropy = Math.floor(Math.log(entropy) / Math.log(8))
return final.join(' ')
}
}
window.customElements.define('random-sentence-generator', RandomSentenceGenerator)
export default RandomSentenceGenerator

View File

@ -0,0 +1,40 @@
export const EXCEPTIONS = {
'are': 'were',
'eat': 'ate',
'go': 'went',
'have': 'had',
'inherit': 'inherited',
'is': 'was',
'run': 'ran',
'sit': 'sat',
'visit': 'visited'
}
export const getPastTense = (verb, exceptions = EXCEPTIONS) => {
if (exceptions[verb]) {
return exceptions[verb]
}
if ((/e$/i).test(verb)) {
return verb + 'd'
}
if ((/[aeiou]c$/i).test(verb)) {
return verb + 'ked'
}
// for american english only
if ((/el$/i).test(verb)) {
return verb + 'ed'
}
if ((/[aeio][aeiou][dlmnprst]$/).test(verb)) {
return verb + 'ed'
}
if ((/[aeiou][bdglmnprst]$/i).test(verb)) {
return verb.replace(/(.+[aeiou])([bdglmnprst])/, '$1$2$2ed')
}
return verb + 'ed'
}

File diff suppressed because one or more lines are too long