QtPass 1.6.0
Multi-platform GUI for pass, the standard unix password manager.
Loading...
Searching...
No Matches
imitatepass.cpp
Go to the documentation of this file.
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 <QElapsedTimer>
9#include <QPointer>
10#include <QRegularExpression>
11#include <QThread>
12#include <utility>
13
14#ifdef QT_DEBUG
15#include "debughelper.h"
16#endif
17
21using Enums::GIT_ADD;
23using Enums::GIT_COPY;
24using Enums::GIT_INIT;
25using Enums::GIT_MOVE;
26using Enums::GIT_PULL;
27using Enums::GIT_PUSH;
28using Enums::GIT_RM;
30using Enums::INVALID;
40
45ImitatePass::ImitatePass() = default;
46
48 static constexpr int kGrepThreadTimeoutMs = 5000;
49 for (QThread *t : std::as_const(m_grepThreads))
50 if (t && t->isRunning())
51 t->requestInterruption();
52 QElapsedTimer elapsed;
53 elapsed.start();
54 for (QThread *t : std::as_const(m_grepThreads)) {
55 if (t && t->isRunning()) {
56 const int remaining =
57 kGrepThreadTimeoutMs - static_cast<int>(elapsed.elapsed());
58 if (remaining > 0)
59 t->wait(remaining);
60 }
61 }
62}
63
64static auto pgit(const QString &path) -> QString {
65 if (!QtPassSettings::getGitExecutable().startsWith("wsl ")) {
66 return path;
67 }
68 QString res = "$(wslpath " + path + ")";
69 return res.replace('\\', '/');
70}
71
72static auto pgpg(const QString &path) -> QString {
73 if (!QtPassSettings::getGpgExecutable().startsWith("wsl ")) {
74 return path;
75 }
76 QString res = "$(wslpath " + path + ")";
77 return res.replace('\\', '/');
78}
79
86
91
98
104 executeGit(GIT_PUSH, {"push"});
105 }
106}
107
111void ImitatePass::Show(QString file) {
112 file = QtPassSettings::getPassStore() + file + ".gpg";
113 QStringList args = {"-d", "--quiet", "--yes", "--no-encrypt-to",
114 "--batch", "--use-agent", pgpg(file)};
115 executeGpg(PASS_SHOW, args);
116}
117
121void ImitatePass::OtpGenerate(QString file) {
122#ifdef QT_DEBUG
123 dbg() << "No OTP generation code for fake pass yet, attempting for file: " +
124 file;
125#else
126 Q_UNUSED(file)
127#endif
128}
129
137void ImitatePass::Insert(QString file, QString newValue, bool overwrite) {
138 file = file + ".gpg";
139 QString gpgIdPath = Pass::getGpgIdPath(file);
140 if (!verifyGpgIdFile(gpgIdPath)) {
141 emit critical(tr("Check .gpgid file signature!"),
142 tr("Signature for %1 is invalid.").arg(gpgIdPath));
143 return;
144 }
145 transactionHelper trans(this, PASS_INSERT);
146 QStringList recipients = Pass::getRecipientList(file);
147 if (recipients.isEmpty()) {
148 // Already emit critical signal to notify user of error - no need to throw
149 emit critical(tr("Can not edit"),
150 tr("Could not read encryption key to use, .gpg-id "
151 "file missing or invalid."));
152 return;
153 }
154 QStringList args = {"--batch", "--status-fd", "2",
155 "-eq", "--output", pgpg(file)};
156 for (auto &r : recipients) {
157 args.append("-r");
158 args.append(r);
159 }
160 if (overwrite) {
161 args.append("--yes");
162 }
163 args.append("-");
164 executeGpg(PASS_INSERT, args, newValue);
166 // Git is used when enabled - this is the standard pass workflow
167 if (!overwrite) {
168 executeGit(GIT_ADD, {"add", pgit(file)});
169 }
170 QString path = QDir(QtPassSettings::getPassStore()).relativeFilePath(file);
171 path.replace(Util::endsWithGpg(), "");
172 QString msg =
173 QString(overwrite ? "Edit" : "Add") + " for " + path + " using QtPass.";
174 gitCommit(file, msg);
175 }
176}
177
184void ImitatePass::gitCommit(const QString &file, const QString &msg) {
185 if (file.isEmpty()) {
186 executeGit(GIT_COMMIT, {"commit", "-m", msg});
187 } else {
188 executeGit(GIT_COMMIT, {"commit", "-m", msg, "--", pgit(file)});
189 }
190}
191
195void ImitatePass::Remove(QString file, bool isDir) {
196 file = QtPassSettings::getPassStore() + file;
197 transactionHelper trans(this, PASS_REMOVE);
198 if (!isDir) {
199 file += ".gpg";
200 }
202 executeGit(GIT_RM, {"rm", (isDir ? "-rf" : "-f"), pgit(file)});
203 // Normalize path the same way as add/edit operations
204 QString path = QDir(QtPassSettings::getPassStore()).relativeFilePath(file);
205 path.replace(Util::endsWithGpg(), "");
206 gitCommit(file, "Remove for " + path + " using QtPass.");
207 } else {
208 if (isDir) {
209 QDir dir(file);
210 dir.removeRecursively();
211 } else {
212 QFile(file).remove();
213 }
214 }
215}
216
224auto ImitatePass::checkSigningKeys(const QStringList &signingKeys) -> bool {
225 QString out;
226 QStringList args =
227 QStringList{"--status-fd=1", "--list-secret-keys"} + signingKeys;
228 int result =
230 if (result != 0) {
231#ifdef QT_DEBUG
232 dbg() << "GPG list-secret-keys failed with code:" << result;
233#endif
234 return false;
235 }
236 for (auto &key : signingKeys) {
237 if (out.contains("[GNUPG:] KEY_CONSIDERED " + key)) {
238 return true;
239 }
240 }
241 return false;
242}
243
256void ImitatePass::writeGpgIdFile(const QString &gpgIdFile,
257 const QList<UserInfo> &users) {
258 QFile gpgId(gpgIdFile);
259 if (!gpgId.open(QIODevice::WriteOnly | QIODevice::Text)) {
260 emit critical(tr("Cannot update"),
261 tr("Failed to open .gpg-id for writing."));
262 return;
263 }
264 bool secret_selected = false;
265 for (const UserInfo &user : users) {
266 if (user.enabled) {
267 gpgId.write((user.key_id + "\n").toUtf8());
268 secret_selected |= user.have_secret;
269 }
270 }
271 gpgId.close();
272 if (!secret_selected) {
273 emit critical(
274 tr("Check selected users!"),
275 tr("None of the selected keys have a secret key available.\n"
276 "You will not be able to decrypt any newly added passwords!"));
277 }
278}
279
293auto ImitatePass::signGpgIdFile(const QString &gpgIdFile,
294 const QStringList &signingKeys) -> bool {
295 QStringList args;
296 // Use only the first signing key; multiple --default-key options would
297 // override each other and only the last one would take effect.
298 if (!signingKeys.isEmpty()) {
299#ifdef QT_DEBUG
300 if (signingKeys.size() > 1) {
301 dbg() << "Multiple signing keys configured; using only the first key:"
302 << signingKeys.first();
303 }
304#endif
305 args.append(QStringList{"--default-key", signingKeys.first()});
306 }
307 args.append(QStringList{"--yes", "--detach-sign", gpgIdFile});
308 int result =
310 if (result != 0) {
311#ifdef QT_DEBUG
312 dbg() << "GPG signing failed with code:" << result;
313#endif
314 emit critical(tr("GPG signing failed!"),
315 tr("Failed to sign %1.").arg(gpgIdFile));
316 return false;
317 }
318 if (!verifyGpgIdFile(gpgIdFile)) {
319 emit critical(tr("Check .gpgid file signature!"),
320 tr("Signature for %1 is invalid.").arg(gpgIdFile));
321 return false;
322 }
323 return true;
324}
325
339void ImitatePass::gitAddGpgId(const QString &gpgIdFile,
340 const QString &gpgIdSigFile, bool addFile,
341 bool addSigFile) {
342 if (addFile) {
343 executeGit(GIT_ADD, {"add", pgit(gpgIdFile)});
344 }
345 QString commitPath = gpgIdFile;
346 commitPath.replace(Util::endsWithGpg(), "");
347 gitCommit(gpgIdFile, "Added " + commitPath + " using QtPass.");
348 if (!addSigFile) {
349 return;
350 }
351 executeGit(GIT_ADD, {"add", pgit(gpgIdSigFile)});
352 commitPath = gpgIdSigFile;
353 commitPath.replace(QRegularExpression("\\.gpg$"), "");
354 gitCommit(gpgIdSigFile, "Added " + commitPath + " using QtPass.");
355}
356
370void ImitatePass::Init(QString path, const QList<UserInfo> &users) {
371#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
372 QStringList signingKeys =
373 QtPassSettings::getPassSigningKey().split(" ", Qt::SkipEmptyParts);
374#else
375 QStringList signingKeys =
376 QtPassSettings::getPassSigningKey().split(" ", QString::SkipEmptyParts);
377#endif
378 QString gpgIdSigFile = path + ".gpg-id.sig";
379 bool addSigFile = false;
380 if (!signingKeys.isEmpty()) {
381 if (!checkSigningKeys(signingKeys)) {
382 emit critical(tr("No signing key!"),
383 tr("None of the secret signing keys is available.\n"
384 "You will not be able to change the user list!"));
385 return;
386 }
387 QFileInfo checkFile(gpgIdSigFile);
388 if (!checkFile.exists() || !checkFile.isFile()) {
389 addSigFile = true;
390 }
391 }
392
393 QString gpgIdFile = path + ".gpg-id";
394 bool addFile = false;
395 transactionHelper trans(this, PASS_INIT);
396 if (QtPassSettings::isAddGPGId(true)) {
397 QFileInfo checkFile(gpgIdFile);
398 if (!checkFile.exists() || !checkFile.isFile()) {
399 addFile = true;
400 }
401 }
402 writeGpgIdFile(gpgIdFile, users);
403
404 if (!signingKeys.isEmpty()) {
405 if (!signGpgIdFile(gpgIdFile, signingKeys)) {
406 return;
407 }
408 }
409
412 gitAddGpgId(gpgIdFile, gpgIdSigFile, addFile, addSigFile);
413 }
414 reencryptPath(path);
415}
416
422auto ImitatePass::verifyGpgIdFile(const QString &file) -> bool {
423#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
424 QStringList signingKeys =
425 QtPassSettings::getPassSigningKey().split(" ", Qt::SkipEmptyParts);
426#else
427 QStringList signingKeys =
428 QtPassSettings::getPassSigningKey().split(" ", QString::SkipEmptyParts);
429#endif
430 if (signingKeys.isEmpty()) {
431 return true;
432 }
433 QString out;
434 QStringList args =
435 QStringList{"--verify", "--status-fd=1", pgpg(file) + ".sig", pgpg(file)};
436 int result =
438 if (result != 0) {
439#ifdef QT_DEBUG
440 dbg() << "GPG verify failed with code:" << result;
441#endif
442 return false;
443 }
444 QRegularExpression re(
445 R"(^\‍[GNUPG:\‍] VALIDSIG ([A-F0-9]{40}) .* ([A-F0-9]{40})\r?$)",
446 QRegularExpression::MultilineOption);
447 QRegularExpressionMatch m = re.match(out);
448 if (!m.hasMatch()) {
449 return false;
450 }
451 QStringList fingerprints = m.capturedTexts();
452 fingerprints.removeFirst();
453 for (auto &key : signingKeys) {
454 if (fingerprints.contains(key)) {
455 return true;
456 }
457 }
458 return false;
459}
460
466auto ImitatePass::removeDir(const QString &dirName) -> bool {
467 bool result = true;
468 QDir dir(dirName);
469
470 if (dir.exists(dirName)) {
471 for (const QFileInfo &info :
472 dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden |
473 QDir::AllDirs | QDir::Files,
474 QDir::DirsFirst)) {
475 if (info.isDir()) {
476 result = removeDir(info.absoluteFilePath());
477 } else {
478 result = QFile::remove(info.absoluteFilePath());
479 }
480
481 if (!result) {
482 return result;
483 }
484 }
485 result = dir.rmdir(dirName);
486 }
487 return result;
488}
489
497auto ImitatePass::verifyGpgIdForDir(const QString &file,
498 QStringList &gpgIdFilesVerified,
499 QStringList &gpgId) -> bool {
500 QString gpgIdPath = Pass::getGpgIdPath(file);
501 if (gpgIdFilesVerified.contains(gpgIdPath)) {
502 return true;
503 }
504 if (!verifyGpgIdFile(gpgIdPath)) {
505 emit critical(tr("Check .gpgid file signature!"),
506 tr("Signature for %1 is invalid.").arg(gpgIdPath));
507 return false;
508 }
509 gpgIdFilesVerified.append(gpgIdPath);
510 gpgId = getRecipientList(file);
511 gpgId.sort();
512 return true;
513}
514
527auto ImitatePass::getKeysFromFile(const QString &fileName) -> QStringList {
528 QStringList args = {
529 "-v", "--no-secmem-warning", "--no-permission-warning",
530 "--list-only", "--keyid-format=long", pgpg(fileName)};
531 QString keys;
532 QString err;
533 const int result = Executor::executeBlocking(
534 QtPassSettings::getGpgExecutable(), args, &keys, &err);
535 if (result != 0 && keys.isEmpty() && err.isEmpty()) {
536 return QStringList();
537 }
538 QStringList actualKeys;
539 keys += err;
540#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
541 QStringList key = keys.split(Util::newLinesRegex(), Qt::SkipEmptyParts);
542#else
543 QStringList key = keys.split(Util::newLinesRegex(), QString::SkipEmptyParts);
544#endif
545 QListIterator<QString> itr(key);
546 while (itr.hasNext()) {
547 QString current = itr.next();
548 QStringList cur = current.split(" ");
549 if (cur.length() > 4) {
550 QString actualKey = cur.takeAt(4);
551 if (actualKey.length() == 16) {
552 actualKeys << actualKey;
553 }
554 }
555 }
556 actualKeys.sort();
557 return actualKeys;
558}
559
573auto ImitatePass::reencryptSingleFile(const QString &fileName,
574 const QStringList &recipients) -> bool {
575#ifdef QT_DEBUG
576 dbg() << "reencrypt " << fileName << " for " << recipients;
577#endif
578 QString local_lastDecrypt;
579 QStringList args = {
580 "-d", "--quiet", "--yes", "--no-encrypt-to",
581 "--batch", "--use-agent", pgpg(fileName)};
583 args, &local_lastDecrypt);
584
585 if (result != 0 || local_lastDecrypt.isEmpty()) {
586#ifdef QT_DEBUG
587 dbg() << "Decrypt error on re-encrypt for:" << fileName;
588#endif
589 return false;
590 }
591
592 if (local_lastDecrypt.right(1) != "\n") {
593 local_lastDecrypt += "\n";
594 }
595
596 // Use passed recipients instead of re-reading from file
597 if (recipients.isEmpty()) {
598 emit critical(tr("Can not edit"),
599 tr("Could not read encryption key to use, .gpg-id "
600 "file missing or invalid."));
601 return false;
602 }
603
604 // Encrypt to temporary file for atomic replacement
605 QString tempPath = fileName + ".reencrypt.tmp";
606 args = QStringList{"--yes", "--batch", "-eq", "--output", pgpg(tempPath)};
607 for (const auto &i : recipients) {
608 args.append("-r");
609 args.append(i);
610 }
611 args.append("-");
613 local_lastDecrypt);
614
615 if (result != 0) {
616#ifdef QT_DEBUG
617 dbg() << "Encrypt error on re-encrypt for:" << fileName;
618#endif
619 QFile::remove(tempPath);
620 return false;
621 }
622
623 // Verify encryption worked by attempting to decrypt the temp file
624 QString verifyOutput;
625 args = QStringList{"-d", "--quiet", "--batch", "--use-agent", pgpg(tempPath)};
627 &verifyOutput);
628 if (result != 0 || verifyOutput.isEmpty()) {
629#ifdef QT_DEBUG
630 dbg() << "Verification failed for:" << tempPath;
631#endif
632 QFile::remove(tempPath);
633 return false;
634 }
635 // Verify content matches original decrypted content (defense in depth)
636 if (verifyOutput.trimmed() != local_lastDecrypt.trimmed()) {
637#ifdef QT_DEBUG
638 dbg() << "Verification content mismatch for:" << tempPath;
639#endif
640 QFile::remove(tempPath);
641 return false;
642 }
643
644 // Atomic replace with backup: rename original to .bak, rename temp to
645 // original, then remove backup
646 QString backupPath = fileName + ".reencrypt.bak";
647 if (!QFile::rename(fileName, backupPath)) {
648#ifdef QT_DEBUG
649 dbg() << "Failed to backup original file:" << fileName;
650#endif
651 QFile::remove(tempPath);
652 return false;
653 }
654 if (!QFile::rename(tempPath, fileName)) {
655#ifdef QT_DEBUG
656 dbg() << "Failed to rename temp file to:" << fileName;
657#endif
658 // Restore backup and clean up temp file
659 QFile::rename(backupPath, fileName);
660 QFile::remove(tempPath);
661 emit critical(
662 tr("Re-encryption failed"),
663 tr("Failed to replace %1. Original has been restored.").arg(fileName));
664 return false;
665 }
666 // Success - remove backup
667 QFile::remove(backupPath);
668
671 {"add", pgit(fileName)});
672 QString path =
673 QDir(QtPassSettings::getPassStore()).relativeFilePath(fileName);
674 path.replace(Util::endsWithGpg(), "");
676 {"commit", pgit(fileName), "-m",
677 "Re-encrypt for " + path + " using QtPass."});
678 }
679
680 return true;
681}
682
690 return true;
691 }
692 emit statusMsg(tr("Creating backup commit"), 2000);
693 const QString git = QtPassSettings::getGitExecutable();
694 QString statusOut;
695 if (Executor::executeBlocking(git, {"status", "--porcelain"}, &statusOut) !=
696 0) {
697 emit critical(
698 tr("Backup commit failed"),
699 tr("Could not inspect git status. Re-encryption was aborted."));
700 return false;
701 }
702 if (!statusOut.trimmed().isEmpty()) {
703 if (Executor::executeBlocking(git, {"add", "-A"}) != 0 ||
705 git, {"commit", "-m", "Backup before re-encryption"}) != 0) {
706 emit critical(tr("Backup commit failed"),
707 tr("Re-encryption was aborted because a git backup could "
708 "not be created."));
709 return false;
710 }
711 }
712 return true;
713}
714
728void ImitatePass::reencryptPath(const QString &dir) {
729 emit statusMsg(tr("Re-encrypting from folder %1").arg(dir), 3000);
730 emit startReencryptPath();
732 emit statusMsg(tr("Updating password-store"), 2000);
733 GitPull_b();
734 }
735
736 // Create backup before re-encryption - abort if it fails
737 if (!createBackupCommit()) {
738 emit endReencryptPath();
739 return;
740 }
741
742 QDir currentDir;
743 QDirIterator gpgFiles(dir, QStringList() << "*.gpg", QDir::Files,
744 QDirIterator::Subdirectories);
745 QStringList gpgIdFilesVerified;
746 QStringList gpgId;
747 int successCount = 0;
748 int failCount = 0;
749 while (gpgFiles.hasNext()) {
750 QString fileName = gpgFiles.next();
751 if (gpgFiles.fileInfo().path() != currentDir.path()) {
752 if (!verifyGpgIdForDir(fileName, gpgIdFilesVerified, gpgId)) {
753 emit endReencryptPath();
754 return;
755 }
756 if (gpgId.isEmpty() && !gpgIdFilesVerified.isEmpty()) {
757 emit critical(tr("GPG ID verification failed"),
758 tr("Could not verify .gpg-id for directory."));
759 emit endReencryptPath();
760 return;
761 }
762 }
763 QStringList actualKeys = getKeysFromFile(fileName);
764 if (actualKeys != gpgId) {
765 if (reencryptSingleFile(fileName, gpgId)) {
766 successCount++;
767 } else {
768 failCount++;
769 emit critical(tr("Re-encryption failed"),
770 tr("Failed to re-encrypt %1").arg(fileName));
771 }
772 }
773 }
774
775 if (failCount > 0) {
776 emit statusMsg(tr("Re-encryption completed: %1 succeeded, %2 failed")
777 .arg(successCount)
778 .arg(failCount),
779 5000);
780 } else {
781 emit statusMsg(
782 tr("Re-encryption completed: %1 files re-encrypted").arg(successCount),
783 3000);
784 }
785
787 emit statusMsg(tr("Updating password-store"), 2000);
788 GitPush();
789 }
790 emit endReencryptPath();
791}
792
808 const QString &dest, bool force)
809 -> QString {
810 QFileInfo srcFileInfo(src);
811 QFileInfo destFileInfo(dest);
812 QString destFile;
813 QString srcFileBaseName = srcFileInfo.fileName();
814
815 if (srcFileInfo.isFile()) {
816 if (destFileInfo.isFile()) {
817 if (!force) {
818#ifdef QT_DEBUG
819 dbg() << "Destination file already exists";
820#endif
821 return QString();
822 }
823 destFile = dest;
824 } else if (destFileInfo.isDir()) {
825 destFile = QDir(dest).filePath(srcFileBaseName);
826 } else {
827 destFile = dest;
828 }
829
830 if (destFile.endsWith(".gpg", Qt::CaseInsensitive)) {
831 destFile.chop(4);
832 }
833 destFile.append(".gpg");
834 } else if (srcFileInfo.isDir()) {
835 if (destFileInfo.isDir()) {
836 destFile = QDir(dest).filePath(srcFileBaseName);
837 } else if (destFileInfo.isFile()) {
838#ifdef QT_DEBUG
839 dbg() << "Destination is a file";
840#endif
841 return QString();
842 } else {
843 destFile = dest;
844 }
845 } else {
846#ifdef QT_DEBUG
847 dbg() << "Source file does not exist";
848#endif
849 return QString();
850 }
851 return destFile;
852}
853
865void ImitatePass::executeMoveGit(const QString &src, const QString &destFile,
866 bool force) {
867 QStringList args;
868 args << "mv";
869 if (force) {
870 args << "-f";
871 }
872 args << pgit(src);
873 args << pgit(destFile);
874 executeGit(GIT_MOVE, args);
875
876 QString relSrc = QDir(QtPassSettings::getPassStore()).relativeFilePath(src);
877 relSrc.replace(Util::endsWithGpg(), "");
878 QString relDest =
879 QDir(QtPassSettings::getPassStore()).relativeFilePath(destFile);
880 relDest.replace(Util::endsWithGpg(), "");
881 QString message = QString("Moved for %1 to %2 using QtPass.");
882 message = message.arg(relSrc, relDest);
883 gitCommit("", message);
884}
885
897void ImitatePass::Move(const QString src, const QString dest,
898 const bool force) {
899 transactionHelper trans(this, PASS_MOVE);
900 QString destFile = resolveMoveDestination(src, dest, force);
901 if (destFile.isEmpty()) {
902 return;
903 }
904
905#ifdef QT_DEBUG
906 dbg() << "Move Source: " << src;
907 dbg() << "Move Destination: " << destFile;
908#endif
909
911 executeMoveGit(src, destFile, force);
912 } else {
913 QDir qDir;
914 if (force) {
915 qDir.remove(destFile);
916 }
917 qDir.rename(src, destFile);
918 }
919}
920
933void ImitatePass::Copy(const QString src, const QString dest,
934 const bool force) {
935 QFileInfo destFileInfo(dest);
936 transactionHelper trans(this, PASS_COPY);
938 QStringList args;
939 args << "cp";
940 if (force) {
941 args << "-f";
942 }
943 args << pgit(src);
944 args << pgit(dest);
945 executeGit(GIT_COPY, args);
946
947 QString message = QString("Copied from %1 to %2 using QtPass.");
948 message = message.arg(src, dest);
949 gitCommit("", message);
950 } else {
951 QDir qDir;
952 if (force) {
953 qDir.remove(dest);
954 }
955 QFile::copy(src, dest);
956 }
957 // reecrypt all files under the new folder
958 if (destFileInfo.isDir()) {
959 reencryptPath(destFileInfo.absoluteFilePath());
960 } else if (destFileInfo.isFile()) {
961 reencryptPath(destFileInfo.dir().path());
962 }
963}
964
969void ImitatePass::executeGpg(PROCESS id, const QStringList &args, QString input,
970 bool readStdout, bool readStderr) {
971 executeWrapper(id, QtPassSettings::getGpgExecutable(), args, std::move(input),
972 readStdout, readStderr);
973}
974
979void ImitatePass::executeGit(PROCESS id, const QStringList &args, QString input,
980 bool readStdout, bool readStderr) {
981 executeWrapper(id, QtPassSettings::getGitExecutable(), args, std::move(input),
982 readStdout, readStderr);
983}
984
995void ImitatePass::finished(int id, int exitCode, const QString &out,
996 const QString &err) {
997#ifdef QT_DEBUG
998 dbg() << "Imitate Pass";
999#endif
1000 static QString transactionOutput;
1001 PROCESS pid = transactionIsOver(static_cast<PROCESS>(id));
1002 transactionOutput.append(out);
1003
1004 if (exitCode == 0) {
1005 if (pid == INVALID) {
1006 return;
1007 }
1008 } else {
1009 while (pid == INVALID) {
1010 id = exec.cancelNext();
1011 if (id == -1) {
1012 // this is probably irrecoverable and shall not happen
1013#ifdef QT_DEBUG
1014 dbg() << "No such transaction!";
1015#endif
1016 return;
1017 }
1018 pid = transactionIsOver(static_cast<PROCESS>(id));
1019 }
1020 }
1021 Pass::finished(pid, exitCode, transactionOutput, err);
1022 transactionOutput.clear();
1023}
1024
1034void ImitatePass::executeWrapper(PROCESS id, const QString &app,
1035 const QStringList &args, QString input,
1036 bool readStdout, bool readStderr) {
1037 transactionAdd(id);
1038 Pass::executeWrapper(id, app, args, input, readStdout, readStderr);
1039}
1040
1044auto ImitatePass::grepMatchFile(const QStringList &env, const QString &gpgExe,
1045 const QString &filePath,
1046 const QRegularExpression &rx) -> QStringList {
1047 QString plaintext;
1048 const int rc =
1049 Executor::executeBlocking(env, gpgExe,
1050 {"-d", "--quiet", "--yes", "--no-encrypt-to",
1051 "--batch", "--use-agent", pgpg(filePath)},
1052 &plaintext);
1053 if (rc != 0 || plaintext.isEmpty())
1054 return {};
1055 QStringList matches;
1056 for (const QString &line : plaintext.split('\n')) {
1057 QString candidate = line;
1058 if (candidate.endsWith('\r'))
1059 candidate.chop(1);
1060 const QString t = candidate.trimmed();
1061 if (!t.isEmpty() && candidate.contains(rx))
1062 matches << t;
1063 }
1064 return matches;
1065}
1066
1070auto ImitatePass::grepScanStore(const QStringList &env, const QString &gpgExe,
1071 const QString &storeDir,
1072 const QRegularExpression &rx)
1073 -> QList<QPair<QString, QStringList>> {
1074 QList<QPair<QString, QStringList>> results;
1075 QDirIterator it(storeDir, QStringList() << "*.gpg", QDir::Files,
1076 QDirIterator::Subdirectories);
1077 while (it.hasNext()) {
1078 if (QThread::currentThread()->isInterruptionRequested())
1079 return {};
1080 const QString filePath = it.next();
1081 const QStringList matches = grepMatchFile(env, gpgExe, filePath, rx);
1082 if (!matches.isEmpty()) {
1083 QString entry = QDir(storeDir).relativeFilePath(filePath);
1084 if (entry.endsWith(QLatin1String(".gpg")))
1085 entry.chop(4);
1086 results.append({entry, matches});
1087 }
1088 }
1089 return results;
1090}
1091
1099void ImitatePass::Grep(QString pattern, bool caseInsensitive) {
1100 for (QThread *t : std::as_const(m_grepThreads))
1101 if (t && t->isRunning())
1102 t->requestInterruption();
1103 // No wait() — blocking the UI thread while GPG decrypts would freeze the
1104 // interface. Stale results are discarded via the sequence counter.
1105
1106 // Advance the sequence before any early return so in-flight workers from the
1107 // previous query fail the seq check and cannot publish stale results.
1108 const int seq = ++m_grepSeq;
1109
1110 // Use trimmed() rather than isEmpty(): a whitespace-only string is a valid
1111 // regex that matches every non-empty line, which is almost never intentional
1112 // and would decrypt the entire store.
1113 //
1114 // Both early returns post finishedGrep via Qt::QueuedConnection so that the
1115 // signal is always delivered asynchronously after Grep() returns, matching
1116 // the contract of the threaded path.
1117 if (pattern.trimmed().isEmpty()) {
1118 QMetaObject::invokeMethod(
1119 this,
1120 [this, seq]() {
1121 if (m_grepSeq == seq)
1122 emit finishedGrep({});
1123 },
1124 Qt::QueuedConnection);
1125 return;
1126 }
1127
1128 const QRegularExpression rx(
1129 pattern, caseInsensitive ? QRegularExpression::CaseInsensitiveOption
1130 : QRegularExpression::PatternOptions{});
1131 if (!rx.isValid()) {
1132 QMetaObject::invokeMethod(
1133 this,
1134 [this, seq]() {
1135 if (m_grepSeq == seq)
1136 emit finishedGrep({});
1137 },
1138 Qt::QueuedConnection);
1139 return;
1140 }
1141 const QString gpgExe = QtPassSettings::getGpgExecutable();
1142 const QString storeDir = QtPassSettings::getPassStore();
1143 const QStringList env = exec.environment();
1144 QPointer<ImitatePass> self(this);
1145
1146 auto emitResults = [self, seq](QList<QPair<QString, QStringList>> results) {
1147 if (!self)
1148 return;
1149 QMetaObject::invokeMethod(
1150 self,
1151 [self, seq, results = std::move(results)]() {
1152 if (self && self->m_grepSeq == seq)
1153 emit self->finishedGrep(results);
1154 },
1155 Qt::QueuedConnection);
1156 };
1157
1158 QThread *thread = QThread::create([gpgExe, storeDir, env, rx, emitResults]() {
1159 emitResults(grepScanStore(env, gpgExe, storeDir, rx));
1160 });
1161
1162 m_grepThreads.append(thread);
1163 connect(thread, &QThread::finished, thread, &QObject::deleteLater);
1164 connect(thread, &QThread::finished, this,
1165 [this, thread]() { m_grepThreads.removeOne(thread); });
1166 thread->start();
1167}
static auto executeBlocking(const QString &app, const QStringList &args, const QString &input=QString(), QString *process_out=nullptr, QString *process_err=nullptr) -> int
Executor::executeBlocking blocking version of the executor, takes input and presents it as stdin.
Definition executor.cpp:223
RAII helper for wrapping operations in transactions.
auto createBackupCommit() -> bool
Create git backup commit before re-encryption.
void OtpGenerate(QString file) override
Generate OTP.
void GitInit() override
Initialize Git repository.
void executeGpg(PROCESS id, const QStringList &args, QString input=QString(), bool readStdout=true, bool readStderr=true)
Execute GPG command.
void GitPull_b() override
Pull with rebase.
~ImitatePass() override
Destructor.
auto checkSigningKeys(const QStringList &signingKeys) -> bool
Check if signing keys are valid.
ImitatePass()
Construct ImitatePass instance.
auto reencryptSingleFile(const QString &fileName, const QStringList &recipients) -> bool
Re-encrypt single file with new recipients.
auto verifyGpgIdFile(const QString &file) -> bool
Verify .gpg-id file exists and is valid.
void executeGit(PROCESS id, const QStringList &args, QString input=QString(), bool readStdout=true, bool readStderr=true)
Execute git command.
void Init(QString path, const QList< UserInfo > &users) override
Initialize store.
void Grep(QString pattern, bool caseInsensitive=false) override
Search all password content by GPG-decrypting each .gpg file.
auto verifyGpgIdForDir(const QString &file, QStringList &gpgIdFilesVerified, QStringList &gpgId) -> bool
Verify .gpg-id file for a directory.
void Insert(QString file, QString newValue, bool overwrite=false) override
Insert new password.
auto removeDir(const QString &dirName) -> bool
Remove directory recursively.
void executeMoveGit(const QString &src, const QString &destFile, bool force)
Execute git move operation.
void Move(const QString src, const QString dest, const bool force=false) override
Move password file.
void Copy(const QString src, const QString dest, const bool force=false) override
Copy password file.
void endReencryptPath()
Emitted after finishing re-encryption.
void finished(int id, int exitCode, const QString &out, const QString &err) override
Handle process completion.
void executeWrapper(PROCESS id, const QString &app, const QStringList &args, QString input, bool readStdout=true, bool readStderr=true) override
Execute command wrapper.
auto signGpgIdFile(const QString &gpgIdFile, const QStringList &signingKeys) -> bool
Sign .gpg-id file with signing keys.
void Remove(QString file, bool isDir=false) override
Remove password.
void GitPull() override
Pull from remote.
void Show(QString file) override
Show decrypted password.
auto resolveMoveDestination(const QString &src, const QString &dest, bool force) -> QString
Resolve destination for move operation.
void GitPush() override
Push to remote.
void writeGpgIdFile(const QString &gpgIdFile, const QList< UserInfo > &users)
Write recipients to .gpg-id file.
void gitAddGpgId(const QString &gpgIdFile, const QString &gpgIdSigFile, bool addFile, bool addSigFile)
Add .gpg-id to git staging.
void startReencryptPath()
Emitted before starting re-encryption.
auto getKeysFromFile(const QString &fileName) -> QStringList
Read recipients from file.
void reencryptPath(const QString &dir)
Re-encrypt entire directory.
void gitCommit(const QString &file, const QString &msg)
Commit changes to git.
void critical(const QString &, const QString &)
Emit critical error.
Enums::PROCESS PROCESS
Definition pass.h:44
void statusMsg(const QString &, int)
Emit status message.
void executeWrapper(PROCESS id, const QString &app, const QStringList &args, bool readStdout=true, bool readStderr=true)
Execute external wrapper command.
Definition pass.cpp:96
static auto getRecipientList(const QString &for_file) -> QStringList
Get list of recipients for a password file.
Definition pass.cpp:778
void finishedGrep(const QList< QPair< QString, QStringList > > &results)
Emitted when grep finishes with matching results.
static auto getGpgIdPath(const QString &for_file) -> QString
Get .gpg-id file path for a password file.
Definition pass.cpp:748
Executor exec
Definition pass.h:42
virtual void finished(int id, int exitCode, const QString &out, const QString &err)
Handle process completion.
Definition pass.cpp:606
static auto isAutoPull(const bool &defaultValue=QVariant().toBool()) -> bool
Check whether automatic pull is enabled.
static auto isUseGit(const bool &defaultValue=QVariant().toBool()) -> bool
Check whether Git integration is enabled.
static auto getPassStore(const QString &defaultValue=QVariant().toString()) -> QString
Get password store directory path.
static auto isAddGPGId(const bool &defaultValue=QVariant().toBool()) -> bool
Get whether to auto-add GPG ID when receiving files.
static auto getGpgExecutable(const QString &defaultValue=QVariant().toString()) -> QString
Get GPG executable path.
static auto getPassSigningKey(const QString &defaultValue=QVariant().toString()) -> QString
Get GPG signing key for pass.
static auto isUseWebDav(const bool &defaultValue=QVariant().toBool()) -> bool
Check whether WebDAV integration is enabled.
static auto isAutoPush(const bool &defaultValue=QVariant().toBool()) -> bool
Check whether automatic push is enabled.
static auto getGitExecutable(const QString &defaultValue=QVariant().toString()) -> QString
Get git executable path.
static auto endsWithGpg() -> const QRegularExpression &
Returns a regex to match .gpg file extensions.
Definition util.cpp:252
static auto newLinesRegex() -> const QRegularExpression &
Returns a regex to match newline characters.
Definition util.cpp:275
auto transactionIsOver(Enums::PROCESS) -> Enums::PROCESS
transactionIsOver checks wheather currently finished process is last in current transaction
void transactionAdd(Enums::PROCESS)
transactionAdd If called after call to transactionStart() and before transactionEnd(),...
Debug utilities for QtPass.
#define dbg()
Simple debug macro that includes file and line number.
Definition debughelper.h:21
@ PASS_INIT
Definition enums.h:36
@ PASS_INSERT
Definition enums.h:34
@ GIT_INIT
Definition enums.h:27
@ PASS_COPY
Definition enums.h:39
@ PASS_MOVE
Definition enums.h:38
@ GIT_MOVE
Definition enums.h:40
@ PASS_REMOVE
Definition enums.h:35
@ GIT_COPY
Definition enums.h:41
@ INVALID
Definition enums.h:45
@ GIT_COMMIT
Definition enums.h:29
@ GIT_RM
Definition enums.h:30
@ PASS_SHOW
Definition enums.h:33
@ GIT_ADD
Definition enums.h:28
@ GIT_PULL
Definition enums.h:31
@ GIT_PUSH
Definition enums.h:32
@ CLIPBOARD_ALWAYS
Definition enums.h:18
@ CLIPBOARD_NEVER
Definition enums.h:17
@ CLIPBOARD_ON_DEMAND
Definition enums.h:19
@ PASS_INIT
Definition enums.h:36
@ PASS_OTP_GENERATE
Definition enums.h:42
@ PASS_INSERT
Definition enums.h:34
@ GIT_INIT
Definition enums.h:27
@ PASS_COPY
Definition enums.h:39
@ PASS_MOVE
Definition enums.h:38
@ GIT_MOVE
Definition enums.h:40
@ PASS_GREP
Definition enums.h:43
@ GPG_GENKEYS
Definition enums.h:37
@ PASS_REMOVE
Definition enums.h:35
@ GIT_COPY
Definition enums.h:41
@ INVALID
Definition enums.h:45
@ GIT_COMMIT
Definition enums.h:29
@ GIT_RM
Definition enums.h:30
@ PASS_SHOW
Definition enums.h:33
@ GIT_ADD
Definition enums.h:28
@ PROCESS_COUNT
Definition enums.h:44
@ GIT_PULL
Definition enums.h:31
@ GIT_PUSH
Definition enums.h:32
Stores key info lines including validity, creation date and more.
Definition userinfo.h:13