From aa3d67424248f98108e07fbdd5bbb5171fb3b20e Mon Sep 17 00:00:00 2001 From: tobtoht Date: Wed, 20 Jan 2021 22:13:51 +0100 Subject: [PATCH] FeatherSeed: allow erasure --- src/appcontext.cpp | 13 ++- src/appcontext.h | 3 +- src/dialog/restoredialog.h | 2 +- src/utils/FeatherSeed.h | 114 ++++++++++++++++++++++ src/utils/RestoreHeightLookup.h | 76 +++++++++++++++ src/utils/seeds.h | 168 -------------------------------- src/wizard/createwalletseed.cpp | 4 +- src/wizard/restorewallet.cpp | 36 ++++--- src/wizard/walletwizard.cpp | 3 +- src/wizard/walletwizard.h | 2 +- 10 files changed, 230 insertions(+), 191 deletions(-) create mode 100644 src/utils/FeatherSeed.h create mode 100644 src/utils/RestoreHeightLookup.h delete mode 100644 src/utils/seeds.h diff --git a/src/appcontext.cpp b/src/appcontext.cpp index 981957a..338490f 100644 --- a/src/appcontext.cpp +++ b/src/appcontext.cpp @@ -533,12 +533,21 @@ void AppContext::createWallet(FeatherSeed seed, const QString &path, const QStri return; } - if(seed.mnemonicSeed.isEmpty()) { + if(seed.mnemonic.isEmpty()) { emit walletCreatedError("Mnemonic seed error. Failed to write wallet."); return; } - this->currentWallet = seed.writeWallet(this->walletManager, this->networkType, path, password, this->kdfRounds); + Wallet *wallet = nullptr; + if (seed.seedType == SeedType::TEVADOR) { + wallet = this->walletManager->createDeterministicWalletFromSpendKey(path, password, seed.language, this->networkType, seed.spendKey, seed.restoreHeight, this->kdfRounds); + wallet->setCacheAttribute("feather.seed", seed.mnemonic.join(" ")); + } + if (seed.seedType == SeedType::MONERO) { + wallet = this->walletManager->recoveryWallet(path, password, seed.mnemonic.join(" "), "", this->networkType, seed.restoreHeight, this->kdfRounds); + } + + this->currentWallet = wallet; if(this->currentWallet == nullptr) { emit walletCreatedError("Failed to write wallet"); return; diff --git a/src/appcontext.h b/src/appcontext.h index c647dfb..d74acb0 100644 --- a/src/appcontext.h +++ b/src/appcontext.h @@ -17,9 +17,10 @@ #include "utils/xmrig.h" #include "utils/wsclient.h" #include "utils/txfiathistory.h" +#include "utils/FeatherSeed.h" #include "widgets/RedditPost.h" #include "widgets/CCSEntry.h" -#include "utils/seeds.h" +#include "utils/RestoreHeightLookup.h" #include "utils/nodes.h" #include "libwalletqt/WalletManager.h" diff --git a/src/dialog/restoredialog.h b/src/dialog/restoredialog.h index f1a91bc..065cc90 100644 --- a/src/dialog/restoredialog.h +++ b/src/dialog/restoredialog.h @@ -10,7 +10,7 @@ #include #include -#include "utils/seeds.h" +#include "utils/RestoreHeightLookup.h" #include "appcontext.h" namespace Ui { diff --git a/src/utils/FeatherSeed.h b/src/utils/FeatherSeed.h new file mode 100644 index 0000000..c7a9346 --- /dev/null +++ b/src/utils/FeatherSeed.h @@ -0,0 +1,114 @@ +#ifndef FEATHER_FEATHERSEED_H +#define FEATHER_FEATHERSEED_H + +#include "libwalletqt/WalletManager.h" +#include "libwalletqt/Wallet.h" + +#include +#include "RestoreHeightLookup.h" + +enum SeedType { + MONERO = 0, // 25 word seeds + TEVADOR // 14 word seeds +}; + +struct FeatherSeed { + QString coin; + QString language; + SeedType seedType; + + QStringList mnemonic; + QString spendKey; + QString correction; + + time_t time; + int restoreHeight = 0; + RestoreHeightLookup *lookup = nullptr; + + QString errorString; + + explicit FeatherSeed(RestoreHeightLookup *lookup, + const QString &coin = "monero", + const QString &language = "English", + const QStringList &mnemonic = {}) + : lookup(lookup), coin(coin), language(language), mnemonic(mnemonic) + { + // Generate a new mnemonic if none was given + if (mnemonic.length() == 0) { + this->time = std::time(nullptr); + monero_seed seed(this->time, coin.toStdString()); + + std::stringstream buffer; + buffer << seed; + this->mnemonic = QString::fromStdString(buffer.str()).split(" "); + + buffer.str(std::string()); + buffer << seed.key(); + this->spendKey = QString::fromStdString(buffer.str()); + + this->setRestoreHeight(); + } + + if (mnemonic.length() == 25) { + this->seedType = SeedType::MONERO; + } + else if (mnemonic.length() == 14) { + this->seedType = SeedType::TEVADOR; + } else { + this->errorString = "Mnemonic seed does not match known type"; + return; + } + + if (seedType == SeedType::TEVADOR) { + try { + monero_seed seed(mnemonic.join(" ").toStdString(), coin.toStdString()); + + this->time = seed.date(); + this->setRestoreHeight(); + + std::stringstream buffer; + buffer << seed.key(); + this->spendKey = QString::fromStdString(buffer.str()); + + this->correction = QString::fromStdString(seed.correction()); + if (!this->correction.isEmpty()) { + buffer.str(std::string()); + buffer << seed; + int index = this->mnemonic.indexOf("xxxx"); + this->mnemonic.replace(index, this->correction); + } + } + catch (const std::exception &e) { + this->errorString = e.what(); + return; + } + } + } + + int setRestoreHeight() { + if (this->lookup == nullptr) + return 1; + + if (this->time == 0) + return 1; + + this->restoreHeight = this->lookup->dateToRestoreHeight(this->time); + return this->restoreHeight; + } + + int setRestoreHeight(int height) { + auto now = std::time(nullptr); + auto nowClearance = 3600 * 24; + auto currentBlockHeight = this->lookup->dateToRestoreHeight(now - nowClearance); + if (height >= currentBlockHeight + nowClearance) { + qCritical() << "unrealistic restore height detected, setting to current blockheight instead: " << currentBlockHeight; + this->restoreHeight = currentBlockHeight; + } else { + this->restoreHeight = height; + } + + return this->restoreHeight; + } +}; + +#endif //FEATHER_FEATHERSEED_H diff --git a/src/utils/RestoreHeightLookup.h b/src/utils/RestoreHeightLookup.h new file mode 100644 index 0000000..3181df4 --- /dev/null +++ b/src/utils/RestoreHeightLookup.h @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#ifndef FEATHER_RESTOREHEIGHTLOOKUP_H +#define FEATHER_RESTOREHEIGHTLOOKUP_H + +#include +#include + +#include + +#include "networktype.h" +#include "utils/utils.h" + +struct RestoreHeightLookup { + NetworkType::Type type; + QMap data; + explicit RestoreHeightLookup(NetworkType::Type type) : type(type) {} + + int dateToRestoreHeight(int date) { + // restore height based on a given timestamp using a lookup + // table. If it cannot find the date in the lookup table, it + // will calculate the blockheight based off the last known + // date: ((now - lastKnownDate) / blockTime) - clearance + + if(this->type == NetworkType::TESTNET) return 1; + int blockTime = 120; + int blocksPerDay = 86400 / blockTime; + int blockCalcClearance = blocksPerDay * 5; + QList values = this->data.keys(); + if(date <= values.at(0)) + return this->data[values.at(0)]; + for(int i = 0; i != values.count(); i++) { + if(values[i] > date) { + return i - 1 < 0 ? this->data[values[i]] : this->data[values[i-1]] - blockCalcClearance; + } + } + + // lookup failed, calculate blockheight from last known checkpoint + int lastBlockHeightTime = values.at(values.count() - 1); + int lastBlockHeight = this->data[lastBlockHeightTime]; + int deltaTime = date - lastBlockHeightTime; + int deltaBlocks = deltaTime / blockTime; + int blockHeight = (lastBlockHeight + deltaBlocks) - blockCalcClearance; + qDebug() << "Calculated blockheight: " << blockHeight << " from epoch " << date; + return blockHeight; + } + + int restoreHeightToDate(int height) { + // @TODO: most likely inefficient, refactor + QMap::iterator i; + int timestamp = 0; + for (i = this->data.begin(); i != this->data.end(); ++i) { + int ts = i.key(); + if (i.value() > height) + return timestamp; + timestamp = ts; + } + return timestamp; + } + + static RestoreHeightLookup *fromFile(const QString &fn, NetworkType::Type type) { + // initialize this class using a lookup table, e.g `:/assets/restore_heights_monero_mainnet.txt`/ + auto rtn = new RestoreHeightLookup(type); + auto data = Utils::barrayToString(Utils::fileOpen(fn)); + QMap _data; + for(const auto &line: data.split('\n')) { + if(line.trimmed().isEmpty()) continue; + auto spl = line.trimmed().split(':'); + rtn->data[spl.at(0).toUInt()] = spl.at(1).toUInt(); + } + return rtn; + } +}; + +#endif //FEATHER_RESTOREHEIGHTLOOKUP_H diff --git a/src/utils/seeds.h b/src/utils/seeds.h deleted file mode 100644 index 979bd49..0000000 --- a/src/utils/seeds.h +++ /dev/null @@ -1,168 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -// Copyright (c) 2020-2021, The Monero Project. - -#ifndef FEATHER_SEEDS_H -#define FEATHER_SEEDS_H - -#include -#include -#include - -#include - -#include "networktype.h" -#include "libwalletqt/WalletManager.h" -#include "libwalletqt/Wallet.h" -#include "utils/utils.h" - -struct RestoreHeightLookup { - NetworkType::Type type; - QMap data; - explicit RestoreHeightLookup(NetworkType::Type type) : type(type) {} - - int dateToRestoreHeight(int date) { - // restore height based on a given timestamp using a lookup - // table. If it cannot find the date in the lookup table, it - // will calculate the blockheight based off the last known - // date: ((now - lastKnownDate) / blockTime) - clearance - - if(this->type == NetworkType::TESTNET) return 1; - int blockTime = 120; - int blocksPerDay = 86400 / blockTime; - int blockCalcClearance = blocksPerDay * 5; - QList values = this->data.keys(); - if(date <= values.at(0)) - return this->data[values.at(0)]; - for(int i = 0; i != values.count(); i++) { - if(values[i] > date) { - return i - 1 < 0 ? this->data[values[i]] : this->data[values[i-1]] - blockCalcClearance; - } - } - - // lookup failed, calculate blockheight from last known checkpoint - int lastBlockHeightTime = values.at(values.count() - 1); - int lastBlockHeight = this->data[lastBlockHeightTime]; - int deltaTime = date - lastBlockHeightTime; - int deltaBlocks = deltaTime / blockTime; - int blockHeight = (lastBlockHeight + deltaBlocks) - blockCalcClearance; - qDebug() << "Calculated blockheight: " << blockHeight << " from epoch " << date; - return blockHeight; - } - - int restoreHeightToDate(int height) { - // @TODO: most likely inefficient, refactor - QMap::iterator i; - int timestamp = 0; - for (i = this->data.begin(); i != this->data.end(); ++i) { - int ts = i.key(); - if (i.value() > height) - return timestamp; - timestamp = ts; - } - return timestamp; - } - - static RestoreHeightLookup *fromFile(const QString &fn, NetworkType::Type type) { - // initialize this class using a lookup table, e.g `:/assets/restore_heights_monero_mainnet.txt`/ - auto rtn = new RestoreHeightLookup(type); - auto data = Utils::barrayToString(Utils::fileOpen(fn)); - QMap _data; - for(const auto &line: data.split('\n')) { - if(line.trimmed().isEmpty()) continue; - auto spl = line.trimmed().split(':'); - rtn->data[spl.at(0).toUInt()] = spl.at(1).toUInt(); - } - return rtn; - } -}; - -struct FeatherSeed { - QString mnemonicSeed; - QString spendKey; - time_t time = 0; - int restoreHeight = 0; - RestoreHeightLookup *lookup = nullptr; - QString language; - std::string coinName; - explicit FeatherSeed(RestoreHeightLookup *lookup, const std::string &coinName = "monero", const QString &language = "English") : lookup(lookup), coinName(coinName), language(language) {} - - static FeatherSeed fromSeed(RestoreHeightLookup *lookup, - const std::string &coinName, - const QString &seedLanguage, - const std::string &mnemonicSeed) { - - auto rtn = FeatherSeed(lookup, coinName, seedLanguage); - rtn.lookup = lookup; - rtn.mnemonicSeed = QString::fromStdString(mnemonicSeed); - - if(QString::fromStdString(mnemonicSeed).split(" ").count() == 14) { - monero_seed seed(mnemonicSeed, coinName); - std::stringstream buffer; - buffer << seed.key(); - rtn.time = seed.date(); - rtn.setRestoreHeight(); - rtn.spendKey = QString::fromStdString(buffer.str()); - } - return rtn; - } - - static FeatherSeed generate(RestoreHeightLookup *lookup, const std::string &coinName, const QString &language) { - auto rtn = FeatherSeed(lookup, coinName, language); - time_t _time = std::time(nullptr); - monero_seed seed(_time, coinName); - - std::stringstream buffer; - buffer << seed; - rtn.mnemonicSeed = QString::fromStdString(buffer.str()); - buffer.str(std::string()); - buffer << seed.key(); - rtn.spendKey = QString::fromStdString(buffer.str()); - rtn.time = _time; - rtn.setRestoreHeight(); - return rtn; - } - - Wallet *writeWallet(WalletManager *manager, NetworkType::Type type, const QString &path, const QString &password, quint64 kdfRounds) { - // writes both 14/25 word mnemonic seeds. - Wallet *wallet = nullptr; - if(this->lookup == nullptr) return wallet; - if(this->mnemonicSeed.split(" ").count() == 14) { - if(this->spendKey.isEmpty()) { - auto _seed = FeatherSeed::fromSeed(this->lookup, this->coinName, this->language, this->mnemonicSeed.toStdString()); - _seed.setRestoreHeight(); - this->time = _seed.time; - this->restoreHeight = _seed.restoreHeight; - this->spendKey = _seed.spendKey; - } - wallet = manager->createDeterministicWalletFromSpendKey(path, password, this->language, type, this->spendKey, this->restoreHeight, kdfRounds); - wallet->setCacheAttribute("feather.seed", this->mnemonicSeed); - } else { - wallet = manager->recoveryWallet(path, password, this->mnemonicSeed, "", type, this->restoreHeight, kdfRounds); - } - - wallet->setPassword(password); - return wallet; - } - - int setRestoreHeight() { - if(this->lookup == nullptr) return 1; - if(this->time == 0) return 1; - this->restoreHeight = this->lookup->dateToRestoreHeight(this->time); - return this->restoreHeight; - } - - int setRestoreHeight(int height) { - auto now = std::time(nullptr); - auto nowClearance = 3600 * 24; - auto currentBlockHeight = this->lookup->dateToRestoreHeight(now - nowClearance); - if(height >= currentBlockHeight + nowClearance) { - qCritical() << "unrealistic restore height detected, setting to current blockheight instead: " << currentBlockHeight; - this->restoreHeight = currentBlockHeight; - } else - this->restoreHeight = height; - - return this->restoreHeight; - } -}; - -#endif //FEATHER_SEEDS_H diff --git a/src/wizard/createwalletseed.cpp b/src/wizard/createwalletseed.cpp index 5417b05..05bc9b4 100644 --- a/src/wizard/createwalletseed.cpp +++ b/src/wizard/createwalletseed.cpp @@ -34,8 +34,8 @@ CreateWalletSeedPage::CreateWalletSeedPage(AppContext *ctx, QWidget *parent) : void CreateWalletSeedPage::seedRoulette(int count) { count += 1; if(count > m_rouletteSpin) return; - auto seed = FeatherSeed::generate(m_ctx->restoreHeights[m_ctx->networkType], m_ctx->coinName.toStdString(), m_ctx->seedLanguage); - m_mnemonic = seed.mnemonicSeed; + FeatherSeed seed = FeatherSeed(m_ctx->restoreHeights[m_ctx->networkType], m_ctx->coinName, m_ctx->seedLanguage); + m_mnemonic = seed.mnemonic.join(" "); m_restoreHeight = seed.restoreHeight; this->displaySeed(m_mnemonic); diff --git a/src/wizard/restorewallet.cpp b/src/wizard/restorewallet.cpp index f765f61..ed6cd5f 100644 --- a/src/wizard/restorewallet.cpp +++ b/src/wizard/restorewallet.cpp @@ -7,8 +7,10 @@ #include #include +#include #include // tevador 14 word +#include "utils/FeatherSeed.h" RestorePage::RestorePage(AppContext *ctx, QWidget *parent) : QWizardPage(parent), @@ -29,6 +31,10 @@ RestorePage::RestorePage(AppContext *ctx, QWidget *parent) : for(int i = 0; i != 2048; i++) m_words14 << QString::fromStdString(wordlist::english.get_word(i)); + // Restore has limited error correction capability, namely it can correct a single erasure + // (illegible word with a known location). This can be tested by replacing a word with xxxx + m_words14 << "xxxx"; + // m_completer14Model = new QStringListModel(m_words14, m_completer14); m_completer14 = new QCompleter(this); @@ -127,13 +133,6 @@ bool RestorePage::validatePage() { return false; } } - - auto _seed = FeatherSeed::fromSeed(m_ctx->restoreHeights[m_ctx->networkType], m_ctx->coinName.toStdString(), m_ctx->seedLanguage, seed.toStdString()); - restoreHeight = _seed.restoreHeight; - - this->setField("restoreHeight", restoreHeight); - this->setField("mnemonicRestoredSeed", seed); - return true; } else if(m_mode == 25) { if(seedSplit.length() != 25) { ui->label_errorString->show(); @@ -150,15 +149,22 @@ bool RestorePage::validatePage() { return false; } } + } - auto _seed = FeatherSeed::fromSeed(m_ctx->restoreHeights[m_ctx->networkType], m_ctx->coinName.toStdString(), m_ctx->seedLanguage, seed.toStdString()); - _seed.setRestoreHeight(restoreHeight); - this->setField("restoreHeight", restoreHeight); - this->setField("mnemonicSeed", seed); - this->setField("mnemonicRestoredSeed", seed); - return true; + auto _seed = FeatherSeed(m_ctx->restoreHeights[m_ctx->networkType], m_ctx->coinName, m_ctx->seedLanguage, seedSplit); + if (!_seed.errorString.isEmpty()) { + QMessageBox::warning(this, "Invalid seed", QString("Invalid seed:\n\n%1").arg(_seed.errorString)); + ui->seedEdit->setStyleSheet(errStyle); + return false; + } + if (!_seed.correction.isEmpty()) { + QMessageBox::information(this, "Corrected erasure", QString("xxxx -> %1").arg(_seed.correction)); } - ui->seedEdit->setStyleSheet(errStyle); - return false; + restoreHeight = _seed.restoreHeight; + + this->setField("restoreHeight", restoreHeight); + this->setField("mnemonicSeed", seed); + this->setField("mnemonicRestoredSeed", seed); + return true; } diff --git a/src/wizard/walletwizard.cpp b/src/wizard/walletwizard.cpp index cf165ca..757e104 100644 --- a/src/wizard/walletwizard.cpp +++ b/src/wizard/walletwizard.cpp @@ -80,7 +80,8 @@ void WalletWizard::createWallet() { return; } - auto seed = FeatherSeed::fromSeed(m_ctx->restoreHeights[m_ctx->networkType], m_ctx->coinName.toStdString(), m_ctx->seedLanguage, mnemonicSeed.toStdString()); + auto seed = FeatherSeed(m_ctx->restoreHeights[m_ctx->networkType], m_ctx->coinName, m_ctx->seedLanguage, mnemonicSeed.split(" ")); + if(restoreHeight > 0) seed.setRestoreHeight(restoreHeight); m_ctx->createWallet(seed, walletPath, walletPasswd); diff --git a/src/wizard/walletwizard.h b/src/wizard/walletwizard.h index 78abab0..3d3b885 100644 --- a/src/wizard/walletwizard.h +++ b/src/wizard/walletwizard.h @@ -9,7 +9,7 @@ #include #include "appcontext.h" -#include "utils/seeds.h" +#include "utils/RestoreHeightLookup.h" #include "utils/config.h" class WalletWizard : public QWizard