QtPass 2.0.0
Developer documentation
Loading...
Searching...
No Matches
passworddialog.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 "passworddialog.h"
10#include "fieldlabel.h"
11#include "filecontent.h"
12#include "pass.h"
14#include "pathvalidator.h"
15#include "qtpasssettings.h"
16#include "secretlineedit.h"
17#include "secretplaintextedit.h"
18#include "templateio.h"
19#include "totp.h"
20#include "ui_passworddialog.h"
21#include "util.h"
22#include <algorithm>
23#include <limits>
24
25#include <QAction>
26#include <QCheckBox>
27#include <QComboBox>
28#include <QDialogButtonBox>
29#include <QDir>
30#include <QFileInfo>
31#include <QFormLayout>
32#include <QHash>
33#include <QIcon>
34#include <QLabel>
35#include <QLineEdit>
36#include <QMessageBox>
37#include <QPushButton>
38#include <QShortcut>
39#include <QSignalBlocker>
40#include <QTextDocument>
41#include <utility>
42
43#include "qtpasslogging.h"
44
45namespace {
48const char kOtpEditedProperty[] = "qtpassOtpEdited";
51const char kOriginalLineProperty[] = "qtpassOriginalLine";
52} // namespace
53
55 QWidget *parent)
56 : QDialog(parent), ui(new Ui::PasswordDialog),
57 m_passConfig(std::move(passConfig)), m_pass(QtPassSettings::getPass()) {
58 // Only reached from tests; production uses the overload taking an
59 // explicit Pass* with a caller-controlled lifetime.
60 m_templating = false;
61 m_isNew = false;
62
63 ui->setupUi(this);
64 connect(ui->checkBoxShow, &QCheckBox::toggled, this,
66 reportCopies(ui->lineEditPassword);
67 connect(ui->plainTextEdit, &SecretPlainTextEdit::copied, this,
72
74 connect(m_pass, &Pass::finishedInsert, this,
77}
78
79PasswordDialog::PasswordDialog(Pass *pass, const AppSettings &s, QString file,
80 const bool &isNew, QWidget *parent)
81 : QDialog(parent), ui(new Ui::PasswordDialog), m_pass(pass),
82 m_file(std::move(file)), m_isNew(isNew) {
83
84 ui->setupUi(this);
85 connect(ui->checkBoxShow, &QCheckBox::toggled, this,
87 reportCopies(ui->lineEditPassword);
88 connect(ui->plainTextEdit, &SecretPlainTextEdit::copied, this,
91 // The name row only appears once setNewEntryLocation() says where a new
92 // entry may go; an existing entry keeps its name.
93 ui->nameRow->hide();
94
95 setWindowTitle(isNew && m_file.isEmpty()
96 ? tr("New password")
97 : this->windowTitle() + " " + m_file);
102
105
108 connect(m_pass, &Pass::finishedInsert, this,
111 connect(this, &PasswordDialog::rejected, this, &PasswordDialog::on_rejected);
112
113 if (!isNew) {
114 // Show() is asynchronous: until finishedShow lands the fields are empty,
115 // so an early Ok would write nothing and the late setPass() would clobber
116 // what was typed. Lock the editor and Ok until the decrypt arrives.
117 ui->statusLabel->setText(tr("Decrypting…"));
118 setEditorEnabled(false);
119 ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
121 }
122}
123
125 // QDialog's destructor hides the window after this body, and losing focus
126 // emits editingFinished() into normalizeOtpField() or renameField(), slots
127 // of a PasswordDialog already gone. Hide here, while the object is whole.
128 hide();
129}
130
132 if (show) {
133 ui->lineEditPassword->setEchoMode(QLineEdit::Normal);
134 } else {
135 ui->lineEditPassword->setEchoMode(QLineEdit::Password);
136 }
137}
138
140 ui->widget->setEnabled(false);
141 const int currentIndex = ui->passwordTemplateSwitch->currentIndex();
142 if (currentIndex < 0 ||
143 currentIndex >= static_cast<int>(PasswordConfiguration::CHARSETS_COUNT)) {
144 ui->widget->setEnabled(true);
145 return;
146 }
147
148 QString newPass = m_pass->generatePassword(
149 static_cast<unsigned int>(ui->spinBox_pwdLength->value()),
151 currentIndex)]);
152 if (!newPass.isEmpty()) {
153 ui->lineEditPassword->setText(newPass);
154 }
155 ui->widget->setEnabled(true);
156}
157
159 // Show is asynchronous: refuse to save an existing entry before its
160 // content loads. getPassword() always returns at least a newline, so an
161 // isEmpty() check cannot stand in for this.
162 if (!m_isNew && !m_contentLoaded) {
163 return;
164 }
165
166 // Canonicalise before serialising: getPassword() stays a dumb serializer.
168
169 QString newValue = getPassword();
170 if (newValue.right(1) != "\n") {
171 newValue += "\n";
172 }
173
174 setSaving(true);
175 // A refusal (the name is taken, the .gpg-id is unusable, a linked path)
176 // comes through critical() before Insert() returns, with nothing started
177 // that could still report.
178 QString refusal;
179 const QMetaObject::Connection refused =
180 connect(m_pass, &Pass::critical, this,
181 [&refusal](const QString &, const QString &message) {
182 refusal = message;
183 });
184 // Reports from inserts already queued (an earlier dialog that a hanging
185 // queue let close while its save was still pending) must pass before one is
186 // taken as this dialog's own. The count is read before our own Insert()
187 // queues, so it does not include it.
189 m_pass->Insert(m_file, newValue, !m_isNew);
190 disconnect(refused);
191 if (m_saving && !refusal.isEmpty()) {
192 setSaving(false);
193 showNotSaved(refusal);
194 }
195}
196
197void PasswordDialog::setSaving(bool saving) {
198 m_saving = saving;
199 setEditorEnabled(!saving);
200 ui->nameRow->setEnabled(!saving);
201 // Another template would rebuild the field rows under a pending insert.
202 ui->templateBox->setEnabled(!saving);
203 // Cancel stays: a save that never reports (an unanswered PIN prompt, a
204 // queue held up by a hanging pull) must not leave quitting as the only
205 // way out. reject() asks first.
206 ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!saving);
207 ui->statusLabel->setText(saving ? tr("Saving…") : QString());
208}
209
211 if (!m_saving) {
212 return false;
213 }
214 // Since a hanging queue can let a dialog close while its insert is still
215 // queued (#1965), more than one insert can be in flight, and neither
216 // finishedInsert nor insertFailed says which it is. The Executor runs the
217 // queue in order, so the first m_insertsAhead reports belong to inserts
218 // queued before this dialog's; let them pass before taking one as our own.
219 if (m_insertsAhead > 0) {
221 return false;
222 }
223 return true;
224}
225
227 if (!isOwnInsertReport()) {
228 return;
229 }
230 setSaving(false);
231 QDialog::accept();
232}
233
234void PasswordDialog::onInsertFailed(const QString &err, bool written) {
235 if (!isOwnInsertReport()) {
236 return;
237 }
238 setSaving(false);
239 // The entry is in the store and a later step (git add or commit) failed,
240 // which the backend reports ("Saved, not committed"). Saved is saved, so
241 // the dialog closes, also for an existing entry: "Not saved" would be
242 // wrong, and a new entry could only be tried again under a name it now
243 // has.
244 if (written) {
245 QDialog::accept();
246 return;
247 }
248 showNotSaved(err);
249}
250
252 if (m_saving) {
253 const QMessageBox::StandardButton answer = QMessageBox::question(
254 this, tr("Still saving"),
255 tr("The entry is still being saved. Close the dialog anyway? If "
256 "saving then fails, what you typed is lost."),
257 QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
258 // The insert may have reported while the question was up.
259 if (answer != QMessageBox::Yes || !m_saving) {
260 return;
261 }
262 // Whatever the insert reports later, the main window shows it.
263 m_saving = false;
264 }
265 QDialog::reject();
266}
267
268void PasswordDialog::showNotSaved(const QString &err) {
269 if (!m_storeRoot.isEmpty()) {
270 validateNewEntry(); // gates OK on the name again
271 }
272 ui->statusLabel->setText(tr("Not saved: %1").arg(err.trimmed()));
273}
274
276
277void PasswordDialog::setNewEntryLocation(const QString &storeRoot,
278 const QStringList &folders,
279 const QString &currentFolder) {
280 m_storeRoot = QDir::cleanPath(storeRoot);
281 ui->folderBox->clear();
282 int current = 0;
283 for (const QString &folder : folders) {
284 const QString rel = QDir::cleanPath(folder) == QStringLiteral(".")
285 ? QString()
286 : QDir::cleanPath(folder);
287 if (rel == QDir::cleanPath(currentFolder) ||
288 (rel.isEmpty() &&
289 QDir::cleanPath(currentFolder) == QStringLiteral("."))) {
290 current = ui->folderBox->count();
291 }
292 ui->folderBox->addItem(rel.isEmpty() ? QStringLiteral("/") : rel + u'/',
293 rel);
294 }
295 ui->folderBox->setCurrentIndex(current);
296 m_newEntryFolder = ui->folderBox->currentData().toString();
297 ui->nameRow->show();
298 connect(ui->nameEdit, &QLineEdit::textChanged, this,
300 connect(ui->folderBox, &QComboBox::currentIndexChanged, this,
303 ui->nameEdit->setFocus();
304}
305
306auto PasswordDialog::resolveNewEntry(QString *problem) const -> QString {
307 QString name = QDir::fromNativeSeparators(ui->nameEdit->text().trimmed());
308 // The suffix belongs to the file, not the entry: Insert() appends it, so a
309 // typed "github.gpg" would otherwise become github.gpg.gpg.
310 if (name.endsWith(QStringLiteral(".gpg"), Qt::CaseInsensitive)) {
311 name.chop(4);
312 }
313 if (name.isEmpty()) {
314 *problem = tr("Give the entry a name.");
315 return {};
316 }
317 if (name.endsWith(u'/')) {
318 *problem = tr("A name cannot end in /.");
319 return {};
320 }
321 const QString folder = ui->folderBox->currentData().toString();
322 const QString rel =
323 QDir::cleanPath(folder.isEmpty() ? name : folder + u'/' + name);
324 const QString absolute = QDir(m_storeRoot).filePath(rel);
325 if (rel.isEmpty() || rel == QStringLiteral(".") ||
327 *problem = tr("That name would resolve outside the password store.");
328 return {};
329 }
330 if (QFileInfo::exists(absolute + QStringLiteral(".gpg"))) {
331 *problem = tr("An entry called %1 already exists.").arg(rel);
332 return {};
333 }
334 if (QFileInfo(absolute).isDir()) {
335 *problem = tr("%1 is a folder.").arg(rel);
336 return {};
337 }
338 problem->clear();
339 return rel;
340}
341
342// Re-checked on every keystroke; gates OK.
344 QString problem;
345 const QString rel = resolveNewEntry(&problem);
346 ui->statusLabel->setText(problem);
347 ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!rel.isEmpty());
348}
349
351 if (m_saving) {
352 return;
353 }
354 if (!m_storeRoot.isEmpty()) {
355 QString problem;
356 const QString rel = resolveNewEntry(&problem);
357 if (rel.isEmpty()) {
358 ui->statusLabel->setText(problem);
359 return;
360 }
361 // A name like work/vpn may need its folder; gpg does not create it.
362 const QString parent = QFileInfo(QDir(m_storeRoot).filePath(rel)).path();
363 if (!QDir(parent).exists() && !Util::mkpathForStore(parent)) {
364 ui->statusLabel->setText(
365 tr("Could not create the folder %1.").arg(parent));
366 return;
367 }
368 m_file = rel;
369 setWindowTitle(tr("Password") + QStringLiteral(" ") + m_file);
370 }
371 save();
372}
373
374void PasswordDialog::setPassword(const QString &password) {
375 // Which lines become fields is the user's choice: forcing allFields
376 // (#1138, for #132) made every `key: value` line a label-locked widget,
377 // even with templates off (#1766). MainWindow renders by the same rule.
378 FileContent fileContent =
379 FileContent::parse(password, m_templating ? m_fields : QStringList(),
381 ui->lineEditPassword->setText(fileContent.getPassword());
382
383 QWidget *previous = ui->checkBoxShow;
384 NamedValues namedValues = fileContent.getNamedValues();
385 for (QLineEdit *line : std::as_const(m_templateLines)) {
386 const NamedValue nv = namedValues.takeNamedValue(line->objectName());
387 line->setText(nv.value);
388 line->setProperty(kOriginalLineProperty, nv.originalLine);
389 // The value is the entry's again, whatever the user typed before this
390 // reload: not a value normalizeOtpField() may rewrite.
391 line->setProperty(kOtpEditedProperty, QVariant());
392 previous = line;
393 }
394 // Remove previously created dynamic widgets to prevent duplicates and leaks
395 for (QLineEdit *line : std::as_const(m_otherLines)) {
396 ui->formLayout->removeRow(line);
397 }
398 m_otherLines.clear();
399 for (const NamedValue &nv : std::as_const(namedValues)) {
400 auto *line = new SecretLineEdit();
401 reportCopies(line);
402 line->setObjectName(nv.name);
403 line->setText(nv.value);
404 line->setProperty(kOriginalLineProperty, nv.originalLine);
405 auto *label = new FieldLabel(nv.name);
406 label->setBuddy(line);
407 ui->formLayout->addRow(label, line);
408 setTabOrder(previous, line);
409 m_otherLines.append(line);
410 previous = line;
411 // Both widgets are rows of the form; removeRow() deletes them together,
412 // so a rename or removal that arrives afterwards finds no receiver.
413 connect(label, &FieldLabel::renamed, this,
414 [this, line, label](const QString &, const QString &to) {
415 renameField(line, label, to);
416 });
417 connect(label, &FieldLabel::removeRequested, this,
418 [this, line] { removeField(line); });
419 // The visible way to remove the field; the context menu is the other.
420 auto *remove = line->addAction(
421 QIcon::fromTheme(QStringLiteral("edit-delete"),
422 QIcon(QStringLiteral(":/icons/edit-delete.svg"))),
423 QLineEdit::TrailingPosition);
424 remove->setObjectName(QStringLiteral("removeFieldAction"));
425 remove->setToolTip(tr("Remove field"));
426 connect(remove, &QAction::triggered, this,
427 [this, line] { removeField(line); });
428 }
429
430 // setPlainText, not insertPlainText: re-populating must replace the body,
431 // not save back a duplicated copy.
432 ui->plainTextEdit->setPlainText(fileContent.getRemainingData());
433
434 // m_otherLines was just rebuilt, so the OTP field may be a new widget.
435 hookOtpField();
436}
437
438void PasswordDialog::renameField(QLineEdit *line, FieldLabel *label,
439 const QString &to) {
440 QList<QLineEdit *> allLines(m_templateLines);
441 allLines.append(m_otherLines);
442 for (QLineEdit *other : std::as_const(allLines)) {
443 if (other != line && other->objectName() == to) {
444 ui->statusLabel->setText(tr("A field called %1 already exists.").arg(to));
445 return;
446 }
447 }
448 ui->statusLabel->clear();
449 line->setObjectName(to);
450 label->setText(to);
451 // The field may have become, or stopped being, the OTP one.
452 hookOtpField();
453}
454
455void PasswordDialog::removeField(QLineEdit *line) {
456 m_otherLines.removeAll(line);
457 // takeRow(), not removeRow(): removeRow() deletes the widgets while the
458 // label's contextMenuEvent() or the action's triggered() is still on the
459 // stack. They are deleted once the stack has unwound.
460 const QFormLayout::TakeRowResult row = ui->formLayout->takeRow(line);
461 // hide() takes the focus from a focused line, and that emits
462 // editingFinished(): not into normalizeOtpField() for a field that is
463 // being removed.
464 disconnect(line, nullptr, this, nullptr);
465 if (m_otpLine == line) {
466 m_otpLine = nullptr;
467 }
468 for (QLayoutItem *item : {row.labelItem, row.fieldItem}) {
469 if (item == nullptr) {
470 continue;
471 }
472 if (QWidget *widget = item->widget()) {
473 widget->hide();
474 widget->deleteLater();
475 }
476 delete item;
477 }
478 hookOtpField();
479}
480
481auto PasswordDialog::otpLineEdit() const -> QLineEdit * {
482 QList<QLineEdit *> allLines(m_templateLines);
483 allLines.append(m_otherLines);
484 QLineEdit *firstMatch = nullptr;
485 for (QLineEdit *line : std::as_const(allLines)) {
486 if (!FileContent::isOtpFieldName(line->objectName())) {
487 continue;
488 }
489 // Prefer a populated field: the default template's empty `OTP` widget
490 // would otherwise shadow a `TOTP:` value in m_otherLines, leaving the
491 // real secret unvalidated and unnormalised.
492 if (!line->text().trimmed().isEmpty()) {
493 return line;
494 }
495 if (firstMatch == nullptr) {
496 firstMatch = line;
497 }
498 }
499 return firstMatch;
500}
501
503 // Only m_otherLines widgets are deleted by removeRow(); a template widget
504 // keeps its action, and the next validate would add a second icon.
505 // m_otpWarning is a QPointer: null when setPassword() deleted its owner.
506 if (!m_otpWarning.isNull()) {
507 if (auto *owner = qobject_cast<QWidget *>(m_otpWarning->parent())) {
508 owner->removeAction(m_otpWarning);
509 }
510 delete m_otpWarning;
511 }
512 m_otpWarning = nullptr;
513
514 QLineEdit *otp = otpLineEdit();
515 // The field that was the OTP one may not be any more (renamed away, or
516 // another one now comes first): it stops being validated and normalised
517 // here, so its OTP hooks go with the role. Its copies are still reported.
518 if (!m_otpLine.isNull() && m_otpLine != otp) {
519 disconnect(m_otpLine, &QLineEdit::textChanged, this,
521 disconnect(m_otpLine, &QLineEdit::textEdited, this,
523 disconnect(m_otpLine, &QLineEdit::editingFinished, this,
525 }
526 m_otpLine = otp;
527 if (otp == nullptr) {
528 return;
529 }
530 otp->setPlaceholderText(tr("otpauth:// URI or base32 secret"));
531 connect(otp, &QLineEdit::textChanged, this, &PasswordDialog::validateOtpField,
532 Qt::UniqueConnection);
533 // textEdited, not textChanged: it fires only for user input, so programmatic
534 // population does not count as the user having touched the field.
535 connect(otp, &QLineEdit::textEdited, this,
536 &PasswordDialog::markOtpFieldEdited, Qt::UniqueConnection);
537 // Canonicalise as soon as the user leaves the field, so the value they end
538 // up saving is the value they were shown.
539 connect(otp, &QLineEdit::editingFinished, this,
540 &PasswordDialog::normalizeOtpField, Qt::UniqueConnection);
542}
543
545 if (QObject *line = sender()) {
546 line->setProperty(kOtpEditedProperty, true);
547 }
548}
549
551 // m_otpLine, not otpLineEdit(): the signals that bring us here and the
552 // edit mark are that line's. The OTP role is settled when fields change.
553 QLineEdit *otp = m_otpLine;
554 if (otp == nullptr) {
555 return;
556 }
557
558 const QString text = otp->text().trimmed();
559 const bool bad = !text.isEmpty() && !Totp::isValid(text);
560 if (bad) {
561 if (m_otpWarning.isNull()) {
562 // Theme-aware indicator: no hardcoded colours and no extra layout row.
564 otp->addAction(QIcon::fromTheme(QStringLiteral("dialog-warning")),
565 QLineEdit::TrailingPosition);
566 }
567 otp->setToolTip(tr("Invalid OTP secret"));
568 } else {
569 if (!m_otpWarning.isNull()) {
570 otp->removeAction(m_otpWarning);
571 delete m_otpWarning;
572 m_otpWarning = nullptr;
573 }
574 otp->setToolTip(QString());
575 }
576}
577
578// Only an otpauth:// URI or a value typed into this field is rewritten.
579// Base32::sanitizeInput() maps 1 to L and 8 to B, so a backup code like
580// `12345678` passes as base32 and was silently rewritten on any OK. A bare
581// secret left alone still works: Totp::parse() accepts one.
583 QLineEdit *otp = m_otpLine;
584 if (otp == nullptr) {
585 return;
586 }
587 const QString text = otp->text().trimmed();
588 if (text.isEmpty()) {
589 return;
590 }
591 if (!otp->property(kOtpEditedProperty).toBool() &&
593 return;
594 }
595 // fileName(), not completeBaseName(): the latter truncates at the last dot,
596 // turning an entry called github.com into a label of "github". m_file already
597 // has its .gpg suffix stripped by MainWindow::getFile().
598 const QString canonical = Totp::normalize(text, QFileInfo(m_file).fileName());
599 if (!canonical.isEmpty()) {
600 otp->setText(canonical);
601 }
602}
603
605 QString passFile = ui->lineEditPassword->text() + "\n";
606 QList<QLineEdit *> allLines(m_templateLines);
607 allLines.append(m_otherLines);
608 for (QLineEdit *line : std::as_const(allLines)) {
609 const QString text = line->text();
610 const QString name = line->objectName();
611 const QString original = line->property(kOriginalLineProperty).toString();
612 // A field whose name and value are still exactly what was parsed keeps its
613 // source line verbatim: reserialising would turn `key:value` into
614 // `key: value`, trim whitespace the value carried, and drop a field with an
615 // empty value (`recovery:`) entirely. The widget holds trimmed name/value,
616 // so compare against the original line split and trimmed the same way.
617 if (!original.isEmpty()) {
618 const qsizetype colon = original.indexOf(QLatin1Char(':'));
619 if (colon >= 0 && original.left(colon).trimmed() == name &&
620 original.right(original.length() - colon - 1).trimmed() == text) {
621 passFile += original + "\n";
622 continue;
623 }
624 }
625 if (text.isEmpty()) {
626 continue;
627 }
628 passFile += name + ": " + text + "\n";
629 }
630 // Not toPlainText(): it saves a blank for every no-break space (U+00A0) in
631 // the notes, so any OK rewrites even lines the user never touched and a
632 // passphrase or recovery code with no-break spaces is corrupted. The raw
633 // document text keeps the no-break spaces; only the block and line
634 // separators it uses are normalised back to '\n'.
635 QString notes = ui->plainTextEdit->document()->toRawText();
636 notes.replace(QChar(QChar::ParagraphSeparator), QLatin1Char('\n'));
637 notes.replace(QChar(QChar::LineSeparator), QLatin1Char('\n'));
638 passFile += notes;
639 return passFile;
640}
641
642void PasswordDialog::setTemplate(const QString &rawFields, bool useTemplate) {
643 m_fields = TemplateIO::fieldsIn(rawFields);
644 m_templating = useTemplate;
645
646 for (QLineEdit *line : std::as_const(m_templateLines)) {
647 ui->formLayout->removeRow(line);
648 }
649 m_templateLines.clear();
650
651 // Defensively remove all rows tracked in m_otherLines to prevent accumulation
652 // when cycling templates or applying new templates after setPassword
653 for (QLineEdit *line : std::as_const(m_otherLines)) {
654 ui->formLayout->removeRow(line);
655 }
656 m_otherLines.clear();
657
658 if (m_templating) {
659 QWidget *previous = ui->checkBoxShow;
660 for (const QString &field : std::as_const(m_fields)) {
661 if (field.isEmpty()) {
662 continue;
663 }
664 auto *line = new SecretLineEdit();
665 reportCopies(line);
666 // Plain text before the name is set: the template comes from the
667 // store, and as rich text <img src="/dev/zero"> has Qt read that file
668 // without end.
669 auto *label = new QLabel();
670 label->setTextFormat(Qt::PlainText);
671 label->setText(field);
672 line->setObjectName(field);
673 ui->formLayout->addRow(label, line);
674 setTabOrder(previous, line);
675 m_templateLines.append(line);
676 previous = line;
677 }
678 }
679
680 hookOtpField();
681}
682
686
690
692 ui->spinBox_pwdLength->setValue(length);
693}
694
696 ui->passwordTemplateSwitch->setCurrentIndex(templateIndex);
697}
698
701 ui->passwordTemplateSwitch->setDisabled(usePwgen);
702 ui->label_characterset->setDisabled(usePwgen);
703}
704
705// The template row stays hidden until setAvailableTemplates() fills it.
707 ui->label_template->hide();
708 ui->templateBox->hide();
709 connect(ui->templateBox, &QComboBox::currentTextChanged, this,
710 [this](const QString &name) {
711 // applyTemplate() rebuilds the field rows, so only act on a real
712 // change; the box is also updated from applyTemplate() itself.
713 if (name != m_currentTemplateName) {
714 applyTemplate(name);
715 }
716 });
717}
718
720 const QHash<QString, QStringList> &templates,
721 const QString &defaultTemplate) {
722 m_availableTemplates = templates;
723 QStringList templateNames = templates.keys();
724 if (templateNames.isEmpty()) {
725 return;
726 }
727 std::sort(templateNames.begin(), templateNames.end());
728 {
729 const QSignalBlocker blocker(ui->templateBox);
730 ui->templateBox->clear();
731 ui->templateBox->addItems(templateNames);
732 }
733 ui->label_template->show();
734 ui->templateBox->show();
735 if (!m_templateShortcut) {
736 m_templateShortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_T), this,
737 this, [this] { cycleTemplate(); });
738 }
739 QString selected = defaultTemplate;
740 if (!templateNames.contains(selected)) {
741 selected = templateNames.first();
742 }
743 applyTemplate(selected);
744}
745
746void PasswordDialog::applyTemplate(const QString &templateName) {
747 if (m_saving) {
748 return;
749 }
750 auto it = m_availableTemplates.constFind(templateName);
751 if (it != m_availableTemplates.constEnd()) {
752 m_currentTemplateName = templateName;
753 const QSignalBlocker blocker(ui->templateBox);
754 ui->templateBox->setCurrentText(templateName);
755 QString fields = it.value().join("\n");
756 setTemplate(fields, true);
757 }
758}
759
761 if (m_availableTemplates.isEmpty()) {
762 return;
763 }
764 QStringList names = m_availableTemplates.keys();
765 std::sort(names.begin(), names.end());
766
767 qsizetype currentIdx = names.indexOf(m_currentTemplateName);
768 qsizetype nextIdx;
769 if (currentIdx < 0) {
770 nextIdx = 0;
771 } else {
772 nextIdx = (currentIdx + 1) % names.size();
773 }
774 applyTemplate(names.at(nextIdx));
775}
776
777void PasswordDialog::setPass(const QString &output, const QString &file) {
778 // finishedShow names its file: a decrypt of any other entry (a tree click
779 // that was still queued when the dialog opened) is not ours.
780 if (!file.isEmpty() && file != m_file) {
781 return;
782 }
783 // An editor block per line: an entry of millions of lines (a small,
784 // compressed one in a shared store can be one) would take QtPass down.
785 // Not loaded at all, so nothing can be saved over it from here either.
786 // Counted as an editor of the whole entry would hold it: the final
787 // newline pass and QtPass write ends the last line, it starts no new one.
788 // Password and fields count too, so this is stricter than the display's
789 // limit on the remaining text alone.
790 if (const qsizetype lines =
791 output.count(QLatin1Char('\n')) +
792 (output.isEmpty() || output.endsWith(QLatin1Char('\n')) ? 0 : 1);
794 // The count in a variable first, as in FileContent: older lupdate
795 // versions lose a tr() with template arguments among its arguments.
796 const int n = static_cast<int>(
797 std::min<qsizetype>(lines, std::numeric_limits<int>::max()));
798 ui->statusLabel->setText(
799 tr("This entry has %n line(s), too many to edit here.", nullptr, n));
800 return;
801 }
802 setPassword(output);
803 m_contentLoaded = true;
804 // Drop the "Decrypting…" note so it cannot be mistaken for a warning.
805 ui->statusLabel->clear();
806 setEditorEnabled(true);
807 ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
808}
809
810void PasswordDialog::onShowError(int /* exitCode */, const QString &err) {
811 // React only while the dialog is still waiting for its own decrypt.
812 // processErrorExit carries no request identity, but for an existing entry
813 // the only Pass operation in flight is the Show() that opened this dialog.
814 if (m_isNew || m_contentLoaded) {
815 return;
816 }
817 ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
818 setEditorEnabled(false);
819 ui->statusLabel->setText(err);
820}
821
823 ui->lineEditPassword->setEnabled(enabled);
824 ui->createPasswordButton->setEnabled(enabled);
825 ui->checkBoxShow->setEnabled(enabled);
826 ui->passwordTemplateSwitch->setEnabled(enabled);
827 ui->label_characterset->setEnabled(enabled);
828 ui->spinBox_pwdLength->setEnabled(enabled);
829 ui->plainTextEdit->setEnabled(enabled);
830 for (QLineEdit *line : std::as_const(m_templateLines)) {
831 line->setEnabled(enabled);
832 }
833 for (QLineEdit *line : std::as_const(m_otherLines)) {
834 line->setEnabled(enabled);
835 }
836 // Re-apply the pwgen policy on the way back up, or the two widgets usePwgen()
837 // locks would be spuriously re-enabled with the rest of the editor.
838 if (enabled && m_usePwgen) {
839 usePwgen(true);
840 }
841}
The name of a key: value field in PasswordDialog, editable in place: double-click or the context menu...
Definition fieldlabel.h:26
FieldLabel(const QString &name, QWidget *parent=nullptr)
Label for the field called name.
void removeRequested()
The user chose "Remove field".
void renamed(const QString &from, const QString &to)
The user typed a different name and confirmed it.
Represents the parsed contents of a password file.
Definition filecontent.h:92
static auto isOtpFieldName(const QString &name) -> bool
Whether a field name designates the one-time password field.
static constexpr int kMaxDisplayLines
Most lines of remaining text getRemainingDataForDisplay() gives.
auto getRemainingData() const -> QString
auto getNamedValues() const -> NamedValues
static auto parse(const QString &fileContent, const QStringList &templateFields, bool allFields, const std::function< bool(const QString &)> &alsoField={}) -> FileContent
parse parses the given fileContent in a FileContent object. The password is accessible through getPas...
auto getPassword() const -> QString
static auto isOtpUriValue(const QString &value) -> bool
Whether a field value is an otpauth URI, and therefore a secret.
The NamedValues class is mostly a list of NamedValue but also has a method to take a specific NamedVa...
Definition filecontent.h:58
auto takeNamedValue(const QString &name) -> NamedValue
Remove and return the named field, its original line included.
Abstract base class for password store operations.
Definition pass.h:75
void finishedInsert(const QString &out, const QString &err)
An Insert() succeeded: the entry is in the store and, with Git in use, committed.
virtual auto generatePassword(unsigned int length, const QString &charset) -> QString
Generate random password.
Definition pass.cpp:218
void finishedShow(const QString &out, const QString &file)
Emitted when a Show() succeeds.
void insertFailed(const QString &err, bool written)
Emitted when an Insert() fails through a queued command, before the generic processErrorExit(): the p...
virtual void Show(QString file)=0
Show decrypted password file.
virtual void Insert(QString file, QString value, bool force)=0
Insert or update password.
void processErrorExit(int exitCode, const QString &err)
An operation ended in failure: its process exited non-zero, could not be started or crashed,...
auto insertsPending() const -> int
How many inserts have started queueing but not yet reported.
Definition pass.h:194
void critical(const QString &title, const QString &message)
Something went wrong that the user has to be told about in a dialog, such as a refused path or a fail...
void validateOtpField()
Flag an OTP value that is neither a URI nor valid base32.
QScopedPointer< Ui::PasswordDialog > ui
Generated form (passworddialog.ui); owned.
bool m_templating
Whether template fields get widgets of their own (the "Use template" setting).
void hookOtpField()
Connect validation and normalisation to the OTP field, if present.
QList< QLineEdit * > m_otherLines
QStringList m_fields
Field names of the current template, in template order.
void onInsertFailed(const QString &err, bool written)
The insert started by accept() failed: unlock the fields, which still hold what was typed,...
void setPassword(const QString &password)
Populate the dialog's password field with the given text.
QString m_newEntryFolder
void setSaving(bool saving)
Lock or unlock the dialog while an insert is in flight.
void validateNewEntry()
Re-runs resolveNewEntry() as the user types or picks a folder: shows the problem (if any) in the stat...
Pass * m_pass
Backend for Show, Insert and generatePassword; not owned.
void cycleTemplate()
Cycle to next template (Ctrl+T).
void setAvailableTemplates(const QHash< QString, QStringList > &templates, const QString &defaultTemplate)
Set available templates from file and select default.
void setupTemplateBox()
Hides the template row until setAvailableTemplates() fills it and wires the combo box so that a user ...
void normalizeOtpField()
Rewrite the OTP field as a canonical otpauth URI.
bool m_allFields
Whether every key: value line becomes a field (the "Templateall fields" setting), as given to FileCon...
void showNotSaved(const QString &err)
Say why the entry was not saved and let the user try again; the fields keep what was typed.
void accept() override
OK: for a new entry, resolve and check the name first and stay open when it is not usable; then inser...
auto otpLineEdit() const -> QLineEdit *
Locate the field that holds the one-time password configuration.
void save()
Start inserting what the dialog holds and wait for the result.
PasswordDialog(PasswordConfiguration passConfig, QWidget *parent=nullptr)
Construct a PasswordDialog for entering a new password.
bool m_isNew
True when creating an entry, false when editing one; decides whether the name row and the decrypt-bef...
void setLength(int length)
Set the desired password length shown in the dialog.
void reject() override
Cancel, Esc and the window's close button. While an insert is in flight they ask first: closing then ...
void setPasswordVisible(bool show)
Show or mask the password field.
QHash< QString, QStringList > m_availableTemplates
QString m_file
Entry path relative to the store, without .gpg; empty for a new entry until accept() resolves the typ...
bool m_saving
True from accept() starting an insert until the backend reports back.
QShortcut * m_templateShortcut
Ctrl+T, created once templates are available; owned by the dialog.
void setEditorEnabled(bool enabled)
Enabled or grey out every editable field of the dialog.
void onShowError(int exitCode, const QString &err)
Handle a process error while waiting for an entry's decrypt.
void removeField(QLineEdit *line)
Drop a key: value field and its row from the form. The widgets are deleted once control is back in th...
QString m_storeRoot
Absolute store root while a new entry is being named; empty otherwise.
void setPass(const QString &output, const QString &file=QString())
Populate the dialog from a decrypted entry (Pass::finishedShow).
void copied(const QString &text)
Text was copied out of the password, a field or the body, marked as a secret: for ClipboardManager::t...
void setNewEntryLocation(const QString &storeRoot, const QStringList &folders, const QString &currentFolder)
Let a new entry be named inside the dialog: a folder picker over the store's folders and a name field...
void renameField(QLineEdit *line, FieldLabel *label, const QString &to)
Give a key: value field a new key (#132). Refused, with the reason in the status label,...
auto isOwnInsertReport() -> bool
Whether the insert report now arriving is this dialog's own.
~PasswordDialog() override
Hides the window before QDialog tears it down, so the focus-out emitted on hiding cannot reach slots ...
PasswordConfiguration m_passConfig
Generator settings: character sets and default length.
auto resolveNewEntry(QString *problem) const -> QString
The name the user typed, resolved against the picked folder.
void usePwgen(bool usePwgen)
Enable or disable pwgen-style password generation mode.
void setPasswordCharTemplate(int templateIndex)
Set the password character template index.
void setTemplate(const QString &rawFields, bool useTemplate)
Set the template fields and whether templating is enabled.
void templateAll(bool templateAll)
Whether every key: value line becomes a field of its own.
void reportCopies(SecretLineEdit *line)
Forwards the line edit's SecretLineEdit::copied to this dialog's copied(), so every copy out of any f...
auto getPassword() -> QString
Retrieve the current text from the dialog's password field.
void markOtpFieldEdited()
Remember, on the line edit that sent textEdited(), that the user typed in it: only such a field has a...
void on_rejected()
Connected to QDialog::rejected: blanks the password and field widgets so the decrypted secret does no...
void onInsertFinished()
The insert started by accept() succeeded: close as accepted.
QList< QLineEdit * > m_templateLines
QPointer< QLineEdit > m_otpLine
void applyTemplate(const QString &templateName)
Switch to a named template: records it, selects it in the combo box and rebuilds the field rows throu...
void on_createPasswordButton_clicked()
"Generate" button: asks the backend for a password of the chosen length from the selected character s...
QString m_currentTemplateName
Name of the template currently applied; cycleTemplate() steps from it.
QPointer< QAction > m_otpWarning
static auto isPathInStore(const QString &storeRoot, const QString &candidate) -> bool
Check that a candidate path resolves inside the password store root, after following symlinks and res...
Singleton that stores QtPass settings, handles persistence and defaults.
A QLineEdit for a password that can be shown: whatever is copied out of it is marked as a secret and ...
void copied(const QString &text)
Selected text was copied out of the line edit.
SecretLineEdit(QWidget *parent=nullptr)
Constructs the line edit.
void copied(const QString &text)
Selected text was copied or cut out of the editor.
static auto fieldsIn(const QString &templateText) -> QStringList
The field names of the template in the settings: one per line, with comment lines (starting with #,...
static auto normalize(const QString &config, const QString &label={}, const QString &issuer={}) -> QString
Normalise a user-entered value to a canonical otpauth URI.
Definition totp.cpp:296
static auto isValid(const QString &config) -> bool
Convenience validity test, for UI feedback.
Definition totp.cpp:201
static auto mkpathForStore(const QString &path) -> bool
Create path and any missing parents as pass would.
Definition util.cpp:438
FieldLabel: the in-place renamable, removable name label of a key: value field in PasswordDialog.
Parser for a decrypted password entry: password line, key: value fields, remaining text and the OTP c...
Pass, the abstract password-store backend that RealPass and ImitatePass implement,...
Value object for the password generator: chosen character set, its alphabets and the length.
Dialog for creating or editing one password entry, including the generator, template fields,...
PathValidator: is a path inside the password store, and how is it spelled relative to the store root.
The "qtpass" logging category.
QtPassSettings, the QSettings singleton behind every saved setting, the profiles and the Pass backend...
QLineEdit whose copies, cuts and primary-selection fills carry the secret clipboard hints and are rep...
QPlainTextEdit for editing decrypted entry text that marks everything copied out of it as a secret.
Plain value-object holding all flat QtPass configuration.
Definition appsettings.h:36
bool usePwgen
Use pwgen for password generation.
QString passTemplate
Newline-separated template field names.
bool templateAllFields
Treat every key: line as a field.
bool useTemplate
Use the password template.
PasswordConfiguration passwordConfiguration
Length/charset/custom chars.
A name/value pair parsed from a password file field.
Definition filecontent.h:21
QString originalLine
The field's verbatim source line, colon and spacing intact.
Definition filecontent.h:38
QString value
Field value (the part after the colon in "name: value").
Definition filecontent.h:29
Holds the password generator settings: which character set is selected, the alphabet of every set,...
int length
Length of the generated password, in characters.
CharacterSet selected
Currently active character set selection.
QString Characters[CHARSETS_COUNT]
The alphabet of each character set, indexed by CharacterSet. Only the CUSTOM entry is meant to be cha...
CharacterSet
Character set options for password generation; also the index into Characters.
TemplateIO: the store's .templates and .default_template files and the # secret: marker in template t...
RFC 6238 TOTP: otpauth URI parsing, code generation and canonical re-serialisation.
Util: static helpers for the process environment, binary discovery, store paths, URL safety and the l...