receive: improve wallet file corruption detection

This commit is contained in:
tobtoht 2024-03-08 17:35:57 +01:00
parent 4756f0dbe7
commit f97fa90521
No known key found for this signature in database
GPG key ID: E45B10DD027D2472
11 changed files with 180 additions and 50 deletions

View file

@ -1591,7 +1591,12 @@ void MainWindow::onInitiateTransaction() {
void MainWindow::onKeysCorrupted() {
if (!m_criticalWarningShown) {
m_criticalWarningShown = true;
Utils::showError(this, "Wallet keys are corrupted", "WARNING!\n\nTo prevent LOSS OF FUNDS do NOT continue to use this wallet file.\n\nRestore your wallet from seed.\n\nPlease report this incident to the Feather developers.\n\nWARNING!");
Utils::showError(this, "Potential wallet file corruption detected",
"WARNING!\n\n"
"To prevent LOSS OF FUNDS do NOT continue to use this wallet file.\n\n"
"Restore your wallet from seed, keys, or device.\n\n"
"Please report this incident to the Feather developers.\n\n"
"WARNING!", {}, "report_an_issue");
m_sendWidget->disallowSending();
}
}

View file

@ -30,7 +30,7 @@ ReceiveWidget::ReceiveWidget(Wallet *wallet, QWidget *parent)
ui->addresses->header()->setSectionResizeMode(SubaddressModel::Address, QHeaderView::ResizeToContents);
ui->addresses->header()->setSectionResizeMode(SubaddressModel::Label, QHeaderView::Stretch);
connect(ui->addresses->selectionModel(), &QItemSelectionModel::currentChanged, [=](QModelIndex current, QModelIndex prev){
connect(ui->addresses->selectionModel(), &QItemSelectionModel::selectionChanged, [=](const QItemSelection &selected, const QItemSelection &deselected){
this->updateQrCode();
});
connect(m_model, &SubaddressModel::modelReset, [this](){
@ -74,12 +74,12 @@ ReceiveWidget::ReceiveWidget(Wallet *wallet, QWidget *parent)
m_showTransactionsAction = new QAction("Show transactions");
connect(m_showTransactionsAction, &QAction::triggered, this, &ReceiveWidget::onShowTransactions);
connect(ui->addresses, &QTreeView::customContextMenuRequested, this, &ReceiveWidget::showContextMenu);
connect(ui->btn_generateSubaddress, &QPushButton::clicked, this, &ReceiveWidget::generateSubaddress);
connect(ui->addresses, &SubaddressView::copyAddress, this, &ReceiveWidget::copyAddress);
connect(ui->qrCode, &ClickableLabel::clicked, this, &ReceiveWidget::showQrCodeDialog);
connect(ui->search, &QLineEdit::textChanged, this, &ReceiveWidget::setSearchFilter);
connect(ui->btn_generateSubaddress, &QPushButton::clicked, this, &ReceiveWidget::generateSubaddress);
connect(ui->btn_createPaymentRequest, &QPushButton::clicked, this, &ReceiveWidget::createPaymentRequest);
}
@ -104,9 +104,32 @@ void ReceiveWidget::focusSearchbar() {
ui->search->setFocus();
}
QString ReceiveWidget::getAddress(quint32 minorIndex) {
bool ok;
QString reason;
QString address = m_wallet->getAddressSafe(m_wallet->currentSubaddressAccount(), minorIndex, ok, reason);
if (!ok) {
Utils::showError(this, "Unable to get address",
QString("Reason: %1\n\n"
"WARNING!\n\n"
"Potential wallet file corruption detected.\n\n"
"To prevent LOSS OF FUNDS do NOT continue to use this wallet file.\n\n"
"Restore your wallet from seed, keys, or device.\n\n"
"Please report this incident to the Feather developers.\n\n"
"WARNING!").arg(reason), {}, "report_an_issue");
return {};
}
return address;
}
void ReceiveWidget::copyAddress() {
QModelIndex index = ui->addresses->currentIndex();
Utils::copyColumn(&index, SubaddressModel::Address);
SubaddressRow* row = this->currentEntry();
if (!row) return;
QString address = this->getAddress(row->getRow());
Utils::copyToClipboard(address);
}
void ReceiveWidget::copyLabel() {
@ -115,7 +138,7 @@ void ReceiveWidget::copyLabel() {
}
void ReceiveWidget::editLabel() {
QModelIndex index = ui->addresses->currentIndex().siblingAtColumn(m_model->ModelColumn::Label);
QModelIndex index = ui->addresses->currentIndex().siblingAtColumn(SubaddressModel::ModelColumn::Label);
ui->addresses->setCurrentIndex(index);
ui->addresses->edit(index);
}
@ -164,24 +187,20 @@ void ReceiveWidget::showContextMenu(const QPoint &point) {
}
void ReceiveWidget::createPaymentRequest() {
QModelIndex index = ui->addresses->currentIndex();
if (!index.isValid()) {
return;
}
SubaddressRow* row = this->currentEntry();
if (!row) return;
QString address = index.model()->data(index.siblingAtColumn(SubaddressModel::Address), Qt::UserRole).toString();
QString address = this->getAddress(row->getRow());
PaymentRequestDialog dialog{this, m_wallet, address};
dialog.exec();
}
void ReceiveWidget::onShowTransactions() {
QModelIndex index = ui->addresses->currentIndex();
if (!index.isValid()) {
return;
}
SubaddressRow* row = this->currentEntry();
if (!row) return;
QString address = index.model()->data(index.siblingAtColumn(SubaddressModel::Address), Qt::UserRole).toString();
QString address = this->getAddress(row->getRow());
emit showTransactions(address);
}
@ -209,14 +228,14 @@ void ReceiveWidget::generateSubaddress() {
}
void ReceiveWidget::updateQrCode(){
QModelIndex index = ui->addresses->currentIndex();
if (!index.isValid()) {
SubaddressRow* row = this->currentEntry();
if (!row) {
ui->qrCode->clear();
ui->btn_createPaymentRequest->hide();
return;
}
QString address = index.model()->data(index.siblingAtColumn(SubaddressModel::Address), Qt::UserRole).toString();
QString address = this->getAddress(row->getRow());
const QrCode qrc(address, QrCode::Version::AUTO, QrCode::ErrorCorrectionLevel::MEDIUM);
int width = ui->qrCode->width() - 4;
@ -227,11 +246,10 @@ void ReceiveWidget::updateQrCode(){
}
void ReceiveWidget::showQrCodeDialog() {
QModelIndex index = ui->addresses->currentIndex();
if (!index.isValid()) {
return;
}
QString address = index.model()->data(index.siblingAtColumn(SubaddressModel::Address), Qt::UserRole).toString();
SubaddressRow* row = this->currentEntry();
if (!row) return;
QString address = this->getAddress(row->getRow());
QrCode qr(address, QrCode::Version::AUTO, QrCode::ErrorCorrectionLevel::HIGH);
QrCodeDialog dialog{this, &qr, "Address"};
dialog.exec();

View file

@ -31,6 +31,7 @@ public:
void focusSearchbar();
public slots:
QString getAddress(quint32 minorIndex);
void copyAddress();
void copyLabel();
void editLabel();

View file

@ -120,33 +120,53 @@ bool Subaddress::refresh(quint32 accountIndex)
emit refreshStarted();
this->clearRows();
for (qsizetype i = 0; i < m_wallet2->get_num_subaddresses(accountIndex); ++i)
bool potentialWalletFileCorruption = false;
for (quint32 i = 0; i < m_wallet2->get_num_subaddresses(accountIndex); ++i)
{
QString address = QString::fromStdString(m_wallet2->get_subaddress_as_str({accountIndex, (uint32_t)i}));
cryptonote::subaddress_index index = {accountIndex, i};
cryptonote::account_public_address address = m_wallet2->get_subaddress(index);
// Make sure we have previously generated Di
auto idx = m_wallet2->get_subaddress_index(address);
if (!idx) {
potentialWalletFileCorruption = true;
break;
}
// Verify mapping
if (idx != index) {
potentialWalletFileCorruption = true;
break;
}
QString addressStr = QString::fromStdString(cryptonote::get_account_address_as_str(m_wallet2->nettype(), !index.is_zero(), address));
auto* row = new SubaddressRow{this,
i,
address,
QString::fromStdString(m_wallet2->get_subaddress_label({accountIndex, (uint32_t)i})),
addressStr,
QString::fromStdString(m_wallet2->get_subaddress_label(index)),
m_wallet2->get_subaddress_used({accountIndex, (uint32_t)i}),
this->isHidden(address),
this->isPinned(address)
this->isHidden(addressStr),
this->isPinned(addressStr)
};
m_rows.append(row);
}
// Make sure keys are intact. We NEVER want to display incorrect addresses in case of memory corruption.
bool keysCorrupt = m_wallet2->get_device_type() == hw::device::SOFTWARE && !m_wallet2->verify_keys();
potentialWalletFileCorruption = potentialWalletFileCorruption || (m_wallet2->get_device_type() == hw::device::SOFTWARE && !m_wallet2->verify_keys());
if (keysCorrupt) {
clearRows();
if (potentialWalletFileCorruption) {
LOG_ERROR("KEY INCONSISTENCY DETECTED, WALLET IS IN CORRUPT STATE.");
clearRows();
emit corrupted();
}
emit refreshFinished();
return !keysCorrupt;
return !potentialWalletFileCorruption;
}
qsizetype Subaddress::count() const

View file

@ -46,6 +46,7 @@ public:
signals:
void refreshStarted() const;
void refreshFinished() const;
void corrupted() const;
private:
explicit Subaddress(Wallet *wallet, tools::wallet2 *wallet2, QObject *parent);

View file

@ -82,6 +82,10 @@ Wallet::Wallet(Monero::Wallet *wallet, QObject *parent)
connect(this, &Wallet::updated, this, &Wallet::onUpdated);
connect(this, &Wallet::heightsRefreshed, this, &Wallet::onHeightsRefreshed);
connect(this, &Wallet::transactionCommitted, this, &Wallet::onTransactionCommitted);
connect(m_subaddress, &Subaddress::corrupted, [this]{
emit keysCorrupted();
});
}
// #################### Status ####################
@ -185,6 +189,85 @@ QString Wallet::address(quint32 accountIndex, quint32 addressIndex) const {
return QString::fromStdString(m_wallet2->get_subaddress_as_str({accountIndex, addressIndex}));
}
QString Wallet::getAddressSafe(quint32 accountIndex, quint32 addressIndex, bool &ok, QString &reason) const {
ok = false;
// If we copy an address to clipboard or create a QR code, there must not be a spark of doubt that
// the address belongs to our wallet.
// This function does a number of sanity checks, some seemingly unnecessary or redundant to ensure
// that, yes, we own this address. It provides basic protection against memory corruption errors.
if (accountIndex >= this->numSubaddressAccounts()) {
reason = "Account index exceeds number of pre-computed subaddress accounts";
return {};
}
if (addressIndex >= this->numSubaddresses(accountIndex)) {
reason = "Address index exceeds number of pre-computed subaddresses";
return {};
}
// Realistically, nobody will have more than 1M subaddresses or accounts in their wallet.
if (accountIndex >= 1000000) {
reason = "Account index exceeds safety limit";
return {};
}
if (addressIndex >= 1000000) {
reason = "Address index exceeds safety limit";
return {};
}
// subaddress public spendkey (Di) = Hs(secret viewkey || subaddress index)G + primary address public spendkey (B)
// subaddress public viewkey (Ci) = D * secret viewkey (a)
if (!m_wallet2->verify_keys()) {
reason = "Unable to verify viewkey";
return {};
}
cryptonote::subaddress_index index = {accountIndex, addressIndex};
cryptonote::account_public_address address = m_wallet2->get_subaddress(index);
// Make sure we have previously generated Di
auto idx = m_wallet2->get_subaddress_index(address);
if (!idx) {
reason = "No mapping found for this subaddress public spendkey";
return {};
}
// Verify mapping
if (idx != index) {
reason = "Invalid subaddress public spendkey mapping";
return {};
}
// Recompute address
cryptonote::account_public_address address2 = m_wallet2->get_subaddress(idx.value());
if (address != address2) {
reason = "Recomputed address does not match original address";
return {};
}
std::string address_str = m_wallet2->get_subaddress_as_str(index);
// Make sure address is parseable
cryptonote::address_parse_info info;
if (!cryptonote::get_account_address_from_str(info, m_wallet2->nettype(), address_str)) {
reason = "Unable to parse address";
return {};
}
if (info.address != address) {
reason = "Parsed address does not match original address";
return {};
}
ok = true;
return QString::fromStdString(address_str);
}
SubaddressIndex Wallet::subaddressIndex(const QString &address) const {
std::pair<uint32_t, uint32_t> i;
if (!m_walletImpl->subaddressIndex(address.toStdString(), i)) {
@ -479,14 +562,7 @@ void Wallet::onRefreshed(bool success, const QString &message) {
void Wallet::refreshModels() {
m_history->refresh();
m_coins->refresh();
bool r = this->subaddress()->refresh(this->currentSubaddressAccount());
if (!r) {
// This should only happen if wallet keys got corrupted or were tampered with
// The list of subaddresses is wiped to prevent loss of funds
// Notify MainWindow to display an error message
emit keysCorrupted();
}
this->subaddress()->refresh(this->currentSubaddressAccount());
}
// #################### Hardware wallet ####################

View file

@ -154,8 +154,8 @@ public:
void updateBalance();
// ##### Subaddresses and Accounts #####
//! returns wallet's public address
QString address(quint32 accountIndex, quint32 addressIndex) const;
QString getAddressSafe(quint32 accountIndex, quint32 addressIndex, bool &ok, QString &reason) const;
//! returns the subaddress index of the address
SubaddressIndex subaddressIndex(const QString &address) const;

View file

@ -3,7 +3,7 @@
#include "SubaddressRow.h"
qsizetype SubaddressRow::getRow() const {
quint32 SubaddressRow::getRow() const {
return m_row;
}

View file

@ -11,7 +11,7 @@ class SubaddressRow : public QObject
Q_OBJECT
public:
SubaddressRow(QObject *parent, qsizetype row, const QString& address, const QString &label, bool used, bool hidden, bool pinned)
SubaddressRow(QObject *parent, quint32 row, const QString& address, const QString &label, bool used, bool hidden, bool pinned)
: QObject(parent)
, m_row(row)
, m_address(address)
@ -20,7 +20,7 @@ public:
, m_hidden(hidden)
, m_pinned(pinned) {}
qsizetype getRow() const;
[[nodiscard]] quint32 getRow() const;
const QString& getAddress() const;
const QString& getLabel() const;
bool isUsed() const;
@ -28,7 +28,7 @@ public:
bool isPinned() const;
private:
qsizetype m_row;
quint32 m_row;
QString m_address;
QString m_label;
bool m_used = false;

View file

@ -4,6 +4,7 @@
#include "SubaddressView.h"
#include "utils/Utils.h"
#include "SubaddressModel.h"
SubaddressView::SubaddressView(QWidget *parent) : QTreeView(parent) {
@ -15,7 +16,11 @@ void SubaddressView::keyPressEvent(QKeyEvent *event){
if(!selectedIndexes().isEmpty()){
if(event->matches(QKeySequence::Copy)){
QModelIndex index = this->currentIndex();
Utils::copyColumn(&index, index.column());
if (index.column() == SubaddressModel::ModelColumn::Address) {
emit copyAddress();
} else {
Utils::copyColumn(&index, index.column());
}
}
else
QTreeView::keyPressEvent(event);

View file

@ -10,10 +10,14 @@
class SubaddressView : public QTreeView
{
Q_OBJECT
public:
SubaddressView(QWidget* parent = nullptr);
signals:
void copyAddress();
protected:
void keyPressEvent(QKeyEvent *event);
};