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

IJHack / QtPass / 24153497225

08 Apr 2026 07:09PM UTC coverage: 21.036% (-0.008%) from 21.044%
24153497225

push

github

web-flow
Merge pull request #941 from IJHack/fix/unchecked-return

fix: check return value of executeBlocking in getKeysFromFile

0 of 4 new or added lines in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

1109 of 5272 relevant lines covered (21.04%)

7.8 hits per line

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

6.06
/src/imitatepass.cpp
1
// SPDX-FileCopyrightText: 2016 Anne Jan Brouwer
2
// SPDX-License-Identifier: GPL-3.0-or-later
3
#include "imitatepass.h"
4
#include "qtpasssettings.h"
5
#include "util.h"
6
#include <QDirIterator>
7
#include <QRegularExpression>
8
#include <utility>
9

10
#ifdef QT_DEBUG
11
#include "debughelper.h"
12
#endif
13

14
using Enums::CLIPBOARD_ALWAYS;
15
using Enums::CLIPBOARD_NEVER;
16
using Enums::CLIPBOARD_ON_DEMAND;
17
using Enums::GIT_ADD;
18
using Enums::GIT_COMMIT;
19
using Enums::GIT_COPY;
20
using Enums::GIT_INIT;
21
using Enums::GIT_MOVE;
22
using Enums::GIT_PULL;
23
using Enums::GIT_PUSH;
24
using Enums::GIT_RM;
25
using Enums::GPG_GENKEYS;
26
using Enums::INVALID;
27
using Enums::PASS_COPY;
28
using Enums::PASS_INIT;
29
using Enums::PASS_INSERT;
30
using Enums::PASS_MOVE;
31
using Enums::PASS_OTP_GENERATE;
32
using Enums::PASS_REMOVE;
33
using Enums::PASS_SHOW;
34
using Enums::PROCESS_COUNT;
35

36
/**
37
 * @brief ImitatePass::ImitatePass for situations when pass is not available
38
 * we imitate the behavior of pass https://www.passwordstore.org/
39
 */
40
ImitatePass::ImitatePass() = default;
20✔
41

42
static auto pgit(const QString &path) -> QString {
×
43
  if (!QtPassSettings::getGitExecutable().startsWith("wsl ")) {
×
44
    return path;
45
  }
46
  QString res = "$(wslpath " + path + ")";
×
47
  return res.replace('\\', '/');
×
48
}
49

50
static auto pgpg(const QString &path) -> QString {
×
51
  if (!QtPassSettings::getGpgExecutable().startsWith("wsl ")) {
×
52
    return path;
53
  }
54
  QString res = "$(wslpath " + path + ")";
×
55
  return res.replace('\\', '/');
×
56
}
57

58
/**
59
 * @brief ImitatePass::GitInit git init wrapper
60
 */
61
void ImitatePass::GitInit() {
×
62
  executeGit(GIT_INIT, {"init", pgit(QtPassSettings::getPassStore())});
×
63
}
×
64

65
/**
66
 * @brief ImitatePass::GitPull git pull wrapper
67
 */
68
void ImitatePass::GitPull() { executeGit(GIT_PULL, {"pull"}); }
×
69

70
/**
71
 * @brief ImitatePass::GitPull_b git pull wrapper
72
 */
73
void ImitatePass::GitPull_b() {
×
74
  Executor::executeBlocking(QtPassSettings::getGitExecutable(), {"pull"});
×
75
}
×
76

77
/**
78
 * @brief ImitatePass::GitPush git push wrapper
79
 */
80
void ImitatePass::GitPush() {
×
81
  if (QtPassSettings::isUseGit()) {
×
82
    executeGit(GIT_PUSH, {"push"});
×
83
  }
84
}
×
85

86
/**
87
 * @brief ImitatePass::Show shows content of file
88
 */
89
void ImitatePass::Show(QString file) {
×
90
  file = QtPassSettings::getPassStore() + file + ".gpg";
×
91
  QStringList args = {"-d",      "--quiet",     "--yes",   "--no-encrypt-to",
92
                      "--batch", "--use-agent", pgpg(file)};
×
93
  executeGpg(PASS_SHOW, args);
×
94
}
×
95

96
/**
97
 * @brief ImitatePass::OtpGenerate generates an otp code
98
 */
99
void ImitatePass::OtpGenerate(QString file) {
×
100
#ifdef QT_DEBUG
101
  dbg() << "No OTP generation code for fake pass yet, attempting for file: " +
102
               file;
103
#else
104
  Q_UNUSED(file)
105
#endif
106
}
×
107

108
/**
109
 * @brief ImitatePass::Insert create new file with encrypted content
110
 *
111
 * @param file      file to be created
112
 * @param newValue  value to be stored in file
113
 * @param overwrite whether to overwrite existing file
114
 */
115
void ImitatePass::Insert(QString file, QString newValue, bool overwrite) {
×
116
  file = file + ".gpg";
×
117
  QString gpgIdPath = Pass::getGpgIdPath(file);
×
118
  if (!verifyGpgIdFile(gpgIdPath)) {
×
119
    emit critical(tr("Check .gpgid file signature!"),
×
120
                  tr("Signature for %1 is invalid.").arg(gpgIdPath));
×
121
    return;
×
122
  }
123
  transactionHelper trans(this, PASS_INSERT);
×
124
  QStringList recipients = Pass::getRecipientList(file);
×
125
  if (recipients.isEmpty()) {
×
126
    // Already emit critical signal to notify user of error - no need to throw
127
    emit critical(tr("Can not edit"),
×
128
                  tr("Could not read encryption key to use, .gpg-id "
×
129
                     "file missing or invalid."));
130
    return;
131
  }
132
  QStringList args = {"--batch", "-eq", "--output", pgpg(file)};
×
133
  for (auto &r : recipients) {
×
134
    args.append("-r");
×
135
    args.append(r);
136
  }
137
  if (overwrite) {
×
138
    args.append("--yes");
×
139
  }
140
  args.append("-");
×
141
  executeGpg(PASS_INSERT, args, newValue);
×
142
  if (!QtPassSettings::isUseWebDav() && QtPassSettings::isUseGit()) {
×
143
    // Git is used when enabled - this is the standard pass workflow
144
    if (!overwrite) {
×
145
      executeGit(GIT_ADD, {"add", pgit(file)});
×
146
    }
147
    QString path = QDir(QtPassSettings::getPassStore()).relativeFilePath(file);
×
148
    path.replace(Util::endsWithGpg(), "");
×
149
    QString msg =
150
        QString(overwrite ? "Edit" : "Add") + " for " + path + " using QtPass.";
×
151
    gitCommit(file, msg);
×
152
  }
153
}
×
154

155
/**
156
 * @brief ImitatePass::gitCommit commit a file to git with an appropriate commit
157
 * message
158
 * @param file
159
 * @param msg
160
 */
161
void ImitatePass::gitCommit(const QString &file, const QString &msg) {
×
162
  if (file.isEmpty()) {
×
163
    executeGit(GIT_COMMIT, {"commit", "-m", msg});
×
164
  } else {
165
    executeGit(GIT_COMMIT, {"commit", "-m", msg, "--", pgit(file)});
×
166
  }
167
}
×
168

169
/**
170
 * @brief ImitatePass::Remove custom implementation of "pass remove"
171
 */
172
void ImitatePass::Remove(QString file, bool isDir) {
×
173
  file = QtPassSettings::getPassStore() + file;
×
174
  transactionHelper trans(this, PASS_REMOVE);
×
175
  if (!isDir) {
×
176
    file += ".gpg";
×
177
  }
178
  if (QtPassSettings::isUseGit()) {
×
179
    executeGit(GIT_RM, {"rm", (isDir ? "-rf" : "-f"), pgit(file)});
×
180
    // Normalize path the same way as add/edit operations
181
    QString path = QDir(QtPassSettings::getPassStore()).relativeFilePath(file);
×
182
    path.replace(Util::endsWithGpg(), "");
×
183
    gitCommit(file, "Remove for " + path + " using QtPass.");
×
184
  } else {
185
    if (isDir) {
×
186
      QDir dir(file);
×
187
      dir.removeRecursively();
×
188
    } else {
×
189
      QFile(file).remove();
×
190
    }
191
  }
192
}
×
193

194
/**
195
 * @brief ImitatePass::Init initialize pass repository
196
 *
197
 * @param path      path in which new password-store will be created
198
 * @param users     list of users who shall be able to decrypt passwords in
199
 * path
200
 */
201
auto ImitatePass::checkSigningKeys(const QStringList &signingKeys) -> bool {
×
202
  QString out;
×
203
  QStringList args =
204
      QStringList{"--status-fd=1", "--list-secret-keys"} + signingKeys;
×
205
  int result =
206
      Executor::executeBlocking(QtPassSettings::getGpgExecutable(), args, &out);
×
207
  if (result != 0) {
×
208
#ifdef QT_DEBUG
209
    dbg() << "GPG list-secret-keys failed with code:" << result;
210
#endif
211
    return false;
212
  }
213
  for (auto &key : signingKeys) {
×
214
    if (out.contains("[GNUPG:] KEY_CONSIDERED " + key)) {
×
215
      return true;
216
    }
217
  }
218
  return false;
219
}
×
220

221
/**
222
 * @brief Writes the selected users' GPG key IDs to a .gpg-id file.
223
 * @details Opens the specified file for writing, stores the key ID of each
224
 * enabled user on a separate line, and warns if none of the selected users has
225
 * a secret key available.
226
 *
227
 * @param QString &gpgIdFile - Path to the .gpg-id file to be written.
228
 * @param QList<UserInfo> &users - List of users to evaluate and write to the
229
 * file.
230
 * @return void - This function does not return a value.
231
 *
232
 */
233
void ImitatePass::writeGpgIdFile(const QString &gpgIdFile,
×
234
                                 const QList<UserInfo> &users) {
235
  QFile gpgId(gpgIdFile);
×
236
  if (!gpgId.open(QIODevice::WriteOnly | QIODevice::Text)) {
×
237
    emit critical(tr("Cannot update"),
×
238
                  tr("Failed to open .gpg-id for writing."));
×
239
    return;
240
  }
241
  bool secret_selected = false;
242
  for (const UserInfo &user : users) {
×
243
    if (user.enabled) {
×
244
      gpgId.write((user.key_id + "\n").toUtf8());
×
245
      secret_selected |= user.have_secret;
×
246
    }
247
  }
248
  gpgId.close();
×
249
  if (!secret_selected) {
×
250
    emit critical(
×
251
        tr("Check selected users!"),
×
252
        tr("None of the selected keys have a secret key available.\n"
×
253
           "You will not be able to decrypt any newly added passwords!"));
254
  }
255
}
×
256

