Skip to content

Commit

Permalink
Merge pull request #526 from rithvikvibhu/mnemonic-fix
Browse files Browse the repository at this point in the history
wallet: mnemonic validation and xpriv import
  • Loading branch information
rithvikvibhu committed Jul 5, 2022
2 parents 1185fbe + 0b28920 commit c0618ad
Show file tree
Hide file tree
Showing 19 changed files with 660 additions and 88 deletions.
50 changes: 41 additions & 9 deletions app/background/wallet/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const {Output, MTX, Address, Coin} = require('hsd/lib/primitives');
const Script = require('hsd/lib/script/script');
const MasterKey = require('hsd/lib/wallet/masterkey');
const Mnemonic = require('hsd/lib/hd/mnemonic');
const HDPrivateKey = require('hsd/lib/hd/private');
const Covenant = require('hsd/lib/primitives/covenant');
const common = require('hsd/lib/wallet/common');
const {Rules} = require('hsd/lib/covenants');
Expand Down Expand Up @@ -411,15 +412,35 @@ class WalletService {
return this.node.wdb.deepClean();
};

importSeed = async (name, passphrase, mnemonic) => {
importSeed = async (name, passphrase, type, secret) => {
this.setWallet(name);

const options = {
passphrase,
// hsd generates different keys for
// menmonics with trailing whitespace
mnemonic: mnemonic.trim(),
};
const options = {passphrase};
switch (type) {
case 'phrase':
options.mnemonic = secret.trim();
break;
case 'xpriv':
options.master = secret.trim();
break;
case 'master':
const data = secret.master;
const parsedData = {
encrypted: data.encrypted,
alg: data.algorithm,
iv: Buffer.from(data.iv, 'hex'),
ciphertext: Buffer.from(data.ciphertext, 'hex'),
n: data.n,
r: data.r,
p: data.p,
};
const mk = new MasterKey(parsedData);
options.master = await mk.unlock(secret.passphrase, 10)
assert(options.master, 'Could not decrypt key.')
break;
default:
throw new Error('Invalid type.')
}

const res = await this.node.wdb.create({id: name, ...options});
const wallets = await this.listWallets();
Expand Down Expand Up @@ -554,8 +575,19 @@ class WalletService {
};

const mk = new MasterKey(parsedData);
await mk.unlock(passphrase, 100);
return mk.mnemonic.getPhrase();
await mk.unlock(passphrase, 10);

let phrase;
let phraseMatchesKey = false;
if (mk.mnemonic) {
phrase = mk.mnemonic.getPhrase();
phraseMatchesKey = mk.key.equals(
HDPrivateKey.fromMnemonic(mk.mnemonic)
);
}
const xpriv = mk.key.xprivkey(this.networkName);

return {phrase, xpriv, phraseMatchesKey, master: data};
},
);

Expand Down
9 changes: 7 additions & 2 deletions app/components/Alert/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,21 @@ export default class Alert extends Component {
message: PropTypes.string,
children: PropTypes.node,
style: PropTypes.object,
className: PropTypes.string,
};

static defaultProps = {
className: '',
};

