From b50c02a2ff6d3aac92ef03c065cb1e86c3033eac Mon Sep 17 00:00:00 2001 From: tobtoht Date: Thu, 14 Sep 2023 11:44:10 +0200 Subject: [PATCH] Seed Recovery Dialog --- src/dialog/SeedRecoveryDialog.cpp | 269 +++++++++++++++++ src/dialog/SeedRecoveryDialog.h | 51 ++++ src/dialog/SeedRecoveryDialog.ui | 436 +++++++++++++++++++++++++++ src/wizard/PageWalletRestoreSeed.cpp | 8 + 4 files changed, 764 insertions(+) create mode 100644 src/dialog/SeedRecoveryDialog.cpp create mode 100644 src/dialog/SeedRecoveryDialog.h create mode 100644 src/dialog/SeedRecoveryDialog.ui diff --git a/src/dialog/SeedRecoveryDialog.cpp b/src/dialog/SeedRecoveryDialog.cpp new file mode 100644 index 0000000..7ad2d4b --- /dev/null +++ b/src/dialog/SeedRecoveryDialog.cpp @@ -0,0 +1,269 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#include "SeedRecoveryDialog.h" +#include "ui_SeedRecoveryDialog.h" + +#include +#include "ColorScheme.h" +#include "utils/Utils.h" +#include "polyseed/polyseed.h" +#include "utils/AsyncTask.h" +#include "device/device_default.hpp" +#include "cryptonote_basic/account.h" +#include "cryptonote_basic/cryptonote_basic_impl.h" + +SeedRecoveryDialog::SeedRecoveryDialog(QWidget *parent) + : WindowModalDialog(parent) + , m_scheduler(this) + , m_watcher(this) + , ui(new Ui::SeedRecoveryDialog) +{ + ui->setupUi(this); + + for (int i = 0; i != 2048; i++) { + m_wordList << QString::fromStdString(wordlist::english.get_word(i)); + } + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false); + ui->buttonBox->button(QDialogButtonBox::Apply)->setText("Check"); + + connect(this, &SeedRecoveryDialog::progressUpdated, this, &SeedRecoveryDialog::onProgressUpdated); + + disconnect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + connect(ui->buttonBox->button(QDialogButtonBox::Apply), &QPushButton::clicked, this, &SeedRecoveryDialog::checkSeed); + connect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, [this]{ + m_cancelled = true; + }); + connect(ui->buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, [this]{ + m_cancelled = true; + m_watcher.waitForFinished(); + this->close(); + }); + + connect(this, &SeedRecoveryDialog::searchFinished, this, &SeedRecoveryDialog::onFinished); + connect(this, &SeedRecoveryDialog::matchFound, this, &SeedRecoveryDialog::onMatchFound); + connect(this, &SeedRecoveryDialog::addressMatchFound, this, &SeedRecoveryDialog::onAddressMatchFound); + + this->adjustSize(); +} + +void SeedRecoveryDialog::onMatchFound(const QString &match) { + ui->potentialSeeds->appendPlainText(match); +} + +void SeedRecoveryDialog::onAddressMatchFound(const QString &match) { + ui->potentialSeeds->appendPlainText(QString("\nFound seed containing address:\n%1").arg(match)); +} + +void SeedRecoveryDialog::onFinished(bool cancelled) { + if (!cancelled) { + ui->progressBar->setMaximum(100); + ui->progressBar->setValue(100); + } + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false); + ui->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true); +} + +QStringList SeedRecoveryDialog::wordsWithRegex(const QRegularExpression ®ex) { + return m_wordList.filter(regex); +} + +bool SeedRecoveryDialog::findNext(const QList &words, QList &index) { + if (words.length() != index.length()) { + return false; + } + + if (words.empty()) { + return false; + } + + for (int i = words.length() - 1; i >= 0; i--) { + if ((words[i].length() - 1) > index[i]) { + index[i] += 1; + for (int j = i + 1; j < words.length(); j++) { + index[j] = 0; + } + return true; + } + } + + return false; +} + +QString SeedRecoveryDialog::mnemonic(const QList &words, const QList &index) { + if (words.length() != index.length()) { + return QString(); + } + + QStringList mnemonic; + for (int i = 0; i < words.length(); i++) { + mnemonic.push_back(words[i][index[i]]); + } + + return mnemonic.join(" "); +} + +bool SeedRecoveryDialog::isAlpha(const QString &word) { + for (const QChar &ch : word) { + if (!ch.isLetter()) { + return false; + } + } + return true; +} + +void SeedRecoveryDialog::onProgressUpdated(int value) { + ui->progressBar->setValue(value); +} + +void SeedRecoveryDialog::checkSeed() { + m_cancelled = false; + + ui->progressBar->setValue(0); + ui->potentialSeeds->clear(); + + ui->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); + ui->buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(true); + + // Check address + QString address = ui->line_address->text(); + crypto::public_key spkey = crypto::null_pkey; + + if (!address.isEmpty()) { + cryptonote::address_parse_info info; + bool addressValid = cryptonote::get_account_address_from_str(info, cryptonote::network_type::MAINNET, address.toStdString()); + if (!addressValid) { + Utils::showError(this, "Invalid address entered"); + this->onFinished(false); + return; + } + spkey = info.address.m_spend_public_key; + } + + QList lineEdits = ui->group_seed->findChildren(); + std::sort(lineEdits.begin(), lineEdits.end(), [](QLineEdit* a, QLineEdit* b) { + return a->objectName() < b->objectName(); + }); + + QList words; + uint64_t combinations = 1; + + for (QLineEdit *lineEdit : lineEdits) { + lineEdit->setStyleSheet(""); + } + + for (QLineEdit *lineEdit : lineEdits) { + ColorScheme::updateFromWidget(this); + QString word = lineEdit->text(); + + QString wordRe = word; + if (this->isAlpha(word)) { + wordRe = QString("^%1").arg(wordRe); + } + QRegularExpression regex{wordRe}; + + if (!regex.isValid()) { + lineEdit->setStyleSheet(ColorScheme::RED.asStylesheet(true)); + Utils::showError(this, "Invalid regex entered", QString("'%1' is not a valid regular expression").arg(wordRe)); + this->onFinished(false); + return; + } + + QStringList possibleWords = wordsWithRegex(regex); + int numWords = possibleWords.length(); + + if (numWords == 1) { + lineEdit->setStyleSheet(ColorScheme::GREEN.asStylesheet(true)); + } + else if (numWords == 0) { + lineEdit->setStyleSheet(ColorScheme::RED.asStylesheet(true)); + Utils::showError(this, "Word is not in wordlist", QString("No words found for: '%1'").arg(word)); + this->onFinished(false); + return; + } else { + lineEdit->setStyleSheet(ColorScheme::YELLOW.asStylesheet(true)); + ui->potentialSeeds->appendPlainText(QString("Possible words for '%1': %2").arg(word, possibleWords.join(", "))); + + if (combinations < std::numeric_limits::max() / numWords) { + combinations *= possibleWords.length(); + } else { + Utils::showError(this, "Too many possible seeds", "Recovery infeasible"); + this->onFinished(false); + return; + } + } + + words << possibleWords; + } + + if (spkey == crypto::null_pkey) { + ui->potentialSeeds->appendPlainText("\nPossible seeds:"); + } + + qDebug() << "Number of possible combinations: " << combinations; + + ui->progressBar->setMaximum(combinations / 1000); + + uint32_t major = ui->line_majorLookahead->text().toInt(); + uint32_t minor = ui->line_minorLookahead->text().toInt(); + + // Single threaded for now + const auto future = m_scheduler.run([this, words, spkey, major, minor]{ + QList index(16, 0); + + qint64 i = 0; + + do { + if (m_cancelled) { + emit searchFinished(true); + return; + } + + if (++i % 1000 == 0) { + emit progressUpdated(i / 1000); + } + + QString seedString = mnemonic(words, index); + + crypto::secret_key key; + try { + polyseed::data seed(POLYSEED_MONERO); + seed.decode(seedString.toStdString().c_str()); + seed.keygen(&key.data, sizeof(key.data)); + } + catch (const polyseed::error& ex) { + continue; + } + + // Handle case where we don't know an address + if (spkey == crypto::null_pkey) { + emit matchFound(seedString); + continue; + } + + cryptonote::account_base base; + base.generate(key, true, false); + + hw::device &hwdev = base.get_device(); + + for (int x = 0; x < major; x++) { + const std::vector pkeys = hwdev.get_subaddress_spend_public_keys(base.get_keys(), x, 0, minor); + for (const auto &k : pkeys) { + if (k == spkey) { + emit addressMatchFound(seedString); + emit searchFinished(false); + return; + } + } + } + } while (findNext(words, index)); + + emit searchFinished(false); + }); + + m_watcher.setFuture(future.second); +} + +SeedRecoveryDialog::~SeedRecoveryDialog() = default; \ No newline at end of file diff --git a/src/dialog/SeedRecoveryDialog.h b/src/dialog/SeedRecoveryDialog.h new file mode 100644 index 0000000..e073dcb --- /dev/null +++ b/src/dialog/SeedRecoveryDialog.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#ifndef FEATHER_SEEDRECOVERYDIALOG_H +#define FEATHER_SEEDRECOVERYDIALOG_H + +#include + +#include "components.h" +#include "utils/scheduler.h" + +namespace Ui { + class SeedRecoveryDialog; +} + +class SeedRecoveryDialog : public WindowModalDialog +{ +Q_OBJECT + +public: + explicit SeedRecoveryDialog(QWidget *parent = nullptr); + ~SeedRecoveryDialog() override; + +signals: + void progressUpdated(int value); + void searchFinished(bool cancelled); + void matchFound(QString match); + void addressMatchFound(QString match); + +private slots: + void onFinished(bool cancelled); + void onMatchFound(const QString &match); + void onAddressMatchFound(const QString &match); + void onProgressUpdated(int value); + +private: + void checkSeed(); + QStringList wordsWithRegex(const QRegularExpression ®ex); + bool isAlpha(const QString &word); + bool findNext(const QList &words, QList &index); + QString mnemonic(const QList &words, const QList &index); + + std::atomic m_cancelled = false; + + QStringList m_wordList; + QFutureWatcher m_watcher; + FutureScheduler m_scheduler; + QScopedPointer ui; +}; + +#endif //FEATHER_SEEDRECOVERYDIALOG_H diff --git a/src/dialog/SeedRecoveryDialog.ui b/src/dialog/SeedRecoveryDialog.ui new file mode 100644 index 0000000..dcb679e --- /dev/null +++ b/src/dialog/SeedRecoveryDialog.ui @@ -0,0 +1,436 @@ + + + SeedRecoveryDialog + + + + 0 + 0 + 570 + 632 + + + + Seed Recovery + + + + + + <html><head/><body><p>This tool allows you to recover partial Polyseeds.</p><p>Enter every seed word you know. If you know a word partially, fill in the part you know. If you don't know a word at all, leave it blank.</p><p>Regex is supported. Entries containing no special characters are assumed to be prefixes.</p></body></html> + + + true + + + + + + + + + + + + + + + + + + + + 14. + + + + + + + 12. + + + + + + + 1. + + + + + + + 16. + + + + + + + + + + + + + + 13. + + + + + + + 8. + + + + + + + 5. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2. + + + + + + + 10. + + + + + + + + + + + + + + 15. + + + + + + + + + + + + + + + + + + + + + 11. + + + + + + + + + + + + + + 9. + + + + + + + + + + + + + + 6. + + + + + + + + + + + + + + 7. + + + + + + + + + + + + + + 4. + + + + + + + + + + + + + + + + + + + + + 3. + + + + + + + + + + + + + + + + + Enter any deposit address associated with the wallet (optional) + + + + + + + QFormLayout::ExpandingFieldsGrow + + + + + Address + + + + + + + + + + Account lookahead + + + + + + + 50 + + + 50 + + + + + + + Address lookahead + + + + + + + 200 + + + 200 + + + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Progress + + + + + + 0 + + + + + + + + 0 + 100 + + + + true + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Close + + + + + + + word_01 + word_02 + word_03 + word_04 + word_05 + word_06 + word_07 + word_08 + word_09 + word_10 + word_11 + word_12 + word_13 + word_14 + word_15 + word_16 + + + + + buttonBox + accepted() + SeedRecoveryDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + SeedRecoveryDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/wizard/PageWalletRestoreSeed.cpp b/src/wizard/PageWalletRestoreSeed.cpp index d9d26fd..2285edd 100644 --- a/src/wizard/PageWalletRestoreSeed.cpp +++ b/src/wizard/PageWalletRestoreSeed.cpp @@ -9,7 +9,9 @@ #include #include #include +#include +#include "dialog/SeedRecoveryDialog.h" #include // tevador 14 word #include "utils/Seed.h" #include "constants.h" @@ -57,6 +59,12 @@ PageWalletRestoreSeed::PageWalletRestoreSeed(WizardFields *fields, QWidget *pare ui->seedEdit->setAcceptRichText(false); ui->seedEdit->setMaximumHeight(150); + QShortcut *shortcut = new QShortcut(QKeySequence("Ctrl+K"), this); + QObject::connect(shortcut, &QShortcut::activated, [&](){ + SeedRecoveryDialog dialog{this}; + dialog.exec(); + }); + connect(ui->seedBtnGroup, QOverload::of(&QButtonGroup::buttonClicked), this, &PageWalletRestoreSeed::onSeedTypeToggled); connect(ui->combo_seedLanguage, &QComboBox::currentTextChanged, this, &PageWalletRestoreSeed::onSeedLanguageChanged); connect(ui->btnOptions, &QPushButton::clicked, this, &PageWalletRestoreSeed::onOptionsClicked);