257
/**
258
 * @brief Signs a GPG ID file and verifies its signature.
259
 * @example
260
 * bool result = ImitatePass::signGpgIdFile(gpgIdFile, signingKeys);
261
 * std::cout << result << std::endl; // Expected output: true if signing and
262
 * verification succeed
263
 *
264
 * @param QString &gpgIdFile - Path to the .gpgid file to be signed.
265
 * @param QStringList &signingKeys - List of signing keys; only the first key is
266
 * used.
267
 * @return bool - True if the file was signed and its signature verified
268
 * successfully; otherwise false.
269
 */
270
auto ImitatePass::signGpgIdFile(const QString &gpgIdFile,
×
271
                                const QStringList &signingKeys) -> bool {
272
  QStringList args;
×
273
  // Use only the first signing key; multiple --default-key options would
274
  // override each other and only the last one would take effect.
275
  if (!signingKeys.isEmpty()) {
×
276
#ifdef QT_DEBUG
277
    if (signingKeys.size() > 1) {
278
      dbg() << "Multiple signing keys configured; using only the first key:"
279
            << signingKeys.first();
280
    }
281
#endif
282
    args.append(QStringList{"--default-key", signingKeys.first()});
×
283
  }
284
  args.append(QStringList{"--yes", "--detach-sign", gpgIdFile});
×
285
  int result =
286
      Executor::executeBlocking(QtPassSettings::getGpgExecutable(), args);
×
287
  if (result != 0) {
×
288
#ifdef QT_DEBUG
289
    dbg() << "GPG signing failed with code:" << result;
290
#endif
291
    emit critical(tr("GPG signing failed!"),
×
292
                  tr("Failed to sign %1.").arg(gpgIdFile));
×
293
    return false;
×
294
  }
295
  if (!verifyGpgIdFile(gpgIdFile)) {
×
296
    emit critical(tr("Check .gpgid file signature!"),
×
297
                  tr("Signature for %1 is invalid.").arg(gpgIdFile));
×
298
    return false;
×
299
  }
300
  return true;
301
}
×
302

303
/**
304
 * @brief Adds a GPG ID file and optionally its signature file to git, then
305
 * creates corresponding commit(s).
306
 * @example
307
 * void result = ImitatePass::gitAddGpgId(gpgIdFile, gpgIdSigFile, true, true);
308
 *
309
 * @param const QString &gpgIdFile - Path to the GPG ID file to add and commit.
310
 * @param const QString &gpgIdSigFile - Path to the signature file associated
311
 * with the GPG ID file.
312
 * @param bool addFile - Whether to stage and commit the GPG ID file.
313
 * @param bool addSigFile - Whether to stage and commit the signature file.
314
 * @return void - This function does not return a value.
315
 */
316
void ImitatePass::gitAddGpgId(const QString &gpgIdFile,
×
317
                              const QString &gpgIdSigFile, bool addFile,
318
                              bool addSigFile) {
319
  if (addFile) {
×
320
    executeGit(GIT_ADD, {"add", pgit(gpgIdFile)});
×
321
  }
322
  QString commitPath = gpgIdFile;
323
  commitPath.replace(Util::endsWithGpg(), "");
×
324
  gitCommit(gpgIdFile, "Added " + commitPath + " using QtPass.");
×
325
  if (!addSigFile) {
×
326
    return;
327
  }
328
  executeGit(GIT_ADD, {"add", pgit(gpgIdSigFile)});
×
329
  commitPath = gpgIdSigFile;
×
330
  commitPath.replace(QRegularExpression("\\.gpg$"), "");
×
331
  gitCommit(gpgIdSigFile, "Added " + commitPath + " using QtPass.");
×
332
}
×
333

334
/**
335
 * @brief Initializes the pass entry by writing and optionally signing the GPG
336
 * ID files.
337
 *
338
 * @example
339
 * void result = ImitatePass::Init(path, users);
340
 *
341
 * @param QString path - Base path for the pass entry where ".gpg-id" and
342
 * optional signature files are created.
343
 * @param const QList<UserInfo> &users - List of users whose keys are written
344
 * into the GPG ID file.
345
 * @return void - No return value.
346
 */
347
void ImitatePass::Init(QString path, const QList<UserInfo> &users) {
×
348
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
349
  QStringList signingKeys =
350
      QtPassSettings::getPassSigningKey().split(" ", Qt::SkipEmptyParts);
×
351
#else
352
  QStringList signingKeys =
353
      QtPassSettings::getPassSigningKey().split(" ", QString::SkipEmptyParts);
354
#endif
355
  QString gpgIdSigFile = path + ".gpg-id.sig";
×
356
  bool addSigFile = false;
357
  if (!signingKeys.isEmpty()) {
×
358
    if (!checkSigningKeys(signingKeys)) {
×
359
      emit critical(tr("No signing key!"),
×
360
                    tr("None of the secret signing keys is available.\n"
×
361
                       "You will not be able to change the user list!"));
362
      return;
×
363
    }
364
    QFileInfo checkFile(gpgIdSigFile);
×
365
    if (!checkFile.exists() || !checkFile.isFile()) {
×
366
      addSigFile = true;
367
    }
368
  }
×
369

370
  QString gpgIdFile = path + ".gpg-id";
×
371
  bool addFile = false;
372
  transactionHelper trans(this, PASS_INIT);
×
373
  if (QtPassSettings::isAddGPGId(true)) {
×
374
    QFileInfo checkFile(gpgIdFile);
×
375
    if (!checkFile.exists() || !checkFile.isFile()) {
×
376
      addFile = true;
377
    }
378
  }
×
379
  writeGpgIdFile(gpgIdFile, users);
×
380

381
  if (!signingKeys.isEmpty()) {
×
382
    if (!signGpgIdFile(gpgIdFile, signingKeys)) {
×
383
      return;
384
    }
385
  }
386

387
  if (!QtPassSettings::isUseWebDav() && QtPassSettings::isUseGit() &&
×
388
      !QtPassSettings::getGitExecutable().isEmpty()) {
×
389
    gitAddGpgId(gpgIdFile, gpgIdSigFile, addFile, addSigFile);
×
390
  }
391
  reencryptPath(path);
×
392
}
393

394
/**
395
 * @brief ImitatePass::verifyGpgIdFile verify detached gpgid file signature.
396
 * @param file which gpgid file.
397
 * @return was verification successful?
398
 */
399
auto ImitatePass::verifyGpgIdFile(const QString &file) -> bool {
×
400
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
401
  QStringList signingKeys =
402
      QtPassSettings::getPassSigningKey().split(" ", Qt::SkipEmptyParts);
×
403
#else
404
  QStringList signingKeys =
405
      QtPassSettings::getPassSigningKey().split(" ", QString::SkipEmptyParts);
406
#endif
407
  if (signingKeys.isEmpty()) {
×
408
    return true;
409
  }
410
  QString out;
×
411
  QStringList args =
412
      QStringList{"--verify", "--status-fd=1", pgpg(file) + ".sig", pgpg(file)};
×
413
  int result =
414
      Executor::executeBlocking(QtPassSettings::getGpgExecutable(), args, &out);
×
415
  if (result != 0) {
×
416
#ifdef QT_DEBUG
417
    dbg() << "GPG verify failed with code:" << result;
418
#endif
419
    return false;
420
  }
421
  QRegularExpression re(
422
      R"(^\[GNUPG:\] VALIDSIG ([A-F0-9]{40}) .* ([A-F0-9]{40})\r?$)",
423
      QRegularExpression::MultilineOption);
×
424
  QRegularExpressionMatch m = re.match(out);
×
425
  if (!m.hasMatch()) {
×
426
    return false;
427
  }
428
  QStringList fingerprints = m.capturedTexts();
×
429
  fingerprints.removeFirst();
×
430
  for (auto &key : signingKeys) {
×
431
    if (fingerprints.contains(key)) {
×
432
      return true;
×
433
    }
434
  }
435
  return false;
×
436
}
×
437

438
/**
439
 * @brief ImitatePass::removeDir delete folder recursive.
440
 * @param dirName which folder.
441
 * @return was removal successful?
442
 */
443
auto ImitatePass::removeDir(const QString &dirName) -> bool {
1✔
444
  bool result = true;
445
  QDir dir(dirName);
1✔
446

447
  if (dir.exists(dirName)) {
1✔
448
    for (const QFileInfo &info :
449
         dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden |
450
                               QDir::AllDirs | QDir::Files,
451
                           QDir::DirsFirst)) {
1✔
452
      if (info.isDir()) {
×
453
        result = removeDir(info.absoluteFilePath());
×
454
      } else {
455
        result = QFile::remove(info.absoluteFilePath());
×
456
      }
457

458
      if (!result) {
×
459
        return result;
460
      }
461
    }
462
    result = dir.rmdir(dirName);
1✔
463
  }
464
  return result;
465
}
1✔
466

467
/**
468
 * @brief ImitatePass::reencryptPath reencrypt all files under the chosen
469
 * directory
470
 *
471
 * This is still quite experimental..
472
 * @param dir
473
 */
474
auto ImitatePass::verifyGpgIdForDir(const QString &file,
×
475
                                    QStringList &gpgIdFilesVerified,
476
                                    QStringList &gpgId) -> bool {
477
  QString gpgIdPath = Pass::getGpgIdPath(file);
×
478
  if (gpgIdFilesVerified.contains(gpgIdPath)) {
×
479
    return true;
480
  }
481
  if (!verifyGpgIdFile(gpgIdPath)) {
×
482
    emit critical(tr("Check .gpgid file signature!"),
×
483
                  tr("Signature for %1 is invalid.").arg(gpgIdPath));
×
484
    return false;
×
485
  }
486
  gpgIdFilesVerified.append(gpgIdPath);
487
  gpgId = getRecipientList(file);
×
488
  gpgId.sort();
489
  return true;
490
}
491

492
/**
493
 * @brief Extracts and returns a sorted list of valid key IDs from a GPG key
494
 * listing file.
495
 * @example
496
 * QStringList result = ImitatePass::getKeysFromFile(fileName);
497
 * std::cout << result.join(", ").toStdString() << std::endl;
498
 *
499
 * @param fileName - Path to the file used to query and parse GPG key
500
 * information.
501
 * @return QStringList - A sorted list of 16-character key IDs found in the
502
 * file.
503
 */
504
auto ImitatePass::getKeysFromFile(const QString &fileName) -> QStringList {
×
505
  QStringList args = {
506
      "-v",          "--no-secmem-warning", "--no-permission-warning",
507
      "--list-only", "--keyid-format=long", pgpg(fileName)};
×
508
  QString keys;
×
509
  QString err;
×
NEW
510
  const int result = Executor::executeBlocking(
×
NEW
511
      QtPassSettings::getGpgExecutable(), args, &keys, &err);
×
NEW
512
  if (result != 0 && keys.isEmpty() && err.isEmpty()) {
×
NEW
513
    return QStringList();
×
514
  }
UNCOV
515
  QStringList actualKeys;
×
516
  keys += err;
517
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
518
  QStringList key = keys.split(Util::newLinesRegex(), Qt::SkipEmptyParts);
×
519
#else
520
  QStringList key = keys.split(Util::newLinesRegex(), QString::SkipEmptyParts);
521
#endif
522
  QListIterator<QString> itr(key);
523
  while (itr.hasNext()) {
×
524
    QString current = itr.next();
525
    QStringList cur = current.split(" ");
×
526
    if (cur.length() > 4) {
×
527
      QString actualKey = cur.takeAt(4);
×
528
      if (actualKey.length() == 16) {
×
529
        actualKeys << actualKey;
530
      }
531
    }
532
  }
533
  actualKeys.sort();
534
  return actualKeys;
535
}
×
536

537
/**
538
 * @brief Re-encrypts a single encrypted file for a new set of recipients.
539
 * @example
540
 * bool result = ImitatePass::reencryptSingleFile(fileName, recipients);
541
 * std::cout << result << std::endl; // Expected output: true on success, false
542
 * on failure
543
 *
544
 * @param const QString &fileName - Path to the encrypted file to re-encrypt.
545
 * @param const QStringList &recipients - List of recipient keys to encrypt the
546
 * file to.
547
 * @return bool - True if the file was successfully decrypted, re-encrypted,
548
 * verified, and replaced; otherwise false.
549
 */
550
auto ImitatePass::reencryptSingleFile(const QString &fileName,
×
551
                                      const QStringList &recipients) -> bool {
552
#ifdef QT_DEBUG
553
  dbg() << "reencrypt " << fileName << " for " << recipients;
554
#endif
555
  QString local_lastDecrypt;
×
556
  QStringList args = {
557
      "-d",      "--quiet",     "--yes",       "--no-encrypt-to",
558
      "--batch", "--use-agent", pgpg(fileName)};
×
559
  int result = Executor::executeBlocking(QtPassSettings::getGpgExecutable(),
×
560
                                         args, &local_lastDecrypt);
561

562
  if (result != 0 || local_lastDecrypt.isEmpty()) {
×
563
#ifdef QT_DEBUG
564
    dbg() << "Decrypt error on re-encrypt for:" << fileName;
565
#endif
566
    return false;
567
  }
568

569
  if (local_lastDecrypt.right(1) != "\n") {
×
570
    local_lastDecrypt += "\n";
×
571
  }
572

573
  // Use passed recipients instead of re-reading from file
574
  if (recipients.isEmpty()) {
×
575
    emit critical(tr("Can not edit"),
×
576
                  tr("Could not read encryption key to use, .gpg-id "
×
577
                     "file missing or invalid."));
578
    return false;
×
579
  }
580

581
  // Encrypt to temporary file for atomic replacement
582
  QString tempPath = fileName + ".reencrypt.tmp";
×
583
  args = QStringList{"--yes", "--batch", "-eq", "--output", pgpg(tempPath)};
×
584
  for (const auto &i : recipients) {
×
585
    args.append("-r");
×
586
    args.append(i);
587
  }
588
  args.append("-");
×
589
  result = Executor::executeBlocking(QtPassSettings::getGpgExecutable(), args,
×
590
                                     local_lastDecrypt);
591

592
  if (result != 0) {
×
593
#ifdef QT_DEBUG
594
    dbg() << "Encrypt error on re-encrypt for:" << fileName;
595
#endif
596
    QFile::remove(tempPath);
×
597
    return false;
598
  }
599

600
  // Verify encryption worked by attempting to decrypt the temp file
601
  QString verifyOutput;
×
602
  args = QStringList{"-d", "--quiet", "--batch", "--use-agent", pgpg(tempPath)};
×
603
  result = Executor::executeBlocking(QtPassSettings::getGpgExecutable(), args,
×
604
                                     &verifyOutput);
605
  if (result != 0 || verifyOutput.isEmpty()) {
×
606
#ifdef QT_DEBUG
607
    dbg() << "Verification failed for:" << tempPath;
608
#endif
609
    QFile::remove(tempPath);
×
610
    return false;
611
  }
612
  // Verify content matches original decrypted content (defense in depth)
613
  if (verifyOutput.trimmed() != local_lastDecrypt.trimmed()) {
×
614
#ifdef QT_DEBUG
615
    dbg() << "Verification content mismatch for:" << tempPath;
616
#endif
617
    QFile::remove(tempPath);
×
618
    return false;
619
  }
620

621
  // Atomic replace with backup: rename original to .bak, rename temp to
622
  // original, then remove backup
623
  QString backupPath = fileName + ".reencrypt.bak";
×
624
  if (!QFile::rename(fileName, backupPath)) {
×
625
#ifdef QT_DEBUG
626
    dbg() << "Failed to backup original file:" << fileName;
627
#endif
628
    QFile::remove(tempPath);
×
629
    return false;
630
  }
631
  if (!QFile::rename(tempPath, fileName)) {
×
632
#ifdef QT_DEBUG
633
    dbg() << "Failed to rename temp file to:" << fileName;
634
#endif
635
    // Restore backup and clean up temp file
636
    QFile::rename(backupPath, fileName);
×
637
    QFile::remove(tempPath);
×
638
    emit critical(
×
639
        tr("Re-encryption failed"),
×
640
        tr("Failed to replace %1. Original has been restored.").arg(fileName));
×
641
    return false;
×
642
  }
643
  // Success - remove backup
644
  QFile::remove(backupPath);
×
645

646
  if (!QtPassSettings::isUseWebDav() && QtPassSettings::isUseGit()) {
×
647
    Executor::executeBlocking(QtPassSettings::getGitExecutable(),
×
648
                              {"add", pgit(fileName)});
649
    QString path =
650
        QDir(QtPassSettings::getPassStore()).relativeFilePath(fileName);
×
651
    path.replace(Util::endsWithGpg(), "");
×
652
    Executor::executeBlocking(QtPassSettings::getGitExecutable(),
×
653
                              {"commit", pgit(fileName), "-m",
654
                               "Re-encrypt for " + path + " using QtPass."});
×
655
  }
656

657
  return true;
658
}
×
659

660
/**
661
 * @brief Create git backup commit before re-encryption.
662
 * @return true if backup created or not needed, false if backup failed.
663
 */
664
auto ImitatePass::createBackupCommit() -> bool {
×
665
  if (!QtPassSettings::isUseGit() ||
×
666
      QtPassSettings::getGitExecutable().isEmpty()) {
×
667
    return true;
668
  }
669
  emit statusMsg(tr("Creating backup commit"), 2000);
×
670
  const QString git = QtPassSettings::getGitExecutable();
×
671
  QString statusOut;
×
672
  if (Executor::executeBlocking(git, {"status", "--porcelain"}, &statusOut) !=
×
673
      0) {
674
    emit critical(
×
675
        tr("Backup commit failed"),
×
676
        tr("Could not inspect git status. Re-encryption was aborted."));
×
677
    return false;
×
678
  }
679
  if (!statusOut.trimmed().isEmpty()) {
×
680
    if (Executor::executeBlocking(git, {"add", "-A"}) != 0 ||
×
681
        Executor::executeBlocking(
×
682
            git, {"commit", "-m", "Backup before re-encryption"}) != 0) {
683
      emit critical(tr("Backup commit failed"),
×
684
                    tr("Re-encryption was aborted because a git backup could "
×
685
                       "not be created."));
686
      return false;
×
687
    }
688
  }
689
  return true;
690
}
×
691

692
/**
693
 * @brief Re-encrypts all `.gpg` files under the given directory using the
694
 *        verified GPG key configuration for each folder.
695
 *
696
 * This method optionally pulls the latest changes before starting, creates a
697
 * backup commit, verifies `.gpg-id` files per directory, and re-encrypts files
698
 * whose current recipients do not match the expected keys. It emits progress,
699
 * status, and error signals throughout the process, and optionally pushes the
700
 * updated password-store when finished.
701
 *
702
 * @param dir - Root directory to scan recursively for `.gpg` files.
703
 * @return void
704
 */
705
void ImitatePass::reencryptPath(const QString &dir) {
×
706
  emit statusMsg(tr("Re-encrypting from folder %1").arg(dir), 3000);
×
707
  emit startReencryptPath();
×
708
  if (QtPassSettings::isAutoPull()) {
×
709
    emit statusMsg(tr("Updating password-store"), 2000);
×
710
    GitPull_b();
×
711
  }
712

713
  // Create backup before re-encryption - abort if it fails
714
  if (!createBackupCommit()) {
×
715
    emit endReencryptPath();
×
716
    return;
×
717
  }
718

719
  QDir currentDir;
×
720
  QDirIterator gpgFiles(dir, QStringList() << "*.gpg", QDir::Files,
×
721
                        QDirIterator::Subdirectories);
×
722
  QStringList gpgIdFilesVerified;
×
723
  QStringList gpgId;
×
724
  int successCount = 0;
725
  int failCount = 0;
726
  while (gpgFiles.hasNext()) {
×
727
    QString fileName = gpgFiles.next();
×
728
    if (gpgFiles.fileInfo().path() != currentDir.path()) {
×
729
      if (!verifyGpgIdForDir(fileName, gpgIdFilesVerified, gpgId)) {
×
730
        emit endReencryptPath();
×
731
        return;
732
      }
733
      if (gpgId.isEmpty() && !gpgIdFilesVerified.isEmpty()) {
×
734
        emit critical(tr("GPG ID verification failed"),
×
735
                      tr("Could not verify .gpg-id for directory."));
×
736
        emit endReencryptPath();
×
737
        return;
738
      }
739
    }
740
    QStringList actualKeys = getKeysFromFile(fileName);
×
741
    if (actualKeys != gpgId) {
×
742
      if (reencryptSingleFile(fileName, gpgId)) {
×
743
        successCount++;
×
744
      } else {
745
        failCount++;
×
746
        emit critical(tr("Re-encryption failed"),
×
747
                      tr("Failed to re-encrypt %1").arg(fileName));
×
748
      }
749
    }
750
  }
751

752
  if (failCount > 0) {
×
753
    emit statusMsg(tr("Re-encryption completed: %1 succeeded, %2 failed")
×
754
                       .arg(successCount)
×
755
                       .arg(failCount),
×
756
                   5000);
757
  } else {
758
    emit statusMsg(
×
759
        tr("Re-encryption completed: %1 files re-encrypted").arg(successCount),
×
760
        3000);
761
  }
762

763
  if (QtPassSettings::isAutoPush()) {
×
764
    emit statusMsg(tr("Updating password-store"), 2000);
×
765
    GitPush();
×
766
  }
767
  emit endReencryptPath();
×
768
}
×
769

770
/**
771
 * @brief Resolves the final destination path for moving a file or directory,
772
 * applying .gpg handling for files.
773
 * @example
774
 * QString result = ImitatePass::resolveMoveDestination("/tmp/source.txt",
775
 * "/backup", false); std::cout << result.toStdString() << std::endl; //
776
 * Expected output sample: "/backup/source.txt.gpg"
777
 *
778
 * @param src - Source path to the file or directory.
779
 * @param dest - Requested destination path, which may be a file or directory.
780
 * @param force - When true, allows overwriting an existing destination file.
781
 * @return QString - Resolved destination path, or an empty QString if the
782
 * source/destination is invalid or conflicts occur.
783
 */
784
auto ImitatePass::resolveMoveDestination(const QString &src,
5✔
785
                                         const QString &dest, bool force)
786
    -> QString {
787
  QFileInfo srcFileInfo(src);
5✔
788
  QFileInfo destFileInfo(dest);
5✔
789
  QString destFile;
5✔
790
  QString srcFileBaseName = srcFileInfo.fileName();
5✔
791

792
  if (srcFileInfo.isFile()) {
5✔
793
    if (destFileInfo.isFile()) {
4✔
794
      if (!force) {
2✔
795
#ifdef QT_DEBUG
796
        dbg() << "Destination file already exists";
797
#endif
798
        return QString();
799
      }
800
      destFile = dest;
1✔
801
    } else if (destFileInfo.isDir()) {
2✔
802
      destFile = QDir(dest).filePath(srcFileBaseName);
2✔
803
    } else {
804
      destFile = dest;
1✔
805
    }
806

807
    if (destFile.endsWith(".gpg", Qt::CaseInsensitive)) {
6✔
808
      destFile.chop(4);
3✔
809
    }
810
    destFile.append(".gpg");
3✔
811
  } else if (srcFileInfo.isDir()) {
1✔
812
    if (destFileInfo.isDir()) {
×
813
      destFile = QDir(dest).filePath(srcFileBaseName);
×
814
    } else if (destFileInfo.isFile()) {
×
815
#ifdef QT_DEBUG
816
      dbg() << "Destination is a file";
817
#endif
818
      return QString();
819
    } else {
820
      destFile = dest;
×
821
    }
822
  } else {
823
#ifdef QT_DEBUG
824
    dbg() << "Source file does not exist";
825
#endif
826
    return QString();
827
  }
828
  return destFile;
829
}
5✔
830

831
/**
832
 * @brief Moves a password store item in the Git repository and commits the
833
 * change.
834
 * @example
835
 * void result = className.executeMoveGit(src, destFile, force);
836
 *
837
 * @param const QString &src - Source path of the item to move.
838
 * @param const QString &destFile - Destination path of the item after the move.
839
 * @param bool force - Whether to force the move using Git's -f option.
840
 * @return void - This method does not return a value.
841
 */
842
void ImitatePass::executeMoveGit(const QString &src, const QString &destFile,
×
843
                                 bool force) {
844
  QStringList args;
×
845
  args << "mv";
×
846
  if (force) {
×
847
    args << "-f";
×
848
  }
849
  args << pgit(src);
×
850
  args << pgit(destFile);
×
851
  executeGit(GIT_MOVE, args);
×
852

853
  QString relSrc = QDir(QtPassSettings::getPassStore()).relativeFilePath(src);
×
854
  relSrc.replace(Util::endsWithGpg(), "");
×
855
  QString relDest =
856
      QDir(QtPassSettings::getPassStore()).relativeFilePath(destFile);
×
857
  relDest.replace(Util::endsWithGpg(), "");
×
858
  QString message = QString("Moved for %1 to %2 using QtPass.");
×
859
  message = message.arg(relSrc, relDest);
×
860
  gitCommit("", message);
×
861
}
×
862

863
/**
864
 * @brief Moves a password entry from the source path to the destination path.
865
 * @example
866
 * ImitatePass::Move(src, dest, true);
867
 *
868
 * @param const QString src - The source path or entry name to move.
869
 * @param const QString dest - The destination path or entry name.
870
 * @param const bool force - If true, overwrites an existing destination entry
871
 * when necessary.
872
 * @return void - This function does not return a value.
873
 */
874
void ImitatePass::Move(const QString src, const QString dest,
×
875
                       const bool force) {
876
  transactionHelper trans(this, PASS_MOVE);
×
877
  QString destFile = resolveMoveDestination(src, dest, force);
×
878
  if (destFile.isEmpty()) {
×
879
    return;
880
  }
881

882
#ifdef QT_DEBUG
883
  dbg() << "Move Source: " << src;
884
  dbg() << "Move Destination: " << destFile;
885
#endif
886

887
  if (QtPassSettings::isUseGit()) {
×
888
    executeMoveGit(src, destFile, force);
×
889
  } else {
890
    QDir qDir;
×
891
    if (force) {
×
892
      qDir.remove(destFile);
×
893
    }
894
    qDir.rename(src, destFile);
×
895
  }
×
896
}
897

898
/**
899
 * @brief Copies a file or directory from source to destination, optionally
900
 * forcing overwrite.
901
 * @example
902
 * void result = ImitatePass::Copy(src, dest, force);
903
 *
904
 * @param QString src - Source path to copy from.
905
 * @param QString dest - Destination path to copy to.
906
 * @param bool force - If true, overwrites the destination when it already
907
 * exists.
908
 * @return void - This function does not return a value.
909
 */
910
void ImitatePass::Copy(const QString src, const QString dest,
×
911
                       const bool force) {
912
  QFileInfo destFileInfo(dest);
×
913
  transactionHelper trans(this, PASS_COPY);
×
914
  if (QtPassSettings::isUseGit()) {
×
915
    QStringList args;
×
916
    args << "cp";
×
917
    if (force) {
×
918
      args << "-f";
×
919
    }
920
    args << pgit(src);
×
921
    args << pgit(dest);
×
922
    executeGit(GIT_COPY, args);
×
923

924
    QString message = QString("Copied from %1 to %2 using QtPass.");
×
925
    message = message.arg(src, dest);
×
926
    gitCommit("", message);
×
927
  } else {
928
    QDir qDir;
×
929
    if (force) {
×
930
      qDir.remove(dest);
×
931
    }
932
    QFile::copy(src, dest);
×
933
  }
×
934
  // reecrypt all files under the new folder
935
  if (destFileInfo.isDir()) {
×
936
    reencryptPath(destFileInfo.absoluteFilePath());
×
937
  } else if (destFileInfo.isFile()) {
×
938
    reencryptPath(destFileInfo.dir().path());
×
939
  }
940
}
×
941

942
/**
943
 * @brief ImitatePass::executeGpg easy wrapper for running gpg commands
944
 * @param args
945
 */
946
void ImitatePass::executeGpg(PROCESS id, const QStringList &args, QString input,
×
947
                             bool readStdout, bool readStderr) {
948
  executeWrapper(id, QtPassSettings::getGpgExecutable(), args, std::move(input),
×
949
                 readStdout, readStderr);
950
}
×
951

952
/**
953
 * @brief ImitatePass::executeGit easy wrapper for running git commands
954
 * @param args
955
 */
956
void ImitatePass::executeGit(PROCESS id, const QStringList &args, QString input,
×
957
                             bool readStdout, bool readStderr) {
958
  executeWrapper(id, QtPassSettings::getGitExecutable(), args, std::move(input),
×
959
                 readStdout, readStderr);
960
}
×
961

962
/**
963
 * @brief ImitatePass::finished this function is overloaded to ensure
964
 *                              identical behaviour to RealPass ie. only PASS_*
965
 *                              processes are visible inside Pass::finish, so
966
 *                              that interface-wise it all looks the same
967
 * @param id
968
 * @param exitCode
969
 * @param out
970
 * @param err
971
 */
972
void ImitatePass::finished(int id, int exitCode, const QString &out,
×
973
                           const QString &err) {
974
#ifdef QT_DEBUG
975
  dbg() << "Imitate Pass";
976
#endif
977
  static QString transactionOutput;
×
978
  PROCESS pid = transactionIsOver(static_cast<PROCESS>(id));
×
979
  transactionOutput.append(out);
×
980

981
  if (exitCode == 0) {
×
982
    if (pid == INVALID) {
×
983
      return;
984
    }
985
  } else {
986
    while (pid == INVALID) {
×
987
      id = exec.cancelNext();
×
988
      if (id == -1) {
×
989
        //  this is probably irrecoverable and shall not happen
990
#ifdef QT_DEBUG
991
        dbg() << "No such transaction!";
992
#endif
993
        return;
994
      }
995
      pid = transactionIsOver(static_cast<PROCESS>(id));
×
996
    }
997
  }
998
  Pass::finished(pid, exitCode, transactionOutput, err);
×
999
  transactionOutput.clear();
×
1000
}
1001

1002
/**
1003
 * @brief executeWrapper    overrided so that every execution is a transaction
1004
 * @param id
1005
 * @param app
1006
 * @param args
1007
 * @param input
1008
 * @param readStdout
1009
 * @param readStderr
1010
 */
1011
void ImitatePass::executeWrapper(PROCESS id, const QString &app,
×
1012
                                 const QStringList &args, QString input,
1013
                                 bool readStdout, bool readStderr) {
1014
  transactionAdd(id);
×
1015
  Pass::executeWrapper(id, app, args, input, readStdout, readStderr);
×
1016
}
×
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