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();
}
}
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](){
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);
}
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() {
}
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);
}
}
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);
}
}
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;
}
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();
void focusSearchbar();
public slots:
+ QString getAddress(quint32 minorIndex);
void copyAddress();
void copyLabel();
void editLabel();
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
signals:
void refreshStarted() const;
void refreshFinished() const;
+ void corrupted() const;
private:
explicit Subaddress(Wallet *wallet, tools::wallet2 *wallet2, 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 ####################
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)) {
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 ####################
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;
#include "SubaddressRow.h"
-qsizetype SubaddressRow::getRow() const {
+quint32 SubaddressRow::getRow() const {
return m_row;
}
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)
, m_hidden(hidden)
, m_pinned(pinned) {}
- qsizetype getRow() const;
+ [[nodiscard]] quint32 getRow() const;
const QString& getAddress() const;
const QString& getLabel() const;
bool isUsed() const;
bool isPinned() const;
private:
- qsizetype m_row;
+ quint32 m_row;
QString m_address;
QString m_label;
bool m_used = false;
#include "SubaddressView.h"
#include "utils/Utils.h"
+#include "SubaddressModel.h"
SubaddressView::SubaddressView(QWidget *parent) : QTreeView(parent) {
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);
class SubaddressView : public QTreeView
{
+Q_OBJECT
public:
SubaddressView(QWidget* parent = nullptr);
+signals:
+ void copyAddress();
+
protected:
void keyPressEvent(QKeyEvent *event);
};