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

IJHack / QtPass / 24593019107

18 Apr 2026 12:52AM UTC coverage: 22.91% (+1.0%) from 21.908%
24593019107

Pull #1037

github

web-flow
Merge 1732d6c55 into 79d758a1a
Pull Request #1037: feat: implement pass grep content search (#109)

72 of 143 new or added lines in 5 files covered. (50.35%)

198 existing lines in 4 files now uncovered.

1296 of 5657 relevant lines covered (22.91%)

8.62 hits per line

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

15.27
/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 "executor.h"
5
#include "qtpasssettings.h"
6
#include "util.h"
7
#include <QDirIterator>
8
#include <QPointer>
9
#include <QRegularExpression>
10
#include <QThread>
11
#include <utility>
12

13
#ifdef QT_DEBUG
14
#include "debughelper.h"
15
#endif
16

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

40
/**
41
 * @brief ImitatePass::ImitatePass for situations when pass is not available
42
 * we imitate the behavior of pass https://www.passwordstore.org/
43
 */
44
ImitatePass::ImitatePass() = default;
42✔
45

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

54
static auto pgpg(const QString &path) -> QString {
1✔
55
  if (!QtPassSettings::getGpgExecutable().startsWith("wsl ")) {
3✔
56
    return path;
57
  }
58
  QString res = "$(wslpath " + path + ")";
×
59
  return res.replace('\\', '/');
×
60
}
61

62
/**
63
 * @brief ImitatePass::GitInit git init wrapper
64
 */
65
void ImitatePass::GitInit() {
×
66
  executeGit(GIT_INIT, {"init", pgit(QtPassSettings::getPassStore())});
×
67
}
×
68

69
/**
70
 * @brief ImitatePass::GitPull git pull wrapper
71
 */
72
void ImitatePass::GitPull() { executeGit(GIT_PULL, {"pull"}); }
×
73

74
/**
75
 * @brief ImitatePass::GitPull_b git pull wrapper
76
 */
77
void ImitatePass::GitPull_b() {
×
78
  Executor::executeBlocking(QtPassSettings::getGitExecutable(), {"pull"});
×
79
}
×
80

81
/**
82
 * @brief ImitatePass::GitPush git push wrapper
83
 */
84
void ImitatePass::GitPush() {
×
85
  if (QtPassSettings::isUseGit()) {
×
86
    executeGit(GIT_PUSH, {"push"});
×
87
  }
88
}
×
89

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

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

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

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

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

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

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

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

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

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

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

386
  if (!signingKeys.isEmpty()) {
×
387
    if (!signGpgIdFile(gpgIdFile, signingKeys)) {
×
388
      return;
389
    }
390
  }
391

392
  if (!QtPassSettings::isUseWebDav() && QtPassSettings::isUseGit() &&
×
393
      !QtPassSettings::getGitExecutable().isEmpty()) {
×
394
    gitAddGpgId(gpgIdFile, gpgIdSigFile, addFile, addSigFile);
×
395
  }
396
  reencryptPath(path);
×
397
}
398

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

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

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

463
      if (!result) {
×
464
        return result;
465
      }
466
    }
467
    result = dir.rmdir(dirName);
1✔
468
  }
469
  return result;
470
}
1✔
471

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

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

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

567
  if (result != 0 || local_lastDecrypt.isEmpty()) {
×
568
#ifdef QT_DEBUG
569
    dbg() << "Decrypt error on re-encrypt for:" << fileName;
570
#endif
571
    return false;
572
  }
573

574
  if (local_lastDecrypt.right(1) != "\n") {
×
575
    local_lastDecrypt += "\n";
×
576
  }
577

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

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

597
  if (result != 0) {
×
598
#ifdef QT_DEBUG
599
    dbg() << "Encrypt error on re-encrypt for:" << fileName;
600
#endif
601
    QFile::remove(tempPath);
×
602
    return false;
603
  }
604

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

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

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

662
  return true;
663
}
×
664

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

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

718
  // Create backup before re-encryption - abort if it fails
719
  if (!createBackupCommit()) {
×
720
    emit endReencryptPath();
×
721
    return;
×
722
  }
723

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

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

768
  if (QtPassSettings::isAutoPush()) {
×
769
    emit statusMsg(tr("Updating password-store"), 2000);
×
770
    GitPush();
×
771
  }
772
  emit endReencryptPath();
×
773
}
×
774

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

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

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

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

858
  QString relSrc = QDir(QtPassSettings::getPassStore()).relativeFilePath(src);
×
859
  relSrc.replace(Util::endsWithGpg(), "");
×
860
  QString relDest =
861
      QDir(QtPassSettings::getPassStore()).relativeFilePath(destFile);
×
862
  relDest.replace(Util::endsWithGpg(), "");
×
863
  QString message = QString("Moved for %1 to %2 using QtPass.");
×
864
  message = message.arg(relSrc, relDest);
×
865
  gitCommit("", message);
×
866
}
×
867

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

887
#ifdef QT_DEBUG
888
  dbg() << "Move Source: " << src;
889
  dbg() << "Move Destination: " << destFile;
890
#endif
891

892
  if (QtPassSettings::isUseGit()) {
×
893
    executeMoveGit(src, destFile, force);
×
894
  } else {
895
    QDir qDir;
×
896
    if (force) {
×
897
      qDir.remove(destFile);
×
898
    }
899
    qDir.rename(src, destFile);
×
900
  }
×
901
}
902

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

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

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

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

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

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

1007
/**
1008
 * @brief executeWrapper    overrided so that every execution is a transaction
1009
 * @param id
1010
 * @param app
1011
 * @param args
1012
 * @param input
1013
 * @param readStdout
1014
 * @param readStderr
1015
 */
1016
void ImitatePass::executeWrapper(PROCESS id, const QString &app,
×
1017
                                 const QStringList &args, QString input,
1018
                                 bool readStdout, bool readStderr) {
1019
  transactionAdd(id);
×
1020
  Pass::executeWrapper(id, app, args, input, readStdout, readStderr);
×
1021
}
×
1022

1023
/**
1024
 * @brief Decrypt one .gpg file and return lines matching rx.
1025
 */
1026
auto ImitatePass::grepMatchFile(const QStringList &env, const QString &gpgExe,
1✔
1027
                                const QString &filePath,
1028
                                const QRegularExpression &rx) -> QStringList {
1029
  QString plaintext;
1✔
1030
  const int rc =
1031
      Executor::executeBlocking(env, gpgExe,
9✔
1032
                                {"-d", "--quiet", "--yes", "--no-encrypt-to",
1033
                                 "--batch", "--use-agent", pgpg(filePath)},
1034
                                &plaintext);
1035
  if (rc != 0 || plaintext.isEmpty())
1✔
1036
    return {};
1✔
NEW
1037
  QStringList matches;
×
NEW
1038
  for (const QString &line : plaintext.split('\n')) {
×
1039
    const QString t = line.trimmed();
NEW
1040
    if (!t.isEmpty() && t.contains(rx))
×
1041
      matches << t;
1042
  }
1043
  return matches;
1044
}
1✔
1045

1046
/**
1047
 * @brief Walk the store, decrypt every .gpg file, collect matches.
1048
 */
1049
auto ImitatePass::grepScanStore(const QStringList &env, const QString &gpgExe,
2✔
1050
                                const QString &storeDir,
1051
                                const QRegularExpression &rx)
1052
    -> QList<QPair<QString, QStringList>> {
1053
  QList<QPair<QString, QStringList>> results;
2✔
1054
  QDirIterator it(storeDir, QStringList() << "*.gpg", QDir::Files,
4✔
1055
                  QDirIterator::Subdirectories);
2✔
1056
  while (it.hasNext()) {
2✔
NEW
1057
    if (QThread::currentThread()->isInterruptionRequested())
×
NEW
1058
      return {};
×
NEW
1059
    const QString filePath = it.next();
×
NEW
1060
    const QStringList matches = grepMatchFile(env, gpgExe, filePath, rx);
×
NEW
1061
    if (!matches.isEmpty()) {
×
NEW
1062
      QString entry = QDir(storeDir).relativeFilePath(filePath);
×
NEW
1063
      if (entry.endsWith(QLatin1String(".gpg")))
×
NEW
1064
        entry.chop(4);
×
NEW
1065
      results.append({entry, matches});
×
1066
    }
1067
  }
1068
  return results;
1069
}
2✔
1070

1071
/**
1072
 * @brief Search all password content by GPG-decrypting each .gpg file.
1073
 *
1074
 * Runs a background thread to avoid blocking the UI. Results are emitted on
1075
 * the main thread via QMetaObject::invokeMethod. A sequence counter discards
1076
 * results from superseded searches.
1077
 */
1078
void ImitatePass::Grep(QString pattern, bool caseInsensitive) {
2✔
1079
  if (m_grepThread && m_grepThread->isRunning())
2✔
NEW
1080
    m_grepThread->requestInterruption();
×
1081

1082
  const int seq = ++m_grepSeq;
2✔
1083
  const QString gpgExe = QtPassSettings::getGpgExecutable();
4✔
1084
  const QString storeDir = QtPassSettings::getPassStore();
4✔
1085
  const QStringList env = exec.environment();
2✔
1086
  QPointer<ImitatePass> self(this);
1087

1088
  auto emitResults = [self, seq](QList<QPair<QString, QStringList>> results) {
2✔
1089
    if (!self)
2✔
1090
      return;
1091
    QMetaObject::invokeMethod(
2✔
1092
        self,
1093
        [self, seq, results = std::move(results)]() {
6✔
1094
          if (self && self->m_grepSeq == seq)
4✔
1095
            emit self->finishedGrep(results);
2✔
1096
        },
2✔
1097
        Qt::QueuedConnection);
1098
  };
2✔
1099

1100
  QThread *thread = QThread::create([self, seq, pattern, caseInsensitive,
4✔
1101
                                     gpgExe, storeDir, env, emitResults]() {
1102
    const QRegularExpression rx(
1103
        pattern, caseInsensitive ? QRegularExpression::CaseInsensitiveOption
4✔
1104
                                 : QRegularExpression::PatternOptions{});
4✔
1105
    if (!rx.isValid()) {
2✔
1106
      if (self)
1✔
1107
        emitResults({});
2✔
1108
      return;
1109
    }
1110
    if (self)
1✔
1111
      emitResults(grepScanStore(env, gpgExe, storeDir, rx));
2✔
1112
  });
2✔
1113

1114
  m_grepThread = thread;
2✔
1115
  connect(thread, &QThread::finished, this, [this, thread]() {
2✔
1116
    if (m_grepThread == thread)
2✔
1117
      m_grepThread = nullptr;
2✔
1118
    thread->deleteLater();
2✔
1119
  });
2✔
1120
  thread->start();
2✔
1121
}
2✔
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