QtPass 2.0.0
Developer documentation
Loading...
Searching...
No Matches
keygendialog.cpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2015 Anne Jan Brouwer
2// SPDX-License-Identifier: GPL-3.0-or-later
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
24KeygenDialog::KeygenDialog(const QString &gpgExe, Pass *pass, QWidget *parent)
25 : QDialog(parent), ui(new Ui::KeygenDialog), m_pass(pass),
26 m_progressIndicator(nullptr) {
27 ui->setupUi(this);
28 connect(ui->checkBox, &QCheckBox::toggled, this,
30 connect(ui->noPassphrase, &QCheckBox::toggled, this,
33
34 WindowStateStore::attach(*this, QStringLiteral("keygenDialog"));
35
36 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 ui->plainTextEdit->setVisible(ui->checkBox->isChecked());
41}
42
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.
49 Q_UNUSED(arg1)
51}
52
53auto KeygenDialog::passphraseChosen() const -> bool {
54 const QString passphrase = ui->passphrase1->text();
55 const bool agreed = passphrase == ui->passphrase2->text();
56 return agreed && (!passphrase.isEmpty() || ui->noPassphrase->isChecked());
57}
58
60 ui->buttonBox->setEnabled(passphraseChosen());
61}
62
64 if (checked) {
65 ui->passphrase1->clear();
66 ui->passphrase2->clear();
67 }
68 ui->passphrase1->setEnabled(!checked);
69 ui->passphrase2->setEnabled(!checked);
71}
72
76
77void KeygenDialog::setExpertMode(bool checked) {
78 ui->plainTextEdit->setReadOnly(!checked);
79 ui->plainTextEdit->setEnabled(checked);
80 ui->plainTextEdit->setVisible(checked);
81}
82
83void KeygenDialog::on_email_textChanged(const QString &arg1) {
84 replace("Name-Email", arg1);
85}
86
87void KeygenDialog::on_name_textChanged(const QString &arg1) {
88 replace("Name-Real", arg1);
89}
90
91void 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 if (!isBatchValue(value)) {
96 return;
97 }
98 QStringList clear;
99 QString expert = ui->plainTextEdit->toPlainText();
100 const QStringList lines =
101 expert.split(Util::newLinesRegex(), Qt::SkipEmptyParts);
102 for (QString line : lines) {
103 line.replace(QRegularExpression(key + ":.*"), key + ": " + value);
104 clear.append(line);
105 }
106 ui->plainTextEdit->setPlainText(clear.join("\n"));
107}
108
119static bool isControlStatement(const QString &line, const QString &control) {
120 const QString trimmed = line.trimmed();
121 if (!trimmed.startsWith(control, Qt::CaseInsensitive))
122 return false;
123 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.
130QString KeygenDialog::applyPassphrase(const QString &batch,
131 const QString &passphrase) {
132 static const QString noProtection = QStringLiteral("%no-protection");
133 static const QString commit = QStringLiteral("%commit");
134 static const QString passphraseKey = QStringLiteral("Passphrase:");
135 const QString protection =
136 passphrase.isEmpty() ? noProtection : passphraseKey + " " + passphrase;
137
138 QStringList clear;
139 bool spliced = false;
140 const QStringList lines =
141 batch.split(Util::newLinesRegex(), Qt::SkipEmptyParts);
142 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 if (const qsizetype nul = line.indexOf(QChar::Null); nul >= 0) {
147 line.truncate(nul);
148 }
149 if (isControlStatement(line, noProtection) ||
150 line.trimmed().startsWith(passphraseKey, Qt::CaseInsensitive)) {
151 if (!spliced) {
152 clear.append(protection);
153 spliced = true;
154 }
155 continue;
156 }
157 clear.append(line);
158 }
159
160 if (!spliced && !passphrase.isEmpty()) {
161 // Parameters must precede %commit; append when the template has none.
162 const auto commitLine =
163 std::find_if(clear.cbegin(), clear.cend(), [](const QString &line) {
164 return isControlStatement(line, commit);
165 });
166 clear.insert(commitLine - clear.cbegin(), protection);
167 }
168 return clear.join("\n");
169}
170
171auto KeygenDialog::isBatchValue(const QString &value) -> bool {
172 return !value.contains(QLatin1Char('\n')) &&
173 !value.contains(QLatin1Char('\r')) && !value.contains(QChar::Null);
174}
175
177 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 if (!passphraseChosen()) {
183 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 if (!isBatchValue(ui->name->text()) ||
189 !isBatchValue(ui->passphrase1->text())) {
190 QMessageBox::critical(this, tr("Invalid input"),
191 tr("The name and the passphrase cannot contain "
192 "line breaks."));
193 return;
194 }
195 if (ui->name->text().length() < 5) {
196 QMessageBox::critical(this, tr("Invalid name"),
197 tr("Name must be at least 5 characters long."));
198 return;
199 }
200
201 static const QRegularExpression mailre(
202 QRegularExpression::anchoredPattern(
203 R"(\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b)"),
204 QRegularExpression::CaseInsensitiveOption);
205 if (!mailre.match(ui->email->text()).hasMatch()) {
206 QMessageBox::critical(
207 this, tr("Invalid email"),
208 tr("The email address you typed is not a valid email address."));
209 return;
210 }
211
212 ui->widget->setEnabled(false);
213 ui->buttonBox->setEnabled(false);
214 ui->checkBox->setEnabled(false);
215 ui->plainTextEdit->setEnabled(false);
216
217 if (!m_progressIndicator) {
218 m_progressIndicator = std::make_unique<QProgressIndicator>();
219 m_progressIndicator->setParent(this);
220 m_progressIndicator->setSizePolicy(QSizePolicy::Expanding,
221 QSizePolicy::Expanding);
222 this->layout()->addWidget(m_progressIndicator.get());
223 }
224 // A retry after generationFailed() reuses the indicator it stopped and hid.
225 m_progressIndicator->show();
226 m_progressIndicator->startAnimation();
227
228 ui->frame->hide();
229 ui->label->setText(
230 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 this->show();
237 startGeneration(applyPassphrase(ui->plainTextEdit->toPlainText(),
238 ui->passphrase1->text()));
239 } else {
240 QObject::disconnect(m_finished);
241 QObject::disconnect(m_failed);
242 QDialog::done(r);
243 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.
249void KeygenDialog::startGeneration(const QString &batch) {
250 if (m_pass == nullptr) {
251 generationFailed(tr("No password store backend available"));
252 return;
253 }
255 [this](const QString &, const QString &) {
256 QObject::disconnect(m_finished);
257 QObject::disconnect(m_failed);
258 QDialog::done(QDialog::Accepted);
259 });
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.
264 [this](const QString &error) {
265 QObject::disconnect(m_finished);
266 QObject::disconnect(m_failed);
267 generationFailed(error);
268 });
269 m_pass->GenerateGPGKeys(batch);
270}
271
272void KeygenDialog::generationFailed(const QString &error) {
274 m_progressIndicator->stopAnimation();
275 m_progressIndicator->hide();
276 }
277 ui->frame->show();
278 ui->widget->setEnabled(true);
279 ui->checkBox->setEnabled(true);
280 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.
284 ui->label->setText(
285 tr("Key generation failed: %1").arg(error.toHtmlEscaped()));
286}
static QString applyPassphrase(const QString &batch, const QString &passphrase)
Splice a passphrase into a GPG batch template.
void replace(const QString &key, const QString &value)
Rewrites every <key>: ... line of the batch template to carry value. Values that fail isBatchValue() ...
std::unique_ptr< QProgressIndicator > m_progressIndicator
Spinner shown while gpg works; created lazily on the first attempt, reused (hidden) across retries.
KeygenDialog(const QString &gpgExe, Pass *pass, QWidget *parent=nullptr)
Construct a KeygenDialog.
void setExpertMode(bool checked)
Enable or disable expert mode (the editable batch template).
void setNoPassphrase(bool checked)
Ticking "no passphrase" clears and disables the passphrase fields and makes OK available; unticking i...
void on_passphrase1_textChanged(const QString &arg1)
First passphrase field edited: re-evaluates whether OK may be enabled. The text itself stays out of t...
void generationFailed(const QString &error)
Stops and hides the progress indicator, re-enables the form under the usual OK rule and shows error i...
void done(int r) override
On Accepted, validates the form (passphrase chosen, no line breaks, name of 5+ characters,...
QMetaObject::Connection m_failed
Connection to Pass::generateGPGKeysFailed, live only during an attempt.
void startGeneration(const QString &batch)
Connects to the backend's finished/failed signals for this one attempt and calls Pass::GenerateGPGKey...
void on_email_textChanged(const QString &arg1)
Mirrors the e-mail field into the template's Name-Email: line.
QScopedPointer< Ui::KeygenDialog > ui
Generated form (keygendialog.ui); owned.
~KeygenDialog() override
Destroys the dialog and its generated UI.
void updateOkState()
OK is available when a passphrase is typed twice the same, or when no passphrase was asked for explic...
Pass * m_pass
Backend that runs gpg; not owned, may be nullptr.
void on_name_textChanged(const QString &arg1)
Mirrors the name field into the template's Name-Real: line.
QMetaObject::Connection m_finished
Connection to Pass::finishedGenerateGPGKeys, live only during an attempt.
auto passphraseChosen() const -> bool
Whether the passphrase question has an answer: typed twice the same, or waived with the checkbox....
void on_passphrase2_textChanged(const QString &arg1)
Confirmation field edited: same as on_passphrase1_textChanged().
static auto isBatchValue(const QString &value) -> bool
Whether value can go into a GPG batch line as one value.
Abstract base class for password store operations.
Definition pass.h:75
void GenerateGPGKeys(QString batch)
Generate GPG keys using batch script.
Definition pass.cpp:394
static QString getDefaultKeyTemplate(const QString &gpgExecutable={})
Get default key template for new GPG keys.
Definition pass.cpp:291
void finishedGenerateGPGKeys(const QString &out, const QString &err)
GenerateGPGKeys() succeeded; the new key pair is in the keyring.
void generateGPGKeysFailed(const QString &err)
Emitted when GPG key generation fails, alongside the generic processErrorExit(); lets the keygen dial...
static auto newLinesRegex() -> const QRegularExpression &
Returns a regex to match newline characters.
Definition util.cpp:656
Settings dialog: every page of the configuration window and the profile table, with the validation th...
static bool isControlStatement(const QString &line, const QString &control)
Whether a batch line is the control statement control.
Dialog that collects name, e-mail and passphrase for a new GPG keypair and drives gpg --gen-key --bat...
void attach(QDialog &dialog, const QString &key)
restore() now, save() whenever the dialog finishes.
Pass, the abstract password-store backend that RealPass and ImitatePass implement,...
The "qtpass" logging category.
QtPassSettings, the QSettings singleton behind every saved setting, the profiles and the Pass backend...
Util: static helpers for the process environment, binary discovery, store paths, URL safety and the l...
WindowStateStore: saves and restores top-level window geometry through QtPassSettings,...