wizard: add diceware support

This commit is contained in:
tobtoht 2023-11-25 00:54:01 +01:00
parent c23869cc32
commit 6225e0e389
No known key found for this signature in database
GPG key ID: E45B10DD027D2472
9 changed files with 583 additions and 12 deletions

View file

@ -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 <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;

View file

@ -0,0 +1,47 @@
// 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

View file

@ -0,0 +1,292 @@
<?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>

View file

@ -13,6 +13,8 @@
#include <QString>
#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<language> 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);

View file

@ -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);

View file

@ -4,8 +4,10 @@
#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) {
@ -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;

View file

@ -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);

View file

@ -8,10 +8,13 @@
#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)
@ -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!");

View file

@ -30,7 +30,7 @@ public slots:
private:
void seedRoulette(int count);
void generateSeed();
void generateSeed(const char* secret = nullptr);
void onOptionsClicked();
signals: