QtPass Developer documentation
QtPass test coverage
Current view: top level - src/keygendialog.cpp (source / functions) Coverage Total Hit
Test: QtPass Lines: 92.0 % 150 138
Test Date: 2026-11-14 13:19:18 Functions: 100.0 % 19 19

            Line data    Source code
       1              : // SPDX-FileCopyrightText: 2015 Anne Jan Brouwer
       2              : // SPDX-License-Identifier: GPL-3.0-or-later
       3              : /**
       4              :  * @file
       5              :  * @brief Implements KeygenDialog: form validation, splicing the passphrase
       6              :  * into the GPG batch the way gpg itself reads it, and the one-attempt
       7              :  * connections to the Pass backend.
       8              :  */
       9              : #include "keygendialog.h"
      10              : #include "configdialog.h"
      11              : #include "pass.h"
      12              : #include "qprogressindicator.h"
      13              : #include "qtpasssettings.h"
      14              : #include "ui_keygendialog.h"
      15              : #include "util.h"
      16              : #include "windowstatestore.h"
      17              : #include <QCheckBox>
      18              : #include <QMessageBox>
      19              : #include <QRegularExpression>
      20              : #include <algorithm>
      21              : 
      22              : #include "qtpasslogging.h"
      23              : 
      24           25 : KeygenDialog::KeygenDialog(const QString &gpgExe, Pass *pass, QWidget *parent)
      25           50 :     : QDialog(parent), ui(new Ui::KeygenDialog), m_pass(pass),
      26           25 :       m_progressIndicator(nullptr) {
      27           25 :   ui->setupUi(this);
      28           25 :   connect(ui->checkBox, &QCheckBox::toggled, this,
      29           25 :           &KeygenDialog::setExpertMode);
      30           25 :   connect(ui->noPassphrase, &QCheckBox::toggled, this,
      31           25 :           &KeygenDialog::setNoPassphrase);
      32           25 :   updateOkState();
      33              : 
      34           50 :   WindowStateStore::attach(*this, QStringLiteral("keygenDialog"));
      35              : 
      36           50 :   ui->plainTextEdit->setPlainText(Pass::getDefaultKeyTemplate(gpgExe));
      37              :   // The batch template is only of interest in expert mode; keep it out of
      38              :   // sight otherwise so the %no-protection placeholder cannot be mistaken for
      39              :   // the effective setting once a passphrase has been typed.
      40           25 :   ui->plainTextEdit->setVisible(ui->checkBox->isChecked());
      41           25 : }
      42              : 
      43           25 : KeygenDialog::~KeygenDialog() = default;
      44              : 
      45              : // The passphrase never goes into the template box: that widget is always
      46              : // readable, which would defeat the masked fields. applyPassphrase() splices it
      47              : // into the batch on accept.
      48           24 : void KeygenDialog::on_passphrase1_textChanged(const QString &arg1) {
      49              :   Q_UNUSED(arg1)
      50           24 :   updateOkState();
      51           24 : }
      52              : 
      53           73 : auto KeygenDialog::passphraseChosen() const -> bool {
      54           73 :   const QString passphrase = ui->passphrase1->text();
      55           73 :   const bool agreed = passphrase == ui->passphrase2->text();
      56          146 :   return agreed && (!passphrase.isEmpty() || ui->noPassphrase->isChecked());
      57              : }
      58              : 
      59           63 : void KeygenDialog::updateOkState() {
      60           63 :   ui->buttonBox->setEnabled(passphraseChosen());
      61           63 : }
      62              : 
      63           10 : void KeygenDialog::setNoPassphrase(bool checked) {
      64           10 :   if (checked) {
      65            8 :     ui->passphrase1->clear();
      66            8 :     ui->passphrase2->clear();
      67              :   }
      68           10 :   ui->passphrase1->setEnabled(!checked);
      69           10 :   ui->passphrase2->setEnabled(!checked);
      70           10 :   updateOkState();
      71           10 : }
      72              : 
      73           12 : void KeygenDialog::on_passphrase2_textChanged(const QString &arg1) {
      74           12 :   on_passphrase1_textChanged(arg1);
      75           12 : }
      76              : 
      77            4 : void KeygenDialog::setExpertMode(bool checked) {
      78            4 :   ui->plainTextEdit->setReadOnly(!checked);
      79            4 :   ui->plainTextEdit->setEnabled(checked);
      80            4 :   ui->plainTextEdit->setVisible(checked);
      81            4 : }
      82              : 
      83           11 : void KeygenDialog::on_email_textChanged(const QString &arg1) {
      84           11 :   replace("Name-Email", arg1);
      85           11 : }
      86              : 
      87           11 : void KeygenDialog::on_name_textChanged(const QString &arg1) {
      88           11 :   replace("Name-Real", arg1);
      89           11 : }
      90              : 
      91           22 : void KeygenDialog::replace(const QString &key, const QString &value) {
      92              :   // A pasted line break would put batch directives of its own into the
      93              :   // template, and correcting the field later would leave them there:
      94              :   // nothing such goes in. done() refuses the field itself.
      95           22 :   if (!isBatchValue(value)) {
      96            2 :     return;
      97              :   }
      98           20 :   QStringList clear;
      99           20 :   QString expert = ui->plainTextEdit->toPlainText();
     100              :   const QStringList lines =
     101           20 :       expert.split(Util::newLinesRegex(), Qt::SkipEmptyParts);
     102          260 :   for (QString line : lines) {
     103          720 :     line.replace(QRegularExpression(key + ":.*"), key + ": " + value);
     104              :     clear.append(line);
     105              :   }
     106           40 :   ui->plainTextEdit->setPlainText(clear.join("\n"));
     107              : }
     108              : 
     109              : /**
     110              :  * @brief Whether a batch line is the control statement @p control.
     111              :  *
     112              :  * Matches the way gpg's read_parameter_file does: leading whitespace skipped,
     113              :  * keyword ends at the first whitespace, case-insensitive.
     114              :  * @param line One line of the batch template.
     115              :  * @param control The statement to look for, e.g. `%no-protection`.
     116              :  * @return true when the line is that statement, alone or followed by
     117              :  *         whitespace.
     118              :  */
     119          161 : static bool isControlStatement(const QString &line, const QString &control) {
     120              :   const QString trimmed = line.trimmed();
     121          161 :   if (!trimmed.startsWith(control, Qt::CaseInsensitive))
     122              :     return false;
     123           21 :   return trimmed.size() == control.size() ||
     124              :          trimmed.at(control.size()).isSpace();
     125              : }
     126              : 
     127              : // Keywords are matched as gpg matches them: an expert's "%No-Protection" left
     128              : // next to the Passphrase: line makes gpg generate an unprotected key anyway.
     129              : // The value is spliced in as a plain string, never through a regex.
     130           26 : QString KeygenDialog::applyPassphrase(const QString &batch,
     131              :                                       const QString &passphrase) {
     132           29 :   static const QString noProtection = QStringLiteral("%no-protection");
     133           27 :   static const QString commit = QStringLiteral("%commit");
     134           27 :   static const QString passphraseKey = QStringLiteral("Passphrase:");
     135              :   const QString protection =
     136           26 :       passphrase.isEmpty() ? noProtection : passphraseKey + " " + passphrase;
     137              : 
     138           26 :   QStringList clear;
     139              :   bool spliced = false;
     140              :   const QStringList lines =
     141           26 :       batch.split(Util::newLinesRegex(), Qt::SkipEmptyParts);
     142          177 :   for (QString line : lines) {
     143              :     // gpg reads each line as a C string, so it ends at a NUL: what follows
     144              :     // one would hide "%no-protection" from the check below and not from
     145              :     // gpg. Cut it here as gpg does.
     146          151 :     if (const qsizetype nul = line.indexOf(QChar::Null); nul >= 0) {
     147            1 :       line.truncate(nul);
     148              :     }
     149          172 :     if (isControlStatement(line, noProtection) ||
     150          286 :         line.trimmed().startsWith(passphraseKey, Qt::CaseInsensitive)) {
     151           21 :       if (!spliced) {
     152              :         clear.append(protection);
     153              :         spliced = true;
     154              :       }
     155              :       continue;
     156              :     }
     157              :     clear.append(line);
     158              :   }
     159              : 
     160           26 :   if (!spliced && !passphrase.isEmpty()) {
     161              :     // Parameters must precede %commit; append when the template has none.
     162              :     const auto commitLine =
     163            5 :         std::find_if(clear.cbegin(), clear.cend(), [](const QString &line) {
     164           10 :           return isControlStatement(line, commit);
     165              :         });
     166              :     clear.insert(commitLine - clear.cbegin(), protection);
     167              :   }
     168           78 :   return clear.join("\n");
     169              : }
     170              : 
     171           43 : auto KeygenDialog::isBatchValue(const QString &value) -> bool {
     172           83 :   return !value.contains(QLatin1Char('\n')) &&
     173           43 :          !value.contains(QLatin1Char('\r')) && !value.contains(QChar::Null);
     174              : }
     175              : 
     176           14 : void KeygenDialog::done(int r) {
     177           14 :   if (QDialog::Accepted == r) {
     178              :     // The button is disabled without a passphrase choice, but done() can be
     179              :     // reached otherwise (a default button, a script); the rule holds here.
     180              :     // No dialog: the disabled OK already says what is missing.
     181           10 :     if (!passphraseChosen()) {
     182            2 :       updateOkState();
     183            2 :       return;
     184              :     }
     185              :     // Pasted text with a line break would become batch directives of its
     186              :     // own (replace() kept it out of the template); the email is matched
     187              :     // whole by its pattern below.
     188           24 :     if (!isBatchValue(ui->name->text()) ||
     189           16 :         !isBatchValue(ui->passphrase1->text())) {
     190            0 :       QMessageBox::critical(this, tr("Invalid input"),
     191            0 :                             tr("The name and the passphrase cannot contain "
     192              :                                "line breaks."));
     193            0 :       return;
     194              :     }
     195           16 :     if (ui->name->text().length() < 5) {
     196            0 :       QMessageBox::critical(this, tr("Invalid name"),
     197            0 :                             tr("Name must be at least 5 characters long."));
     198            0 :       return;
     199              :     }
     200              : 
     201              :     static const QRegularExpression mailre(
     202            1 :         QRegularExpression::anchoredPattern(
     203            2 :             R"(\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b)"),
     204           11 :         QRegularExpression::CaseInsensitiveOption);
     205           16 :     if (!mailre.match(ui->email->text()).hasMatch()) {
     206            0 :       QMessageBox::critical(
     207            0 :           this, tr("Invalid email"),
     208            0 :           tr("The email address you typed is not a valid email address."));
     209            0 :       return;
     210              :     }
     211              : 
     212            8 :     ui->widget->setEnabled(false);
     213            8 :     ui->buttonBox->setEnabled(false);
     214            8 :     ui->checkBox->setEnabled(false);
     215            8 :     ui->plainTextEdit->setEnabled(false);
     216              : 
     217            8 :     if (!m_progressIndicator) {
     218           14 :       m_progressIndicator = std::make_unique<QProgressIndicator>();
     219            7 :       m_progressIndicator->setParent(this);
     220            7 :       m_progressIndicator->setSizePolicy(QSizePolicy::Expanding,
     221              :                                          QSizePolicy::Expanding);
     222            7 :       this->layout()->addWidget(m_progressIndicator.get());
     223              :     }
     224              :     // A retry after generationFailed() reuses the indicator it stopped and hid.
     225            8 :     m_progressIndicator->show();
     226            8 :     m_progressIndicator->startAnimation();
     227              : 
     228            8 :     ui->frame->hide();
     229            8 :     ui->label->setText(
     230            8 :         tr("This operation can take some minutes.<br />"
     231              :            "We need to generate a lot of random bytes. It is a good idea to "
     232              :            "perform some other action (type on the keyboard, move the mouse, "
     233              :            "utilize the disks) during the prime generation; this gives the "
     234              :            "random number generator a better chance to gain enough entropy."));
     235              : 
     236            8 :     this->show();
     237           16 :     startGeneration(applyPassphrase(ui->plainTextEdit->toPlainText(),
     238           16 :                                     ui->passphrase1->text()));
     239              :   } else {
     240            4 :     QObject::disconnect(m_finished);
     241            4 :     QObject::disconnect(m_failed);
     242            4 :     QDialog::done(r);
     243            4 :     return;
     244              :   }
     245              : }
     246              : 
     247              : // Success accepts the dialog, so an exec() caller (the first-run wizard's
     248              : // checkSecretKeys()) sees Accepted; failure gives the form back.
     249            8 : void KeygenDialog::startGeneration(const QString &batch) {
     250            8 :   if (m_pass == nullptr) {
     251            0 :     generationFailed(tr("No password store backend available"));
     252            0 :     return;
     253              :   }
     254            8 :   m_finished = connect(m_pass, &Pass::finishedGenerateGPGKeys, this,
     255            8 :                        [this](const QString &, const QString &) {
     256            2 :                          QObject::disconnect(m_finished);
     257            2 :                          QObject::disconnect(m_failed);
     258            2 :                          QDialog::done(QDialog::Accepted);
     259            2 :                        });
     260              :   // Not processErrorExit: that fires for any command that fails while the
     261              :   // key is being generated (a queued git push, say) and would take the
     262              :   // dialog down while gpg is still working.
     263            8 :   m_failed = connect(m_pass, &Pass::generateGPGKeysFailed, this,
     264            8 :                      [this](const QString &error) {
     265            2 :                        QObject::disconnect(m_finished);
     266            2 :                        QObject::disconnect(m_failed);
     267            2 :                        generationFailed(error);
     268            2 :                      });
     269           16 :   m_pass->GenerateGPGKeys(batch);
     270              : }
     271              : 
     272            2 : void KeygenDialog::generationFailed(const QString &error) {
     273            2 :   if (m_progressIndicator) {
     274            2 :     m_progressIndicator->stopAnimation();
     275            2 :     m_progressIndicator->hide();
     276              :   }
     277            2 :   ui->frame->show();
     278            2 :   ui->widget->setEnabled(true);
     279            2 :   ui->checkBox->setEnabled(true);
     280            2 :   ui->plainTextEdit->setEnabled(true);
     281              :   // OK comes back under the same rule as before the attempt, not
     282              :   // unconditionally: the passphrase choice still has to stand.
     283            2 :   updateOkState();
     284            4 :   ui->label->setText(
     285            4 :       tr("Key generation failed: %1").arg(error.toHtmlEscaped()));
     286            2 : }
        

Generated by: LCOV version 2.0-1