]> Nutra Git (v1) - gamesguru/feather.git/commitdiff
wizard: add diceware support
authortobtoht <tob@featherwallet.org>
Fri, 24 Nov 2023 23:54:01 +0000 (00:54 +0100)
committertobtoht <tob@featherwallet.org>
Fri, 24 Nov 2023 23:54:01 +0000 (00:54 +0100)
src/dialog/SeedDiceDialog.cpp [new file with mode: 0644]
src/dialog/SeedDiceDialog.h [new file with mode: 0644]
src/dialog/SeedDiceDialog.ui [new file with mode: 0644]
src/polyseed/polyseed.cpp
src/polyseed/polyseed.h
src/utils/Seed.cpp
src/utils/Seed.h
src/wizard/PageWalletSeed.cpp
src/wizard/PageWalletSeed.h

diff --git a/src/dialog/SeedDiceDialog.cpp b/src/dialog/SeedDiceDialog.cpp
new file mode 100644 (file)
index 0000000..f0bb4ca
--- /dev/null
@@ -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;
\ No newline at end of file
diff --git a/src/dialog/SeedDiceDialog.h b/src/dialog/SeedDiceDialog.h
new file mode 100644 (file)
index 0000000..228e03b
--- /dev/null
@@ -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
diff --git a/src/dialog/SeedDiceDialog.ui b/src/dialog/SeedDiceDialog.ui
new file mode 100644 (file)
index 0000000..4bc46fc
--- /dev/null
@@ -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>
index 9074687528b64d4d25bdbdbee750ae1767d2eae7..ba100bed4c17bb34c3c6f27ddd40f7ed8d661dd3 100644 (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);
index e676b475817a29208ea9be0b6ee12d5e8c004c86..441b9e37fb333462ee109464d9e747b6a00aa8f3 100644 (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);
 
index e916550fe65351757fa2420212cd3c104505fce2..f18544513b0992dcd7c00a470802ed118de8736a 100644 (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;
\ No newline at end of file
index f306a2779b744fff88e2a9dd45c0df76077a6436..6d0a86824b470bf60c00be5549ac6fcf952bd3be 100644 (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);
 
index 212fbef91c57234d07cb42518b4833b5c27be2d6..33a62b167fc219dc9f779076ae4e0e64e076811a 100644 (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!");
index 89794789297c6ea1c5c3b6120fe4b7c08d5dc05b..63c89bde787c39e6fd8fc4037b4985fa6eae7a31 100644 (file)
@@ -30,7 +30,7 @@ public slots:
 
 private:
     void seedRoulette(int count);
-    void generateSeed();
+    void generateSeed(const char* secret = nullptr);
     void onOptionsClicked();
 
 signals: