wizard: legacy seed recovery

This commit is contained in:
tobtoht 2024-03-06 13:16:34 +01:00
parent 83042f3d0f
commit e20186f428
No known key found for this signature in database
GPG key ID: E45B10DD027D2472
4 changed files with 515 additions and 2 deletions

View file

@ -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 <mnemonics/electrum-words.h>
#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<const Language::Base*> wordlists = crypto::ElectrumWords::get_language_list();
for (const auto& wordlist: wordlists) {
QStringList words_qt;
std::vector<std::string> 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<crypto::public_key> 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;

View file

@ -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 <QDialog>
#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<QStringList> &words, const QList<int> &index);
bool testSeed(const QString &seed, const crypto::public_key &spkey);
std::atomic<bool> m_cancelled = false;
int m_major = 50;
int m_minor = 200;
QHash<QString, QStringList> m_wordLists;
QFutureWatcher<void> m_watcher;
FutureScheduler m_scheduler;
QScopedPointer<Ui::LegacySeedRecovery> ui;
};
#endif //FEATHER_LEGACYSEEDRECOVERY_H

View file

@ -0,0 +1,156 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>LegacySeedRecovery</class>
<widget class="QDialog" name="LegacySeedRecovery">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>612</width>
<height>530</height>
</rect>
</property>
<property name="windowTitle">
<string>Legacy Seed Recovery</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Enter seed here (use a space between each word):</string>
</property>
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="seed"/>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Enter any deposit address associated with the wallet (optional)</string>
</property>
</widget>
</item>
<item>
<layout class="QFormLayout" name="formLayout">
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Address</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Account lookahead</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Address lookahead</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="line_majorLookahead">
<property name="text">
<string>5</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="line_minorLookahead">
<property name="text">
<string>50</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="line_depositAddress"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Language</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="combo_seedLanguage"/>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Results</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="results">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>LegacySeedRecovery</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>LegacySeedRecovery</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View file

@ -12,6 +12,7 @@
#include <QShortcut>
#include "dialog/SeedRecoveryDialog.h"
#include "dialog/LegacySeedRecovery.h"
#include <monero_seed/wordlist.hpp> // 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();