From e20186f428cd6cc5f0187116cedaf5f80f2e40d0 Mon Sep 17 00:00:00 2001 From: tobtoht Date: Wed, 6 Mar 2024 13:16:34 +0100 Subject: [PATCH] wizard: legacy seed recovery --- src/dialog/LegacySeedRecovery.cpp | 286 +++++++++++++++++++++++++++ src/dialog/LegacySeedRecovery.h | 64 ++++++ src/dialog/LegacySeedRecovery.ui | 156 +++++++++++++++ src/wizard/PageWalletRestoreSeed.cpp | 11 +- 4 files changed, 515 insertions(+), 2 deletions(-) create mode 100644 src/dialog/LegacySeedRecovery.cpp create mode 100644 src/dialog/LegacySeedRecovery.h create mode 100644 src/dialog/LegacySeedRecovery.ui diff --git a/src/dialog/LegacySeedRecovery.cpp b/src/dialog/LegacySeedRecovery.cpp new file mode 100644 index 0000000..94a9d22 --- /dev/null +++ b/src/dialog/LegacySeedRecovery.cpp @@ -0,0 +1,286 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#include "LegacySeedRecovery.h" +#include "ui_LegacySeedRecovery.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" +#include "cryptonote_basic/blobdatatype.h" +#include "common/base58.h" +#include "serialization/binary_utils.h" + +LegacySeedRecovery::LegacySeedRecovery(QWidget *parent) + : WindowModalDialog(parent) + , m_scheduler(this) + , m_watcher(this) + , ui(new Ui::LegacySeedRecovery) +{ + ui->setupUi(this); + + std::vector wordlists = crypto::ElectrumWords::get_language_list(); + for (const auto& wordlist: wordlists) { + QStringList words_qt; + std::vector words_std = wordlist->get_word_list(); + for (const auto& word: words_std) { + words_qt += QString::fromStdString(word); + } + + QString language = QString::fromStdString(wordlist->get_english_language_name()); + ui->combo_seedLanguage->addItem(language); + m_wordLists[language] = words_qt; + } + + ui->combo_seedLanguage->setCurrentIndex(1); + + ui->buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false); + ui->buttonBox->button(QDialogButtonBox::Apply)->setText("Check"); + + disconnect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + connect(ui->buttonBox->button(QDialogButtonBox::Apply), &QPushButton::clicked, this, &LegacySeedRecovery::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, &LegacySeedRecovery::progressUpdated, this, &LegacySeedRecovery::onProgressUpdated); + + connect(this, &LegacySeedRecovery::searchFinished, this, &LegacySeedRecovery::onFinished); + connect(this, &LegacySeedRecovery::matchFound, this, &LegacySeedRecovery::onMatchFound); + connect(this, &LegacySeedRecovery::addressMatchFound, this, &LegacySeedRecovery::onAddressMatchFound); + connect(this, &LegacySeedRecovery::addResultText, this, &LegacySeedRecovery::onAddResultText); + + this->adjustSize(); +} + +void LegacySeedRecovery::onMatchFound(const QString &match) { + ui->results->appendPlainText(match); +} + +void LegacySeedRecovery::onAddressMatchFound(const QString &match) { + ui->results->appendPlainText(QString("Found seed containing address:\n%1").arg(match)); +} + +void LegacySeedRecovery::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); +} + +void LegacySeedRecovery::onProgressUpdated(int value) { + ui->progressBar->setValue(value); +} + +void LegacySeedRecovery::onAddResultText(const QString &text) { + ui->results->appendPlainText(text); +} + +bool LegacySeedRecovery::testSeed(const QString &seed, const crypto::public_key &spkey) { + std::string mnemonic = seed.toStdString(); + + crypto::secret_key k; + std::string lang; + bool r = crypto::ElectrumWords::words_to_bytes(mnemonic, k, lang); + + if (!r) { + return false; + } + + if (spkey == crypto::null_pkey) { + emit matchFound(seed); + return false; + } + + + cryptonote::account_base base; + base.generate(k, true, false); + + hw::device &hwdev = base.get_device(); + + for (int x = 0; x < m_major; x++) { + const std::vector pkeys = hwdev.get_subaddress_spend_public_keys(base.get_keys(), x, 0, m_minor); + for (const auto &k : pkeys) { + if (k == spkey) { + emit addressMatchFound(seed); + emit searchFinished(false); + return true; + } + } + } + + return false; +} + +void LegacySeedRecovery::checkSeed() { + m_cancelled = false; + + ui->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); + ui->buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(true); + + ui->results->clear(); + ui->progressBar->setMaximum(39024); + ui->progressBar->setValue(0); + + QStringList words = ui->seed->toPlainText().replace("\n", " ").replace("\r", "").trimmed().split(" ", Qt::SkipEmptyParts); + if (words.length() < 24) { + Utils::showError(this, "Invalid seed", "Less than 24 words were entered", {"Remember to use a single space between each word."}); + return; + } + if (words.length() > 25) { + Utils::showError(this, "Invalid seed", "More than 25 words were entered", {"Remember to use a single space between each word."}); + return; + } + + Mode mode = words.length() == 25 ? Mode::WORD_25 : Mode::WORD_24; + + QString address = ui->line_depositAddress->text(); + crypto::public_key spkey = crypto::null_pkey; + + if (!address.isEmpty()) { + cryptonote::blobdata data; + uint64_t prefix; + if (!tools::base58::decode_addr(address.toStdString(), prefix, data)) + { + Utils::showError(this, "Unable to decode address"); + this->onFinished(false); + return; + } + + cryptonote::account_public_address a; + if (!::serialization::parse_binary(data, a)) + { + Utils::showError(this, "Account public address keys can't be parsed"); + this->onFinished(false); + return; + } + + if (!crypto::check_key(a.m_spend_public_key) || !crypto::check_key(a.m_view_public_key)) + { + Utils::showError(this, "Failed to validate address keys"); + this->onFinished(false); + return; + } + + spkey = a.m_spend_public_key; + } + + if (spkey == crypto::null_pkey) { + ui->results->appendPlainText("\nPossible seeds:"); + } + + m_major = ui->line_majorLookahead->text().toInt(); + m_minor = ui->line_minorLookahead->text().toInt(); + + QString language = ui->combo_seedLanguage->currentText(); + if (!m_wordLists.contains(language)) { + Utils::showError(this, "Unable to start recovery tool", QString("No wordlist for language: %1").arg(language)); + return; + } + + ui->results->appendPlainText(QString("%1 words entered\n").arg(QString::number(words.length()))); + + // Single threaded for now + const auto future = m_scheduler.run([this, words, spkey, mode, language]{ + + if (mode == Mode::WORD_25) { + emit addResultText("Strategy [1/2]: swap adjacent words\n"); + + ui->progressBar->setValue(0); + ui->progressBar->setMaximum(24); + + for (int i = 0; i < 24; i++) { + QStringList seed = words; + seed.swapItemsAt(i, i+1); + + QString m = seed.join(" "); + bool done = this->testSeed(m, spkey); + if (done) { + return; + } + + // Swap back + seed.swapItemsAt(i, i+1); + } + + if (m_cancelled) { + emit searchFinished(true); + return; + } + + emit addResultText("Strategy [2/2]: one word is incorrect\n"); + + ui->progressBar->setValue(0); + ui->progressBar->setMaximum(39024); + + int tries = 0; + for (int i = 0; i < 24; i++) { + QStringList seed = words; + + for (const auto &word : m_wordLists[language]) { + if (m_cancelled) { + emit searchFinished(true); + return; + } + emit progressUpdated(++tries); + + seed[i] = word; + + QString m = seed.join(" "); + bool done = this->testSeed(m, spkey); + if (done) { + return; + } + } + } + } + + if (mode == Mode::WORD_24) { + emit addResultText("Strategy [1/1]: one word is missing\n"); + + ui->progressBar->setValue(0); + ui->progressBar->setMaximum(39024); + + int tries = 0; + for (int i = 0; i < 24; i++) { + QStringList seed = words; + seed.insert(i, "placeholder"); + + for (const auto &word : m_wordLists[language]) { + if (m_cancelled) { + emit searchFinished(true); + return; + } + emit progressUpdated(++tries); + + seed[i] = word; + QString m = seed.join(" "); + + bool done = this->testSeed(m, spkey); + if (done) { + return; + } + } + } + } + + emit searchFinished(false); + }); + + m_watcher.setFuture(future.second); +} + +LegacySeedRecovery::~LegacySeedRecovery() = default; \ No newline at end of file diff --git a/src/dialog/LegacySeedRecovery.h b/src/dialog/LegacySeedRecovery.h new file mode 100644 index 0000000..e133157 --- /dev/null +++ b/src/dialog/LegacySeedRecovery.h @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#ifndef FEATHER_LEGACYSEEDRECOVERY_H +#define FEATHER_LEGACYSEEDRECOVERY_H + + +#include + +#include "components.h" +#include "utils/scheduler.h" + +#include "cryptonote_basic/account.h" + +namespace Ui { + class LegacySeedRecovery; +} + +class LegacySeedRecovery : public WindowModalDialog +{ +Q_OBJECT + +public: + explicit LegacySeedRecovery(QWidget *parent = nullptr); + ~LegacySeedRecovery() override; + + enum Mode { + WORD_24 = 0, + WORD_25 = 1 + }; + +signals: + void progressUpdated(int value); + void searchFinished(bool cancelled); + void matchFound(QString match); + void addressMatchFound(QString match); + void addResultText(QString text); + +private slots: + void onFinished(bool cancelled); + void onMatchFound(const QString &match); + void onAddressMatchFound(const QString &match); + void onProgressUpdated(int value); + void onAddResultText(const QString &text); + +private: + void checkSeed(); + QString mnemonic(const QList &words, const QList &index); + + bool testSeed(const QString &seed, const crypto::public_key &spkey); + + std::atomic m_cancelled = false; + + int m_major = 50; + int m_minor = 200; + + QHash m_wordLists; + QFutureWatcher m_watcher; + FutureScheduler m_scheduler; + QScopedPointer ui; +}; + + +#endif //FEATHER_LEGACYSEEDRECOVERY_H diff --git a/src/dialog/LegacySeedRecovery.ui b/src/dialog/LegacySeedRecovery.ui new file mode 100644 index 0000000..090551b --- /dev/null +++ b/src/dialog/LegacySeedRecovery.ui @@ -0,0 +1,156 @@ + + + LegacySeedRecovery + + + + 0 + 0 + 612 + 530 + + + + Legacy Seed Recovery + + + + + + Enter seed here (use a space between each word): + + + + + + + + + + Enter any deposit address associated with the wallet (optional) + + + + + + + + + Address + + + + + + + Account lookahead + + + + + + + Address lookahead + + + + + + + 5 + + + + + + + 50 + + + + + + + + + + Language + + + + + + + + + + + + Results + + + + + + 0 + + + + + + + true + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Close + + + + + + + + + buttonBox + accepted() + LegacySeedRecovery + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + LegacySeedRecovery + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/wizard/PageWalletRestoreSeed.cpp b/src/wizard/PageWalletRestoreSeed.cpp index 823f2ca..7080b74 100644 --- a/src/wizard/PageWalletRestoreSeed.cpp +++ b/src/wizard/PageWalletRestoreSeed.cpp @@ -12,6 +12,7 @@ #include #include "dialog/SeedRecoveryDialog.h" +#include "dialog/LegacySeedRecovery.h" #include // tevador 14 word #include "utils/Seed.h" #include "constants.h" @@ -61,8 +62,14 @@ PageWalletRestoreSeed::PageWalletRestoreSeed(WizardFields *fields, QWidget *pare QShortcut *shortcut = new QShortcut(QKeySequence("Ctrl+K"), this); QObject::connect(shortcut, &QShortcut::activated, [&](){ - SeedRecoveryDialog dialog{this}; - dialog.exec(); + if (ui->radio16->isChecked()) { + SeedRecoveryDialog dialog{this}; + dialog.exec(); + } + if (ui->radio25->isChecked()) { + LegacySeedRecovery dialog{this}; + dialog.exec(); + } }); ui->seedObscured->hide();