20#include "ui_passworddialog.h"
28#include <QDialogButtonBox>
39#include <QSignalBlocker>
40#include <QTextDocument>
48const char kOtpEditedProperty[] =
"qtpassOtpEdited";
51const char kOriginalLineProperty[] =
"qtpassOriginalLine";
64 connect(
ui->checkBoxShow, &QCheckBox::toggled,
this,
80 const bool &isNew, QWidget *parent)
85 connect(
ui->checkBoxShow, &QCheckBox::toggled,
this,
95 setWindowTitle(isNew &&
m_file.isEmpty()
97 : this->windowTitle() +
" " +
m_file);
117 ui->statusLabel->setText(tr(
"Decrypting…"));
119 ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(
false);
133 ui->lineEditPassword->setEchoMode(QLineEdit::Normal);
135 ui->lineEditPassword->setEchoMode(QLineEdit::Password);
140 ui->widget->setEnabled(
false);
141 const int currentIndex =
ui->passwordTemplateSwitch->currentIndex();
142 if (currentIndex < 0 ||
144 ui->widget->setEnabled(
true);
149 static_cast<unsigned int>(
ui->spinBox_pwdLength->value()),
152 if (!newPass.isEmpty()) {
153 ui->lineEditPassword->setText(newPass);
155 ui->widget->setEnabled(
true);
170 if (newValue.right(1) !=
"\n") {
179 const QMetaObject::Connection refused =
181 [&refusal](
const QString &,
const QString &message) {
191 if (
m_saving && !refusal.isEmpty()) {
200 ui->nameRow->setEnabled(!saving);
202 ui->templateBox->setEnabled(!saving);
206 ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!saving);
207 ui->statusLabel->setText(saving ? tr(
"Saving…") : QString());
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);
259 if (answer != QMessageBox::Yes || !
m_saving) {
272 ui->statusLabel->setText(tr(
"Not saved: %1").arg(err.trimmed()));
278 const QStringList &folders,
279 const QString ¤tFolder) {
281 ui->folderBox->clear();
283 for (
const QString &folder : folders) {
284 const QString rel = QDir::cleanPath(folder) == QStringLiteral(
".")
286 : QDir::cleanPath(folder);
287 if (rel == QDir::cleanPath(currentFolder) ||
289 QDir::cleanPath(currentFolder) == QStringLiteral(
"."))) {
290 current =
ui->folderBox->count();
292 ui->folderBox->addItem(rel.isEmpty() ? QStringLiteral(
"/") : rel + u
'/',
295 ui->folderBox->setCurrentIndex(current);
298 connect(
ui->nameEdit, &QLineEdit::textChanged,
this,
300 connect(
ui->folderBox, &QComboBox::currentIndexChanged,
this,
303 ui->nameEdit->setFocus();
307 QString name = QDir::fromNativeSeparators(
ui->nameEdit->text().trimmed());
310 if (name.endsWith(QStringLiteral(
".gpg"), Qt::CaseInsensitive)) {
313 if (name.isEmpty()) {
314 *problem = tr(
"Give the entry a name.");
317 if (name.endsWith(u
'/')) {
318 *problem = tr(
"A name cannot end in /.");
321 const QString folder =
ui->folderBox->currentData().toString();
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.");
330 if (QFileInfo::exists(absolute + QStringLiteral(
".gpg"))) {
331 *problem = tr(
"An entry called %1 already exists.").arg(rel);
334 if (QFileInfo(absolute).isDir()) {
335 *problem = tr(
"%1 is a folder.").arg(rel);
346 ui->statusLabel->setText(problem);
347 ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!rel.isEmpty());
358 ui->statusLabel->setText(problem);
362 const QString parent = QFileInfo(QDir(
m_storeRoot).filePath(rel)).path();
364 ui->statusLabel->setText(
365 tr(
"Could not create the folder %1.").arg(parent));
369 setWindowTitle(tr(
"Password") + QStringLiteral(
" ") +
m_file);
383 QWidget *previous =
ui->checkBoxShow;
387 line->setText(nv.
value);
388 line->setProperty(kOriginalLineProperty, nv.
originalLine);
391 line->setProperty(kOtpEditedProperty, QVariant());
396 ui->formLayout->removeRow(line);
399 for (
const NamedValue &nv : std::as_const(namedValues)) {
402 line->setObjectName(nv.name);
403 line->setText(nv.value);
404 line->setProperty(kOriginalLineProperty, nv.originalLine);
406 label->setBuddy(line);
407 ui->formLayout->addRow(label, line);
408 setTabOrder(previous, line);
414 [
this, line, label](
const QString &,
const QString &to) {
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,
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));
448 ui->statusLabel->clear();
449 line->setObjectName(to);
460 const QFormLayout::TakeRowResult row =
ui->formLayout->takeRow(line);
464 disconnect(line,
nullptr,
this,
nullptr);
468 for (QLayoutItem *item : {row.labelItem, row.fieldItem}) {
469 if (item ==
nullptr) {
472 if (QWidget *widget = item->widget()) {
474 widget->deleteLater();
484 QLineEdit *firstMatch =
nullptr;
485 for (QLineEdit *line : std::as_const(allLines)) {
492 if (!line->text().trimmed().isEmpty()) {
495 if (firstMatch ==
nullptr) {
507 if (
auto *owner = qobject_cast<QWidget *>(
m_otpWarning->parent())) {
519 disconnect(
m_otpLine, &QLineEdit::textChanged,
this,
521 disconnect(
m_otpLine, &QLineEdit::textEdited,
this,
523 disconnect(
m_otpLine, &QLineEdit::editingFinished,
this,
527 if (otp ==
nullptr) {
530 otp->setPlaceholderText(tr(
"otpauth:// URI or base32 secret"));
532 Qt::UniqueConnection);
535 connect(otp, &QLineEdit::textEdited,
this,
539 connect(otp, &QLineEdit::editingFinished,
this,
545 if (QObject *line = sender()) {
546 line->setProperty(kOtpEditedProperty,
true);
554 if (otp ==
nullptr) {
558 const QString text = otp->text().trimmed();
564 otp->addAction(QIcon::fromTheme(QStringLiteral(
"dialog-warning")),
565 QLineEdit::TrailingPosition);
567 otp->setToolTip(tr(
"Invalid OTP secret"));
574 otp->setToolTip(QString());
584 if (otp ==
nullptr) {
587 const QString text = otp->text().trimmed();
588 if (text.isEmpty()) {
591 if (!otp->property(kOtpEditedProperty).toBool() &&
599 if (!canonical.isEmpty()) {
600 otp->setText(canonical);
605 QString passFile =
ui->lineEditPassword->text() +
"\n";
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();
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";
625 if (text.isEmpty()) {
628 passFile += name +
": " + text +
"\n";
635 QString notes =
ui->plainTextEdit->document()->toRawText();
636 notes.replace(QChar(QChar::ParagraphSeparator), QLatin1Char(
'\n'));
637 notes.replace(QChar(QChar::LineSeparator), QLatin1Char(
'\n'));
647 ui->formLayout->removeRow(line);
654 ui->formLayout->removeRow(line);
659 QWidget *previous =
ui->checkBoxShow;
660 for (
const QString &field : std::as_const(
m_fields)) {
661 if (field.isEmpty()) {
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);
692 ui->spinBox_pwdLength->setValue(length);
696 ui->passwordTemplateSwitch->setCurrentIndex(templateIndex);
701 ui->passwordTemplateSwitch->setDisabled(
usePwgen);
702 ui->label_characterset->setDisabled(
usePwgen);
707 ui->label_template->hide();
708 ui->templateBox->hide();
709 connect(
ui->templateBox, &QComboBox::currentTextChanged,
this,
710 [
this](
const QString &name) {
713 if (name != m_currentTemplateName) {
720 const QHash<QString, QStringList> &templates,
721 const QString &defaultTemplate) {
723 QStringList templateNames = templates.keys();
724 if (templateNames.isEmpty()) {
727 std::sort(templateNames.begin(), templateNames.end());
729 const QSignalBlocker blocker(
ui->templateBox);
730 ui->templateBox->clear();
731 ui->templateBox->addItems(templateNames);
733 ui->label_template->show();
734 ui->templateBox->show();
739 QString selected = defaultTemplate;
740 if (!templateNames.contains(selected)) {
741 selected = templateNames.first();
753 const QSignalBlocker blocker(
ui->templateBox);
754 ui->templateBox->setCurrentText(templateName);
755 QString fields = it.value().join(
"\n");
765 std::sort(names.begin(), names.end());
769 if (currentIdx < 0) {
772 nextIdx = (currentIdx + 1) % names.size();
780 if (!file.isEmpty() && file !=
m_file) {
790 if (
const qsizetype lines =
791 output.count(QLatin1Char(
'\n')) +
792 (output.isEmpty() || output.endsWith(QLatin1Char(
'\n')) ? 0 : 1);
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));
805 ui->statusLabel->clear();
807 ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(
true);
817 ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(
false);
819 ui->statusLabel->setText(err);
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);
831 line->setEnabled(enabled);
834 line->setEnabled(enabled);
The name of a key: value field in PasswordDialog, editable in place: double-click or the context menu...
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.
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...
auto takeNamedValue(const QString &name) -> NamedValue
Remove and return the named field, its original line included.
Abstract base class for password store operations.
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.
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.
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.
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 ¤tFolder)
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.
static auto isValid(const QString &config) -> bool
Convenience validity test, for UI feedback.
static auto mkpathForStore(const QString &path) -> bool
Create path and any missing parents as pass would.
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.
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.
QString originalLine
The field's verbatim source line, colon and spacing intact.
QString value
Field value (the part after the colon in "name: value").
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...