--- /dev/null
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: 2020-2023 The Monero Project
+
+#include "SeedDiceDialog.h"
+#include "ui_SeedDiceDialog.h"
+
+#include <cmath>
+#include <algorithm>
+
+#include <QPasswordDigestor>
+
+#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
--- /dev/null
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: 2020-2023 The Monero Project
+
+#include <QDialog>
+
+#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::SeedDiceDialog> ui;
+ QStringList m_rolls;
+ QByteArray m_key;
+ int entropyNeeded = 152; // Polyseed requests 19 bytes of random data
+ QString m_mnemonic;
+};
+
+
+#endif //FEATHER_SEEDDICEDIALOG_H
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>SeedDiceDialog</class>
+ <widget class="QDialog" name="SeedDiceDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>881</width>
+ <height>547</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Diceware</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QGroupBox" name="groupBox">
+ <property name="title">
+ <string>Select method:</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_4">
+ <item>
+ <widget class="QRadioButton" name="radio_dice">
+ <property name="text">
+ <string>Dice rolls</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QSpinBox" name="spin_sides">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimum">
+ <number>2</number>
+ </property>
+ <property name="maximum">
+ <number>100</number>
+ </property>
+ <property name="value">
+ <number>6</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_4">
+ <property name="text">
+ <string>-sided die</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="radio_coinflip">
+ <property name="text">
+ <string>Coinflips</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="Line" name="line">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QFormLayout" name="formLayout">
+ <property name="fieldGrowthPolicy">
+ <enum>QFormLayout::ExpandingFieldsGrow</enum>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="label_2">
+ <property name="text">
+ <string>Entropy: </string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="label_entropy">
+ <property name="text">
+ <string>0 bits</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label_rollsLeft2">
+ <property name="text">
+ <string>Rolls left:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="label_rollsLeft">
+ <property name="text">
+ <string>0 rolls</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_rolls">
+ <property name="text">
+ <string>Rolls:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QPlainTextEdit" name="rolls">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>500</width>
+ <height>0</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Minimum</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>10</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QFrame" name="frame_dice">
+ <property name="frameShape">
+ <enum>QFrame::StyledPanel</enum>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Raised</enum>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QLabel" name="label_5">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Enter roll:</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="line_roll">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="btn_next">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Next roll</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_6">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>(Use a space between each roll to enter multiple rolls in one go)</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QFrame" name="frame_coinflip">
+ <property name="frameShape">
+ <enum>QFrame::StyledPanel</enum>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Raised</enum>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout_5">
+ <item>
+ <widget class="QLabel" name="label_7">
+ <property name="text">
+ <string>Add flip:</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="btn_heads">
+ <property name="text">
+ <string>Heads</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="btn_tails">
+ <property name="text">
+ <string>Tails</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <item>
+ <widget class="QPushButton" name="btn_reset">
+ <property name="text">
+ <string>Reset</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="btn_cancel">
+ <property name="text">
+ <string>Cancel</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="btn_createPolyseed">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Create polyseed</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
#include <QString>
+#define POLYSEED_RANDBYTES 19
+
namespace polyseed {
static std::locale locale;
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<language> languages;
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;
}
}
+ 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);
}
void create(feature_type features);
+ void create_from_secret(feature_type features, const char* secret);
void load(polyseed_storage storage);
#include <iomanip>
#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) {
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));
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
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);
#include <QMessageBox>
#include <QCheckBox>
#include <QDialogButtonBox>
+#include <QPushButton>
+#include <QShortcut>
#include "constants.h"
#include "Seed.h"
#include "Icons.h"
+#include "dialog/SeedDiceDialog.h"
PageWalletSeed::PageWalletSeed(WizardFields *fields, QWidget *parent)
: QWizardPage(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);
});
});
}
-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;
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);
}
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!");
private:
void seedRoulette(int count);
- void generateSeed();
+ void generateSeed(const char* secret = nullptr);
void onOptionsClicked();
signals: