• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IJHack / QtPass / 28650029157

03 Jul 2026 08:59AM UTC coverage: 56.726% (-0.002%) from 56.728%
28650029157

push

github

web-flow
fix: prevent PasswordDialog content duplication and premature-save data loss (#1605)

* fix: prevent PasswordDialog content duplication and premature-save data loss

Two data-integrity bugs when editing an existing entry:

- setPassword() populated the free-text body with insertPlainText(), which
  appends. PasswordDialog listens to the shared Pass::finishedShow signal, so a
  second Show (a later one for any entry) re-ran the handler and appended a
  second copy of the body; getPassword() then saved the duplicated content back
  to the file. Use setPlainText() so re-populating replaces the body, and
  disconnect from finishedShow after the first (our) result so a later Show of a
  different entry can no longer overwrite or duplicate these fields.

- on_accepted() guarded with `if (newValue.isEmpty()) return;`, but
  getPassword() always returns at least a trailing newline, so the guard was
  dead code. Clicking OK before the asynchronous Show finished decrypting
  therefore overwrote the existing entry with the empty dialog fields. Track a
  m_contentLoaded flag (set when the entry's content arrives) and refuse to save
  an existing entry until it has loaded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JuQsrHonihp1nARE7bzstc

* fix: keep listening for finishedShow instead of disconnecting after first

finishedShow carries no request identity, so disconnecting after the first
signal risked consuming a stale queued Show as this dialog's result and never
seeing its own. The dialog is modal and always edits the selected entry, and
setPassword() replaces every field (so repeated results overwrite rather than
duplicate — the original duplication bug is fixed by setPlainText). Drop the
disconnect and rely on last-writer-wins instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JuQsrHonihp1nARE7bzstc

---------... (continued)

2 of 4 new or added lines in 1 file covered. (50.0%)

1 existing line in 1 file now uncovered.

3812 of 6720 relevant lines covered (56.73%)

30.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

69.34
/src/passworddialog.cpp
1
// SPDX-FileCopyrightText: 2015 Anne Jan Brouwer
2
// SPDX-License-Identifier: GPL-3.0-or-later
3
#include "passworddialog.h"
4
#include "filecontent.h"
5
#include "pass.h"
6
#include "passwordconfiguration.h"
7
#include "qtpasssettings.h"
8
#include "ui_passworddialog.h"
9
#include "util.h"
10
#include <algorithm>
11

12
#include <QHash>
13
#include <QLabel>
14
#include <QLineEdit>
15
#include <QShortcut>
16
#include <utility>
17

18
#ifdef QT_DEBUG
19
#include "debughelper.h"
20
#endif
21

22
/**
23
 * @brief PasswordDialog::PasswordDialog basic constructor.
24
 * @param passConfig configuration constant
25
 * @param parent
26
 */
27
PasswordDialog::PasswordDialog(PasswordConfiguration passConfig,
17✔
28
                               QWidget *parent)
17✔
29
    : QDialog(parent), ui(new Ui::PasswordDialog),
34✔
30
      m_passConfig(std::move(passConfig)), m_pass(QtPassSettings::getPass()) {
17✔
31
  // m_pass is captured once from the singleton here. This constructor is
32
  // only reached from tests; production always uses the two-arg overload
33
  // that receives an explicit Pass* with a stable, caller-controlled lifetime.
34
  m_templating = false;
35
  m_isNew = false;
36

37
  ui->setupUi(this);
17✔
38
  setLength(m_passConfig.length);
17✔
39
  setPasswordCharTemplate(m_passConfig.selected);
17✔
40

41
  connect(m_pass, &Pass::finishedShow, this, &PasswordDialog::setPass);
17✔
42
}
17✔
43

44
/**
45
 * @brief PasswordDialog::PasswordDialog complete constructor.
46
 * @param file
47
 * @param isNew
48
 * @param parent pointer
49
 */
50
PasswordDialog::PasswordDialog(Pass *pass, const AppSettings &s, QString file,
×
51
                               const bool &isNew, QWidget *parent)
×
52
    : QDialog(parent), ui(new Ui::PasswordDialog), m_pass(pass),
×
53
      m_file(std::move(file)), m_isNew(isNew) {
×
54

55
  if (!isNew) {
×
56
    m_pass->Show(m_file);
×
57
  }
58

59
  ui->setupUi(this);
×
60

61
  setWindowTitle(this->windowTitle() + " " + m_file);
×
62
  m_passConfig = s.passwordConfiguration;
×
63
  usePwgen(s.usePwgen);
×
64
  setTemplate(s.passTemplate, s.useTemplate);
×
65

66
  setLength(m_passConfig.length);
×
67
  setPasswordCharTemplate(m_passConfig.selected);
×
68

69
  connect(m_pass, &Pass::finishedShow, this, &PasswordDialog::setPass);
×
70
  connect(m_pass, &Pass::processErrorExit, this, &PasswordDialog::close);
×
71
  connect(this, &PasswordDialog::accepted, this, &PasswordDialog::on_accepted);
×
72
  connect(this, &PasswordDialog::rejected, this, &PasswordDialog::on_rejected);
×
73
}
×
74

75
/**
76
 * @brief PasswordDialog::~PasswordDialog basic destructor.
77
 */
78
PasswordDialog::~PasswordDialog() { delete ui; }
68✔
79

80
/**
81
 * @brief PasswordDialog::on_checkBoxShow_stateChanged hide or show passwords.
82
 * @param arg1
83
 */
84
void PasswordDialog::on_checkBoxShow_stateChanged(int arg1) {
2✔
85
  if (arg1) {
2✔
86
    ui->lineEditPassword->setEchoMode(QLineEdit::Normal);
1✔
87
  } else {
88
    ui->lineEditPassword->setEchoMode(QLineEdit::Password);
1✔
89
  }
90
}
2✔
91

92
/**
93
 * @brief PasswordDialog::on_createPasswordButton_clicked generate a random
94
 * password.
95
 */
96
void PasswordDialog::on_createPasswordButton_clicked() {
×
97
  ui->widget->setEnabled(false);
×
98
  const int currentIndex = ui->passwordTemplateSwitch->currentIndex();
×
99
  if (currentIndex < 0 ||
×
100
      currentIndex >= static_cast<int>(PasswordConfiguration::CHARSETS_COUNT)) {
101
    ui->widget->setEnabled(true);
×
102
    return;
×
103
  }
104

105
  QString newPass = m_pass->generatePassword(
×
106
      static_cast<unsigned int>(ui->spinBox_pwdLength->value()),
×
107
      m_passConfig.Characters[static_cast<PasswordConfiguration::characterSet>(
108
          currentIndex)]);
×
109
  if (!newPass.isEmpty()) {
×
110
    ui->lineEditPassword->setText(newPass);
×
111
  }
112
  ui->widget->setEnabled(true);
×
113
}
114

115
/**
116
 * @brief PasswordDialog::on_accepted handle Ok click for QDialog
117
 */
118
void PasswordDialog::on_accepted() {
×
119
  // For an existing entry, refuse to save until its decrypted content has
120
  // loaded (Show is asynchronous). getPassword() always returns at least a
121
  // newline, so the previous isEmpty() check never fired and clicking OK early
122
  // overwrote the entry with the empty dialog fields.
NEW
123
  if (!m_isNew && !m_contentLoaded) {
×
UNCOV
124
    return;
×
125
  }
126

NEW
127
  QString newValue = getPassword();
×
128
  if (newValue.right(1) != "\n") {
×
129
    newValue += "\n";
×
130
  }
131

132
  m_pass->Insert(m_file, newValue, !m_isNew);
×
133
}
134

135
/**
136
 * @brief PasswordDialog::on_rejected handle Cancel click for QDialog
137
 */
138
void PasswordDialog::on_rejected() { setPassword(QString()); }
×
139

140
/**
141
 * @brief PasswordDialog::setPassword populate the (templated) fields.
142
 * @param password
143
 */
144
void PasswordDialog::setPassword(const QString &password) {
9✔
145
  // Always parse all fields as editable so users can edit any field in the
146
  // password file. This fixes issue #132 where users couldn't edit
147
  // fields without toggling the "Show all fields templated" setting.
148
  FileContent fileContent = FileContent::parse(password, m_fields, true);
9✔
149
  ui->lineEditPassword->setText(fileContent.getPassword());
9✔
150

151
  QWidget *previous = ui->checkBoxShow;
9✔
152
  // first set templated values
153
  NamedValues namedValues = fileContent.getNamedValues();
9✔
154
  for (QLineEdit *line : std::as_const(m_templateLines)) {
10✔
155
    line->setText(namedValues.takeValue(line->objectName()));
2✔
156
    previous = line;
157
  }
158
  // show remaining values (if there are)
159
  // Remove previously created dynamic widgets to prevent duplicates and leaks
160
  for (QLineEdit *line : std::as_const(m_otherLines)) {
9✔
161
    ui->formLayout->removeRow(line);
×
162
  }
163
  m_otherLines.clear();
9✔
164
  for (const NamedValue &nv : std::as_const(namedValues)) {
12✔
165
    auto *line = new QLineEdit();
3✔
166
    line->setObjectName(nv.name);
3✔
167
    line->setText(nv.value);
3✔
168
    ui->formLayout->addRow(new QLabel(nv.name), line);
3✔
169
    setTabOrder(previous, line);
3✔
170
    m_otherLines.append(line);
3✔
171
    previous = line;
172
  }
173

174
  // setPlainText (not insertPlainText) so re-populating replaces the body
175
  // instead of appending a second copy, which would be saved back as
176
  // duplicated content.
177
  ui->plainTextEdit->setPlainText(fileContent.getRemainingData());
18✔
178
}
9✔
179

180
/**
181
 * @brief PasswordDialog::getPassword  join the (templated) fields to a QString
182
 * for writing back.
183
 * @return collapsed password.
184
 */
185
auto PasswordDialog::getPassword() -> QString {
9✔
186
  QString passFile = ui->lineEditPassword->text() + "\n";
18✔
187
  QList<QLineEdit *> allLines(m_templateLines);
188
  allLines.append(m_otherLines);
9✔
189
  for (QLineEdit *line : std::as_const(allLines)) {
13✔
190
    QString text = line->text();
4✔
191
    if (text.isEmpty()) {
4✔
192
      continue;
193
    }
194
    passFile += line->objectName() + ": " + text + "\n";
8✔
195
  }
196
  passFile += ui->plainTextEdit->toPlainText();
18✔
197
  return passFile;
9✔
198
}
199

200
/**
201
 * @brief PasswordDialog::setTemplate set the template and create the fields.
202
 * @param rawFields
203
 */
204
void PasswordDialog::setTemplate(const QString &rawFields, bool useTemplate) {
12✔
205
  m_fields = rawFields.split('\n');
12✔
206
  m_templating = useTemplate;
12✔
207

208
  for (QLineEdit *line : std::as_const(m_templateLines)) {
14✔
209
    ui->formLayout->removeRow(line);
2✔
210
  }
211
  m_templateLines.clear();
12✔
212

213
  // Defensively remove all rows tracked in m_otherLines to prevent accumulation
214
  // when cycling templates or applying new templates after setPassword
215
  for (QLineEdit *line : std::as_const(m_otherLines)) {
12✔
216
    ui->formLayout->removeRow(line);
×
217
  }
218
  m_otherLines.clear();
12✔
219

220
  if (m_templating) {
12✔
221
    QWidget *previous = ui->checkBoxShow;
6✔
222
    for (const QString &field : std::as_const(m_fields)) {
13✔
223
      if (field.isEmpty()) {
7✔
224
        continue;
×
225
      }
226
      auto *line = new QLineEdit();
7✔
227
      auto *label = new QLabel(field);
7✔
228
      line->setObjectName(field);
7✔
229
      ui->formLayout->addRow(label, line);
7✔
230
      setTabOrder(previous, line);
7✔
231
      m_templateLines.append(line);
7✔
232
      previous = line;
233
    }
234
  }
235
}
12✔
236

237
/**
238
 * @brief PasswordDialog::setLength
239
 * PasswordDialog::setLength password length.
240
 * @param length
241
 */
242
void PasswordDialog::setLength(int length) {
19✔
243
  ui->spinBox_pwdLength->setValue(length);
19✔
244
}
19✔
245

246
/**
247
 * @brief PasswordDialog::setPasswordCharTemplate
248
 * PasswordDialog::setPasswordCharTemplate chose the template style.
249
 * @param templateIndex
250
 */
251
void PasswordDialog::setPasswordCharTemplate(int templateIndex) {
19✔
252
  ui->passwordTemplateSwitch->setCurrentIndex(templateIndex);
19✔
253
}
19✔
254

255
/**
256
 * @brief PasswordDialog::usePwgen
257
 * PasswordDialog::usePwgen don't use own password generator.
258
 * @param usePwgen
259
 */
260
void PasswordDialog::usePwgen(bool usePwgen) {
2✔
261
  ui->passwordTemplateSwitch->setDisabled(usePwgen);
2✔
262
  ui->label_characterset->setDisabled(usePwgen);
2✔
263
}
2✔
264

265
/**
266
 * @brief Set available templates from .templates file and apply default.
267
 * @param templates Hash of template name to field list.
268
 * @param defaultTemplate Name of default template to select.
269
 */
270
void PasswordDialog::setAvailableTemplates(
3✔
271
    const QHash<QString, QStringList> &templates,
272
    const QString &defaultTemplate) {
273
  m_availableTemplates = templates;
3✔
274
  QStringList templateNames = templates.keys();
3✔
275
  if (templateNames.isEmpty()) {
3✔
276
    return;
277
  }
278
  std::sort(templateNames.begin(), templateNames.end());
6✔
279
  QString selected = defaultTemplate;
280
  if (!templateNames.contains(selected)) {
3✔
281
    selected = templateNames.first();
×
282
  }
283
  applyTemplate(selected);
3✔
284
}
285

286
/**
287
 * @brief Apply a template by name.
288
 * @param templateName Name of template to apply.
289
 */
290
void PasswordDialog::applyTemplate(const QString &templateName) {
5✔
291
  auto it = m_availableTemplates.constFind(templateName);
5✔
292
  if (it != m_availableTemplates.constEnd()) {
293
    m_currentTemplateName = templateName;
5✔
294
    QString fields = it.value().join("\n");
5✔
295
    setTemplate(fields, true);
5✔
296
  }
297
}
5✔
298

299
/**
300
 * @brief Cycle to next template (Ctrl+T).
301
 */
302
void PasswordDialog::cycleTemplate() {
4✔
303
  if (m_availableTemplates.isEmpty()) {
304
    return;
2✔
305
  }
306
  QStringList names = m_availableTemplates.keys();
2✔
307
  std::sort(names.begin(), names.end());
4✔
308

309
  qsizetype currentIdx = names.indexOf(m_currentTemplateName);
310
  qsizetype nextIdx;
311
  if (currentIdx < 0) {
2✔
312
    nextIdx = 0;
313
  } else {
314
    nextIdx = (currentIdx + 1) % names.size();
2✔
315
  }
316
  applyTemplate(names.at(nextIdx));
2✔
317
}
318

319
/**
320
 * @brief Sets the password from pass show output.
321
 * @param output Output from pass show command
322
 */
323
void PasswordDialog::setPass(const QString &output) {
9✔
324
  // setPassword() replaces (not appends) every field, so if more than one
325
  // finishedShow arrives the last one wins rather than duplicating content.
326
  // We deliberately keep listening: the dialog is modal and always edits the
327
  // selected entry, so its own Show is the result that matters, and we cannot
328
  // safely disconnect on the first signal because finishedShow carries no
329
  // request identity — a stale queued Show could otherwise be consumed as ours.
330
  setPassword(output);
9✔
331
  m_contentLoaded = true;
9✔
332
}
9✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc