From 6225e0e389b8ce639f25e0801b96facb06cdecfa Mon Sep 17 00:00:00 2001 From: tobtoht Date: Sat, 25 Nov 2023 00:54:01 +0100 Subject: [PATCH] wizard: add diceware support --- src/dialog/SeedDiceDialog.cpp | 183 +++++++++++++++++++++ src/dialog/SeedDiceDialog.h | 47 ++++++ src/dialog/SeedDiceDialog.ui | 292 ++++++++++++++++++++++++++++++++++ src/polyseed/polyseed.cpp | 27 +++- src/polyseed/polyseed.h | 1 + src/utils/Seed.cpp | 14 +- src/utils/Seed.h | 2 +- src/wizard/PageWalletSeed.cpp | 27 +++- src/wizard/PageWalletSeed.h | 2 +- 9 files changed, 583 insertions(+), 12 deletions(-) create mode 100644 src/dialog/SeedDiceDialog.cpp create mode 100644 src/dialog/SeedDiceDialog.h create mode 100644 src/dialog/SeedDiceDialog.ui diff --git a/src/dialog/SeedDiceDialog.cpp b/src/dialog/SeedDiceDialog.cpp new file mode 100644 index 0000000..f0bb4ca --- /dev/null +++ b/src/dialog/SeedDiceDialog.cpp @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#include "SeedDiceDialog.h" +#include "ui_SeedDiceDialog.h" + +#include +#include + +#include + +#include "utils/Seed.h" + +SeedDiceDialog::SeedDiceDialog(QWidget *parent) + : WindowModalDialog(parent) + , ui(new Ui::SeedDiceDialog) +{ + ui->setupUi(this); + + ui->frame_dice->hide(); + ui->frame_coinflip->hide(); + + connect(ui->radio_dice, &QRadioButton::toggled, [this](bool toggled){ + ui->frame_dice->setVisible(toggled); + this->updateRollsLeft(); + ui->label_rollsLeft2->setText("Rolls left:"); + ui->label_rolls->setText("Rolls:"); + }); + + connect(ui->radio_coinflip, &QRadioButton::toggled, [this](bool toggled){ + ui->frame_coinflip->setVisible(toggled); + this->updateRollsLeft(); + ui->label_rollsLeft2->setText("Flips left:"); + ui->label_rolls->setText("Flips:"); + }); + + connect(ui->spin_sides, &QSpinBox::valueChanged, [this](int value){ + if (!ui->radio_dice->isChecked()) { + return; + } + this->updateRollsLeft(); + }); + + connect(ui->line_roll, &QLineEdit::textChanged, this, &SeedDiceDialog::validateRollEntry); + + connect(ui->btn_next, &QPushButton::clicked, [this]{ + this->setEnableMethodSelection(false); + + if (!this->validateRollEntry()) { + return; + } + + QStringList rolls = ui->line_roll->text().simplified().split(" "); + for (const auto &roll : rolls) { + this->addRoll(roll); + } + + ui->line_roll->clear(); + ui->line_roll->setFocus(); + }); + + connect(ui->btn_heads, &QPushButton::clicked, [this]{ + this->setEnableMethodSelection(false); + this->addFlip(true); + }); + + connect(ui->btn_tails, &QPushButton::clicked, [this]{ + this->setEnableMethodSelection(false); + this->addFlip(false); + }); + + connect(ui->btn_reset, &QPushButton::clicked, [this]{ + m_rolls.clear(); + this->update(); + this->setEnableMethodSelection(true); + ui->btn_createPolyseed->setEnabled(false); + }); + + connect(ui->btn_createPolyseed, &QPushButton::clicked, [this]{ + QByteArray salt = "POLYSEED"; + QByteArray data = m_rolls.join(" ").toUtf8(); + + // We already have enough entropy assuming unbiased throws, but a few extra rounds can't hurt + // Polyseed requests 19 bytes of random data and discards two bits (for a total of 150 bits) + m_key = QPasswordDigestor::deriveKeyPbkdf2(QCryptographicHash::Sha256, data, salt, 2048, 19); + + this->accept(); + }); + + connect(ui->btn_cancel, &QPushButton::clicked, [this]{ + this->reject(); + }); + + ui->radio_dice->setChecked(true); + + this->update(); + this->adjustSize(); +} + +void SeedDiceDialog::addFlip(bool heads) { + m_rolls << (heads ? "H" : "T"); + this->update(); +} + +void SeedDiceDialog::addRoll(const QString &roll) { + if (roll.isEmpty()) { + return; + } + + m_rolls << roll; + this->update(); +} + +bool SeedDiceDialog::validateRollEntry() { + ui->line_roll->setStyleSheet(""); + + QString errStyle = "QLineEdit{border: 1px solid red;}"; + QStringList rolls = ui->line_roll->text().simplified().split(" "); + + for (const auto &rollstr : rolls) { + if (rollstr.isEmpty()) { + continue; + } + + bool ok; + int roll = rollstr.toInt(&ok); + if (!ok || roll < 1 || roll > ui->spin_sides->value()) { + ui->line_roll->setStyleSheet(errStyle); + return false; + } + } + + return true; +} + +void SeedDiceDialog::update() { + this->updateRollsLeft(); + this->updateRolls(); + + if (this->updateEntropy()) { + ui->btn_createPolyseed->setEnabled(true); + } +} + +bool SeedDiceDialog::updateEntropy() { + double entropy = entropyPerRoll() * m_rolls.length(); + ui->label_entropy->setText(QString("%1 / %2 bits").arg(QString::number(entropy, 'f', 2), QString::number(entropyNeeded))); + + return entropy > entropyNeeded; +} + +void SeedDiceDialog::updateRolls() { + ui->rolls->setPlainText(m_rolls.join(" ")); +} + +double SeedDiceDialog::entropyPerRoll() { + if (ui->radio_dice->isChecked()) { + return log(ui->spin_sides->value()) / log(2); + } else { + return 1; + } +} + +void SeedDiceDialog::updateRollsLeft() { + int rollsLeft = std::max((int)(ceil((entropyNeeded - (this->entropyPerRoll() * m_rolls.length())) / this->entropyPerRoll())), 0); + ui->label_rollsLeft->setText(QString::number(rollsLeft)); +} + +void SeedDiceDialog::setEnableMethodSelection(bool enabled) { + ui->radio_dice->setEnabled(enabled); + ui->radio_coinflip->setEnabled(enabled); + ui->spin_sides->setEnabled(enabled); +} + +const char* SeedDiceDialog::getSecret() { + return m_key.data(); +} + +const QString& SeedDiceDialog::getMnemonic() { + return m_mnemonic; +} + +SeedDiceDialog::~SeedDiceDialog() = default; \ No newline at end of file diff --git a/src/dialog/SeedDiceDialog.h b/src/dialog/SeedDiceDialog.h new file mode 100644 index 0000000..228e03b --- /dev/null +++ b/src/dialog/SeedDiceDialog.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#include + +#include "components.h" + +#ifndef FEATHER_SEEDDICEDIALOG_H +#define FEATHER_SEEDDICEDIALOG_H + +namespace Ui { + class SeedDiceDialog; +} + +class SeedDiceDialog : public WindowModalDialog +{ + Q_OBJECT + +public: + explicit SeedDiceDialog(QWidget *parent); + ~SeedDiceDialog() override; + + const char* getSecret(); + + const QString& getMnemonic(); + +private: + void addFlip(bool heads); + void addRoll(const QString &roll); + double entropyPerRoll(); + bool validateRollEntry(); + + void update(); + bool updateEntropy(); + void updateRolls(); + void updateRollsLeft(); + void setEnableMethodSelection(bool enabled); + + QScopedPointer ui; + QStringList m_rolls; + QByteArray m_key; + int entropyNeeded = 152; // Polyseed requests 19 bytes of random data + QString m_mnemonic; +}; + + +#endif //FEATHER_SEEDDICEDIALOG_H diff --git a/src/dialog/SeedDiceDialog.ui b/src/dialog/SeedDiceDialog.ui new file mode 100644 index 0000000..4bc46fc --- /dev/null +++ b/src/dialog/SeedDiceDialog.ui @@ -0,0 +1,292 @@ + + + SeedDiceDialog + + + + 0 + 0 + 881 + 547 + + + + Diceware + + + + + + Select method: + + + + + + + + Dice rolls + + + + + + + + + + 0 + 0 + + + + 2 + + + 100 + + + 6 + + + + + + + -sided die + + + + + + + + + + + Coinflips + + + + + + + + + + Qt::Horizontal + + + + + + + QFormLayout::ExpandingFieldsGrow + + + + + Entropy: + + + + + + + 0 bits + + + + + + + Rolls left: + + + + + + + 0 rolls + + + + + + + Rolls: + + + + + + + false + + + + 500 + 0 + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Minimum + + + + 20 + 10 + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + + 0 + 0 + + + + Enter roll: + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + Next roll + + + + + + + + + false + + + (Use a space between each roll to enter multiple rolls in one go) + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + Add flip: + + + + + + + Heads + + + + + + + Tails + + + + + + + + + + + + Reset + + + + + + + Cancel + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + false + + + Create polyseed + + + + + + + + + + diff --git a/src/polyseed/polyseed.cpp b/src/polyseed/polyseed.cpp index 9074687..ba100be 100644 --- a/src/polyseed/polyseed.cpp +++ b/src/polyseed/polyseed.cpp @@ -13,6 +13,8 @@ #include +#define POLYSEED_RANDBYTES 19 + namespace polyseed { static std::locale locale; @@ -39,6 +41,18 @@ namespace polyseed { return size; } + static char seed[POLYSEED_RANDBYTES]{}; + + static void randombytes(void* const buf, const size_t size) { + assert(size <= POLYSEED_RANDBYTES); + if (std::all_of(seed, seed + size, [](char c) { return c == '\0'; })) { + randombytes_buf(buf, size); + } else { + memcpy(buf, seed, size); + sodium_memzero(seed, POLYSEED_RANDBYTES); + } + } + struct dependency { dependency(); std::vector languages; @@ -55,8 +69,10 @@ namespace polyseed { gen.locale_cache_enabled(true); locale = gen(""); + sodium_memzero(seed, POLYSEED_RANDBYTES); + polyseed_dependency pd; - pd.randbytes = &randombytes_buf; + pd.randbytes = &randombytes; pd.pbkdf2_sha256 = &crypto_pbkdf2_sha256; pd.memzero = &sodium_memzero; pd.u8_nfc = &utf8_nfc; @@ -121,6 +137,15 @@ namespace polyseed { } } + void data::create_from_secret(feature_type features, const char* secret) { + check_init(); + memcpy(seed, secret, POLYSEED_RANDBYTES); + auto status = polyseed_create(features, &m_data); + if (status != POLYSEED_OK) { + throw get_error(status); + } + } + void data::load(polyseed_storage storage) { check_init(); auto status = polyseed_load(storage, &m_data); diff --git a/src/polyseed/polyseed.h b/src/polyseed/polyseed.h index e676b47..441b9e3 100644 --- a/src/polyseed/polyseed.h +++ b/src/polyseed/polyseed.h @@ -61,6 +61,7 @@ namespace polyseed { } void create(feature_type features); + void create_from_secret(feature_type features, const char* secret); void load(polyseed_storage storage); diff --git a/src/utils/Seed.cpp b/src/utils/Seed.cpp index e916550..f185445 100644 --- a/src/utils/Seed.cpp +++ b/src/utils/Seed.cpp @@ -4,8 +4,10 @@ #include #include "Seed.h" -Seed::Seed(Type type, NetworkType::Type networkType, QString language) - : type(type), networkType(networkType), language(std::move(language)) +Seed::Seed(Type type, NetworkType::Type networkType, QString language, const char* secret) + : type(type) + , networkType(networkType) + , language(std::move(language)) { // We only support the creation of Polyseeds if (this->type != Type::POLYSEED) { @@ -23,7 +25,12 @@ Seed::Seed(Type type, NetworkType::Type networkType, QString language) try { polyseed::data seed(POLYSEED_MONERO); - seed.create(0); + + if (secret) { + seed.create_from_secret(0, secret); + } else { + seed.create(0); + } uint8_t key[32]; seed.keygen(&key, sizeof(key)); @@ -129,7 +136,6 @@ void Seed::setRestoreHeight(int height) { void Seed::setRestoreHeight() { // Ignore the embedded restore date, new wallets should sync from the current block height. this->restoreHeight = appData()->restoreHeights[networkType]->dateToHeight(this->time); - int a = 0; } Seed::Seed() = default; \ No newline at end of file diff --git a/src/utils/Seed.h b/src/utils/Seed.h index f306a27..6d0a868 100644 --- a/src/utils/Seed.h +++ b/src/utils/Seed.h @@ -41,7 +41,7 @@ struct Seed { bool encrypted = false; explicit Seed(); - explicit Seed(Type type, NetworkType::Type networkType = NetworkType::MAINNET, QString language = "English"); + explicit Seed(Type type, NetworkType::Type networkType = NetworkType::MAINNET, QString language = "English", const char* secret = nullptr); explicit Seed(Type type, QStringList mnemonic, NetworkType::Type networkType = NetworkType::MAINNET); void setRestoreHeight(int height); diff --git a/src/wizard/PageWalletSeed.cpp b/src/wizard/PageWalletSeed.cpp index 212fbef..33a62b1 100644 --- a/src/wizard/PageWalletSeed.cpp +++ b/src/wizard/PageWalletSeed.cpp @@ -8,10 +8,13 @@ #include #include #include +#include +#include #include "constants.h" #include "Seed.h" #include "Icons.h" +#include "dialog/SeedDiceDialog.h" PageWalletSeed::PageWalletSeed(WizardFields *fields, QWidget *parent) : QWizardPage(parent) @@ -29,6 +32,15 @@ PageWalletSeed::PageWalletSeed(WizardFields *fields, QWidget *parent) "Please contact the developers immediately."); ui->frame_invalidSeed->hide(); + QShortcut *shortcut = new QShortcut(QKeySequence("Ctrl+K"), this); + QObject::connect(shortcut, &QShortcut::activated, [&](){ + SeedDiceDialog dialog{this}; + int r = dialog.exec(); + if (r == QDialog::Accepted) { + this->generateSeed(dialog.getSecret()); + } + }); + connect(ui->btnRoulette, &QPushButton::clicked, [=]{ this->seedRoulette(0); }); @@ -55,10 +67,10 @@ void PageWalletSeed::seedRoulette(int count) { }); } -void PageWalletSeed::generateSeed() { +void PageWalletSeed::generateSeed(const char* secret) { QString mnemonic; - m_seed = Seed(Seed::Type::POLYSEED, constants::networkType); + m_seed = Seed(Seed::Type::POLYSEED, constants::networkType, "English", secret); mnemonic = m_seed.mnemonic.join(" "); m_restoreHeight = m_seed.restoreHeight; @@ -99,6 +111,7 @@ void PageWalletSeed::onOptionsClicked() { QCheckBox checkbox("Extend this seed with a passphrase"); checkbox.setChecked(m_fields->showSetSeedPassphrasePage); layout.addWidget(&checkbox); + QDialogButtonBox buttons(QDialogButtonBox::Ok); layout.addWidget(&buttons); dialog.setLayout(&layout); @@ -117,12 +130,16 @@ int PageWalletSeed::nextId() const { } bool PageWalletSeed::validatePage() { - if (m_seed.mnemonic.isEmpty()) return false; - if (!m_restoreHeight) return false; + if (m_seed.mnemonic.isEmpty()) { + return false; + } + if (!m_restoreHeight) { + return false; + } QMessageBox seedWarning(this); seedWarning.setWindowTitle("Warning!"); - seedWarning.setInformativeText("• Never disclose your seed\n" + seedWarning.setText("• Never disclose your seed\n" "• Never type it on a website\n" "• Store it safely (offline)\n" "• Do not lose your seed!"); diff --git a/src/wizard/PageWalletSeed.h b/src/wizard/PageWalletSeed.h index 8979478..63c89bd 100644 --- a/src/wizard/PageWalletSeed.h +++ b/src/wizard/PageWalletSeed.h @@ -30,7 +30,7 @@ public slots: private: void seedRoulette(int count); - void generateSeed(); + void generateSeed(const char* secret = nullptr); void onOptionsClicked(); signals: