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

IJHack / QtPass / 24609334109

18 Apr 2026 04:50PM UTC coverage: 22.734% (+0.8%) from 21.908%
24609334109

Pull #1037

github

web-flow
Merge f564bc578 into 68ca2a489
Pull Request #1037: feat: implement pass grep content search (#109)

76 of 192 new or added lines in 7 files covered. (39.58%)

401 existing lines in 9 files now uncovered.

1299 of 5714 relevant lines covered (22.73%)

8.6 hits per line

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

15.57
/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
ImitatePass::~ImitatePass() {
22✔
47
  if (m_grepThread && m_grepThread->isRunning()) {
21✔
NEW
48
    m_grepThread->requestInterruption();
×
NEW
49
    m_grepThread->wait();
×
50
  }
51
}
22✔
52

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

61
static auto pgpg(const QString &path) -> QString {
1✔
62
  if (!QtPassSettings::getGpgExecutable().startsWith("wsl ")) {
3✔
63
    return path;
64
  }
65
  QString res = "$(wslpath " + path + ")";
×
66
  return res.replace('\\', '/');
×
67
}
68

69
/**
70
 * @brief ImitatePass::GitInit git init wrapper
71
 */
72
void ImitatePass::GitInit() {
×
73
  executeGit(GIT_INIT, {"init", pgit(QtPassSettings::getPassStore())});
×
74
}
×
75

76
/**
77
 * @brief ImitatePass::GitPull git pull wrapper
78
 */
79
void ImitatePass::GitPull() { executeGit(GIT_PULL, {"pull"}); }
×
80

81
/**
82
 * @brief ImitatePass::GitPull_b git pull wrapper
83
 */
84
void ImitatePass::GitPull_b() {
×
85
  Executor::executeBlocking(QtPassSettings::getGitExecutable(), {"pull"});
×
86
}
×
87

88
/**
89
 * @brief ImitatePass::GitPush git push wrapper
90
 */
91
void ImitatePass::GitPush() {
×
92
  if (QtPassSettings::isUseGit()) {
×
93
    executeGit(GIT_PUSH, {"push"});
×
94
  }
95
}
×
96

97
/**
98
 * @brief ImitatePass::Show shows content of file
99
 */
100
void ImitatePass::Show(QString file) {
×
101
  file = QtPassSettings::getPassStore() + file + ".gpg";
×
102
  QStringList args = {"-d",      "--quiet",     "--yes",   "--no-encrypt-to",
103
                      "--batch", "--use-agent", pgpg(file)};
×
104
  executeGpg(PASS_SHOW, args);
×
105
}
×
106

107
/**
108
 * @brief ImitatePass::OtpGenerate generates an otp code
109
 */
110
void ImitatePass::OtpGenerate(QString file) {
×
111
#ifdef QT_DEBUG
112
  dbg() << "No OTP generation code for fake pass yet, attempting for file: " +
113
               file;
114
#else
115
  Q_UNUSED(file)
116
#endif
117
}
×
118

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

167
/**
168
 * @brief ImitatePass::gitCommit commit a file to git with an appropriate commit
169
 * message
170
 * @param file
171
 * @param msg
172
 */
173
void ImitatePass::gitCommit(const QString &file, const QString &msg) {
×
174
  if (file.isEmpty()) {
×
175
    executeGit(GIT_COMMIT, {"commit", "-m", msg});
×
176
  } else {
177
    executeGit(GIT_COMMIT, {"commit", "-m", msg, "--", pgit(file)});
×
178
  }
179
}
×
180

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

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

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

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

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

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

382
  QString gpgIdFile = path + ".gpg-id";
×
383
  bool addFile = false;
384
  transactionHelper trans(this, PASS_INIT);
×
385
  if (QtPassSettings::isAddGPGId(true)) {
×
386
    QFileInfo checkFile(gpgIdFile);
×
387
    if (!checkFile.exists() || !checkFile.isFile()) {
×
388
      addFile = true;
389
    }
390
  }
×
391
  writeGpgIdFile(gpgIdFile, users);
×
392

393
  if (!signingKeys.isEmpty()) {
×
394
    if (!signGpgIdFile(gpgIdFile, signingKeys)) {
×
395
      return;
396
    }
397
  }
398

399
  if (!QtPassSettings::isUseWebDav() && QtPassSettings::isUseGit() &&
×
400
      !QtPassSettings::getGitExecutable().isEmpty()) {
×
401
    gitAddGpgId(gpgIdFile, gpgIdSigFile, addFile, addSigFile);
×
402
  }
403
  reencryptPath(path);
×
404
}
405

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

450
/**
451
 * @brief ImitatePass::removeDir delete folder recursive.
452
 * @param dirName which folder.
453
 * @return was removal successful?
454
 */
455
auto ImitatePass::removeDir(const QString &dirName) -> bool {
1✔
456
  bool result = true;
457
  QDir dir(dirName);
1✔
458

459
  if (dir.exists(dirName)) {
1✔
460
    for (const QFileInfo &info :
461
         dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden |
462
                               QDir::AllDirs | QDir::Files,
463
                           QDir::DirsFirst)) {
1✔
464
      if (info.isDir()) {
×
465
        result = removeDir(info.absoluteFilePath());
×
466
      } else {
467
        result = QFile::remove(info.absoluteFilePath());
×
468
      }
469

470
      if (!result) {
×
471
        return result;
472
      }
473
    }
474
    result = dir.rmdir(dirName);
1✔
475
  }
476
  return result;
477
}
1✔
478

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

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

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

574
  if (result != 0 || local_lastDecrypt.isEmpty()) {
×
575
#ifdef QT_DEBUG
576
    dbg() << "Decrypt error on re-encrypt for:" << fileName;
577
#endif
578
    return false;
579
  }
580

581
  if (local_lastDecrypt.right(1) != "\n") {
×
582
    local_lastDecrypt += "\n";
×
583
  }
584

585
  // Use passed recipients instead of re-reading from file
586
  if (recipients.isEmpty()) {
×
587
    emit critical(tr("Can not edit"),
×
588
                  tr("Could not read encryption key to use, .gpg-id "
×
589
                     "file missing or invalid."));
590
    return false;
×
591
  }
592

593
  // Encrypt to temporary file for atomic replacement
594
  QString tempPath = fileName + ".reencrypt.tmp";
×
595
  args = QStringList{"--yes", "--batch", "-eq", "--output", pgpg(tempPath)};
×
596
  for (const auto &i : recipients) {
×
597
    args.append("-r");
×
598
    args.append(i);
599
  }
600
  args.append("-");
×
601
  result = Executor::executeBlocking(QtPassSettings::getGpgExecutable(), args,
×
602
                                     local_lastDecrypt);
603

604
  if (result != 0) {
×
605
#ifdef QT_DEBUG
606
    dbg() << "Encrypt error on re-encrypt for:" << fileName;
607
#endif
608
    QFile::remove(tempPath);
×
609
    return false;
610
  }
611

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

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

658
  if (!QtPassSettings::isUseWebDav() && QtPassSettings::isUseGit()) {
×
659
    Executor::executeBlocking(QtPassSettings::getGitExecutable(),
×
660
                              {"add", pgit(fileName)});
661
    QString path =
662
        QDir(QtPassSettings::getPassStore()).relativeFilePath(fileName);
×
663
    path.replace(Util::endsWithGpg(), "");
×
664
    Executor::executeBlocking(QtPassSettings::getGitExecutable(),
×
665
                              {"commit", pgit(fileName), "-m",
666
                               "Re-encrypt for " + path + " using QtPass."});
×
667
  }
668

669
  return true;
670
}
×
671

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

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

725
  // Create backup before re-encryption - abort if it fails
726
  if (!createBackupCommit()) {
×
727
    emit endReencryptPath();
×
728
    return;
×
729
  }
730

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

764
  if (failCount > 0) {
×
765
    emit statusMsg(tr("Re-encryption completed: %1 succeeded, %2 failed")
×
766
                       .arg(successCount)
×
767
                       .arg(failCount),
×
768
                   5000);
769
  } else {
770
    emit statusMsg(
×
771
        tr("Re-encryption completed: %1 files re-encrypted").arg(successCount),
×
772
        3000);
773
  }
774

775
  if (QtPassSettings::isAutoPush()) {
×
776
    emit statusMsg(tr("Updating password-store"), 2000);
×
777
    GitPush();
×
778
  }
779
  emit endReencryptPath();
×
780
}
×
781

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

804
  if (srcFileInfo.isFile()) {
5✔
805
    if (destFileInfo.isFile()) {
4✔
806
      if (!force) {
2✔
807
#ifdef QT_DEBUG
808
        dbg() << "Destination file already exists";
809
#endif
810
        return QString();
811
      }
812
      destFile = dest;
1✔
813
    } else if (destFileInfo.isDir()) {
2✔
814
      destFile = QDir(dest).filePath(srcFileBaseName);
2✔
815
    } else {
816
      destFile = dest;
1✔
817
    }
818

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

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

865
  QString relSrc = QDir(QtPassSettings::getPassStore()).relativeFilePath(src);
×
866
  relSrc.replace(Util::endsWithGpg(), "");
×
867
  QString relDest =
868
      QDir(QtPassSettings::getPassStore()).relativeFilePath(destFile);
×
869
  relDest.replace(Util::endsWithGpg(), "");
×
870
  QString message = QString("Moved for %1 to %2 using QtPass.");
×
871
  message = message.arg(relSrc, relDest);
×
872
  gitCommit("", message);
×
873
}
×
874

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

894
#ifdef QT_DEBUG
895
  dbg() << "Move Source: " << src;
896
  dbg() << "Move Destination: " << destFile;
897
#endif
898

899
  if (QtPassSettings::isUseGit()) {
×
900
    executeMoveGit(src, destFile, force);
×
901
  } else {
902
    QDir qDir;
×
903
    if (force) {
×
904
      qDir.remove(destFile);
×
905
    }
906
    qDir.rename(src, destFile);
×
907
  }
×
908
}
909

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

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

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

964
/**
965
 * @brief ImitatePass::executeGit easy wrapper for running git commands
966
 * @param args
967
 */
968
void ImitatePass::executeGit(PROCESS id, const QStringList &args, QString input,
×
969
                             bool readStdout, bool readStderr) {
970
  executeWrapper(id, QtPassSettings::getGitExecutable(), args, std::move(input),
×
971
                 readStdout, readStderr);
972
}
×
973

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

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

1014
/**
1015
 * @brief executeWrapper    overrided so that every execution is a transaction
1016
 * @param id
1017
 * @param app
1018
 * @param args
1019
 * @param input
1020
 * @param readStdout
1021
 * @param readStderr
1022
 */
1023
void ImitatePass::executeWrapper(PROCESS id, const QString &app,
×
1024
                                 const QStringList &args, QString input,
1025
                                 bool readStdout, bool readStderr) {
1026
  transactionAdd(id);
×
1027
  Pass::executeWrapper(id, app, args, input, readStdout, readStderr);
×
1028
}
×
1029

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

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

1078
/**
1079
 * @brief Search all password content by GPG-decrypting each .gpg file.
1080
 *
1081
 * Runs a background thread to avoid blocking the UI. Results are emitted on
1082
 * the main thread via QMetaObject::invokeMethod. A sequence counter discards
1083
 * results from superseded searches.
1084
 */
1085
void ImitatePass::Grep(QString pattern, bool caseInsensitive) {
2✔
1086
  if (m_grepThread && m_grepThread->isRunning())
2✔
NEW
1087
    m_grepThread->requestInterruption();
×
1088
  // No wait() here — blocking the UI thread while GPG decrypts would freeze
1089
  // the interface. Stale results are discarded via the sequence counter.
1090

1091
  const int seq = ++m_grepSeq;
2✔
1092
  const QString gpgExe = QtPassSettings::getGpgExecutable();
4✔
1093
  const QString storeDir = QtPassSettings::getPassStore();
4✔
1094
  const QStringList env = exec.environment();
2✔
1095
  QPointer<ImitatePass> self(this);
1096

1097
  auto emitResults = [self, seq](QList<QPair<QString, QStringList>> results) {
2✔
1098
    if (!self)
2✔
1099
      return;
1100
    QMetaObject::invokeMethod(
2✔
1101
        self,
1102
        [self, seq, results = std::move(results)]() {
6✔
1103
          if (self && self->m_grepSeq == seq)
4✔
1104
            emit self->finishedGrep(results);
2✔
1105
        },
2✔
1106
        Qt::QueuedConnection);
1107
  };
2✔
1108

1109
  QThread *thread = QThread::create([self, seq, pattern, caseInsensitive,
4✔
1110
                                     gpgExe, storeDir, env, emitResults]() {
1111
    const QRegularExpression rx(
1112
        pattern, caseInsensitive ? QRegularExpression::CaseInsensitiveOption
4✔
1113
                                 : QRegularExpression::PatternOptions{});
4✔
1114
    if (!rx.isValid()) {
2✔
1115
      if (self)
1✔
1116
        emitResults({});
2✔
1117
      return;
1118
    }
1119
    if (self)
1✔
1120
      emitResults(grepScanStore(env, gpgExe, storeDir, rx));
2✔
1121
  });
2✔
1122

1123
  m_grepThread = thread;
2✔
1124
  connect(thread, &QThread::finished, thread, &QObject::deleteLater);
2✔
1125
  connect(thread, &QThread::finished, this, [this, thread]() {
2✔
1126
    if (m_grepThread == thread)
2✔
1127
      m_grepThread = nullptr;
2✔
1128
  });
1129
  thread->start();
2✔
1130
}
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