render() {
const { message, type, children, style} = this.props;
const { message, type, children, style, className} = this.props;

if (!message && !children) {
return null;
}

const name = `alert alert--${type}`;
const name = `alert alert--${type} ${className}`;

return (
<div className={name} style={style}>
Expand Down
3 changes: 2 additions & 1 deletion app/components/Modal/MiniModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export class MiniModal extends Component {
onClose: PropTypes.func,
children: PropTypes.node.isRequired,
title: PropTypes.string.isRequired,
className: PropTypes.string,
centered: PropTypes.bool,
wide: PropTypes.bool,
top: PropTypes.bool
Expand All @@ -27,7 +28,7 @@ export class MiniModal extends Component {
};

render() {
const names = classnames('mini-modal', {
const names = classnames('mini-modal', this.props.className, {
'mini-modal--centered': this.props.centered,
'mini-modal--wide': this.props.wide,
'mini-modal--tip': this.props.top
Expand Down
181 changes: 181 additions & 0 deletions app/components/PhraseMismatch/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import fs from 'fs';
import React, { Component } from 'react';
import { connect } from "react-redux";
import PropTypes from 'prop-types';
import c from 'classnames';
import Alert from '../Alert';
import MiniModal from '../Modal/MiniModal';
import { getPassphrase, revealSeed } from "../../ducks/walletActions";
import { I18nContext } from '../../utils/i18n';
import walletClient from "../../utils/walletClient";
import './phrase-mismatch.scss';
const { dialog } = require('@electron/remote');


@connect(
(state) => ({
wid: state.wallet.wid,
phraseMismatch: state.wallet.phraseMismatch,
}),
(dispatch) => ({
getPassphrase: (resolve, reject) => dispatch(getPassphrase(resolve, reject)),
revealSeed: passphrase => dispatch(revealSeed(passphrase))
})
)
export default class PhraseMismatch extends Component {
static propTypes = {
phraseMismatch: PropTypes.bool.isRequired,
getPassphrase: PropTypes.func.isRequired,
};

static contextType = I18nContext;

state = {
isOpen: false,
error: false,
message: '',
};

onClickSaveMasterKey = async () => {
const {wid, getPassphrase, revealSeed} = this.props;

this.setState({
error: false,
message: '',
});

let master;
try {
const passphrase = await new Promise((resolve, reject) => getPassphrase(resolve, reject));
if (!passphrase) return;

const revealSeedRes = await revealSeed(passphrase);
master = revealSeedRes.master;

await walletClient.lock();
} catch (e) {
this.setState({
error: true,
message:
typeof e === 'string' ? e : `An error occurred, please try again: ${e?.message}`
});
return;
}

const data = JSON.stringify(master);
const savePath = dialog.showSaveDialogSync({
defaultPath: `master-key-for-wallet-${wid}.json`,
filters: [{name: 'Master Key backup', extensions: ['json']}],
});

await new Promise((resolve, reject) => {
if (savePath) {
fs.writeFile(savePath, data, (err) => {
if (err) {
reject(err);
} else {
resolve(savePath);
}
});
}
});

this.setState({
error: false,
message: 'Saved to file.',
});
}

render() {
const {isOpen} = this.state;
const {phraseMismatch} = this.props;

// Hide everything if good phrase
if (!phraseMismatch)
return null;

return (
<>
{/* Alert always visible */}
{this.renderAlert()}

{/* Modal only when open */}
{isOpen && this.renderModal()}
</>
)
}

renderAlert() {
const {t} = this.context;

return (
<div onClick={() => this.setState({ isOpen: true })}>
<Alert
type="error"
className="phrase-mismatch-alert"
message={t('phraseMismatchAlert')}
/>
</div>
);
}

renderModal() {
const {wid} = this.props;
const {t} = this.context;

return (
<MiniModal
title="Wallet Phrase Mismatch"
className="phrase-mismatch-modal"
onClose={() => this.setState({ isOpen: false })}
centered
>
<div className="section">
{t('phraseMismatchText1', wid)}
</div>
<div className="section">
<p>{t('howAmIAffected')}</p>
<p>
{t('phraseMismatchText2')}
</p>
<p>
{t('phraseMismatchText3')}
</p>
</div>
<div className="section">
<p>{t('whatCanIDo')}</p>
<p>
{t('phraseMismatchText4')}
</p>
<p>
{t('phraseMismatchText5')}
</p>
</div>

{this.renderMessage()}

<div className="actions">
<button
onClick={() => this.onClickSaveMasterKey()}
>
{t('downloadMasterKey')}
</button>
</div>
</MiniModal>
);
}

renderMessage() {
const {error, message} = this.state;

if (!message) {
return null;
}

return (
<div className={c('message', {'message--error': error})}>
{message}
</div>
);
}
}
59 changes: 59 additions & 0 deletions app/components/PhraseMismatch/phrase-mismatch.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
@import "../../variables.scss";

.phrase-mismatch-alert {
padding: 1rem;
margin-bottom: 1rem;
font-size: 0.9rem;
font-weight: 600;
overflow-wrap: break-word;
background: #f8d7da;
color: #721c24;
cursor: pointer;
border: none;
border-radius: 0;

&:hover {
text-decoration: underline;
}
}

.phrase-mismatch-modal {
.section {
text-align: left;
margin-bottom: 1rem;
font-size: 0.9rem;
line-height: 1.3rem;

& > p {
margin: 0;
margin-block-end: 0.3rem;

&:nth-of-type(1) {
font-weight: 700;
margin-block-start: 1rem;
}
}
}

.message {
font-size: 0.9rem;
width: 350px;
margin: 0 auto 24px auto;
text-align: center;

&--error {
color: $orange-red;
}
}

.actions {
display: flex;
flex-wrap: wrap;
gap: 1rem;

button {
@extend %btn-primary;
flex: 1 0 auto;
}
}
}
19 changes: 19 additions & 0 deletions app/ducks/walletActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
GET_PASSPHRASE,
INCREMENT_IDLE,
LOCK_WALLET,
SET_PHRASE_MISMATCH,
RESET_IDLE,
SET_MAX_IDLE,
SET_PENDING_TRANSACTIONS,
Expand Down Expand Up @@ -151,6 +152,24 @@ export const lockWallet = () => async (dispatch) => {
});
};

export const verifyPhrase = (passphrase) => async (dispatch, getState) => {
const {watchOnly} = getState().wallet;
if (watchOnly) {
dispatch({
type: SET_PHRASE_MISMATCH,
payload: false,
})
return;
};

const {phraseMatchesKey} = await walletClient.revealSeed(passphrase);

dispatch({
type: SET_PHRASE_MISMATCH,
payload: !phraseMatchesKey,
})
}

export const reset = () => async (dispatch, getState) => {
const network = getState().wallet.network;
await walletClient.reset();
Expand Down
Loading

0 comments on commit c0618ad

Please sign in to comment.