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 : }
|