QtPass Developer documentation
QtPass test coverage
Current view: top level - src/realpass.cpp (source / functions) Coverage Total Hit
Test: QtPass Lines: 97.5 % 237 231
Test Date: 2026-11-14 13:19:18 Functions: 100.0 % 28 28

            Line data    Source code
       1              : // SPDX-FileCopyrightText: 2016 Anne Jan Brouwer
       2              : // SPDX-License-Identifier: GPL-3.0-or-later
       3              : /**
       4              :  * @file
       5              :  * @brief Implements RealPass: builds the `pass` command lines and does itself
       6              :  * the few things `pass` must not be trusted with, such as removing a linked
       7              :  * folder without following it.
       8              :  */
       9              : #include "realpass.h"
      10              : #include "executor.h"
      11              : #include "gpgidsigner.h"
      12              : #include "pathvalidator.h"
      13              : #include "qtpasslogging.h"
      14              : #include "util.h"
      15              : 
      16              : #include <QCoreApplication>
      17              : #include <QCryptographicHash>
      18              : #include <QDir>
      19              : #include <QFile>
      20              : #include <QFileInfo>
      21              : #include <QProcess>
      22              : #include <QRegularExpression>
      23              : #include <utility>
      24              : 
      25              : using Enums::GIT_COMMIT;
      26              : using Enums::GIT_INIT;
      27              : using Enums::GIT_PULL;
      28              : using Enums::GIT_PUSH;
      29              : using Enums::PASS_COPY;
      30              : using Enums::PASS_INIT;
      31              : using Enums::PASS_INSERT;
      32              : using Enums::PASS_MOVE;
      33              : using Enums::PASS_REMOVE;
      34              : using Enums::PASS_SHOW;
      35              : 
      36           32 : RealPass::RealPass() : m_grep(this) {
      37           32 :   connect(&m_grep, &NativeGrep::finished, this, &RealPass::finishedGrep);
      38           32 :   connect(&exec, &Executor::dequeuing, this, &RealPass::onDequeuing);
      39           32 : }
      40              : 
      41           84 : RealPass::~RealPass() { m_grep.cancel(); }
      42              : 
      43            6 : void RealPass::GitInit() { executePass(GIT_INIT, {"git", "init"}); }
      44              : 
      45            2 : void RealPass::GitPull_b() {
      46              :   int result =
      47           12 :       Executor::executeBlocking(m_settings.passExecutable, {"git", "pull"});
      48            2 :   if (result != 0) {
      49            2 :     qCDebug(lcQtPass) << "Git pull failed with code:" << result;
      50              :   }
      51            2 : }
      52              : 
      53           12 : void RealPass::GitPull() { executePass(GIT_PULL, {"git", "pull"}); }
      54              : 
      55            6 : void RealPass::GitPush() { executePass(GIT_PUSH, {"git", "push"}); }
      56              : 
      57            4 : void RealPass::Show(QString file) {
      58              :   // pass follows links as readily as gpg does.
      59            8 :   if (refuseLinkedPath(file + ".gpg")) {
      60              :     return;
      61              :   }
      62              :   queueShow(file);
      63           10 :   executePass(PASS_SHOW, {"show", file}, "", true);
      64            2 : }
      65              : 
      66           44 : auto RealPass::listSigner() -> GpgIdSigner {
      67              :   return {m_settings.gpgExecutable,
      68           88 :           GpgIdSigner::keysFromSetting(m_settings.passSigningKey),
      69           44 :           [this](const QString &app, const QStringList &args,
      70              :                  const QString &input, QString *out, QString *err) {
      71           22 :             QProcess gpg;
      72           22 :             gpg.setProcessEnvironment(exec.environment());
      73           44 :             return Executor::executeBlocking(gpg, app, args, input, out, err);
      74          110 :           }};
      75              : }
      76              : 
      77           30 : auto RealPass::refuseList(const QString &file, const GpgIdSigner &signer,
      78              :                           QSet<QString> &verified, const QString &within,
      79              :                           const QString &except) -> bool {
      80           30 :   if (refuseSkippedGpgId(file, true, except)) {
      81              :     return true;
      82              :   }
      83           29 :   const QString gpgIdFile = Pass::getGpgIdPath(file, m_settings.passStore);
      84              :   const QString absolute =
      85           58 :       QDir::cleanPath(QFileInfo(gpgIdFile).absoluteFilePath());
      86           28 :   if (verified.contains(gpgIdFile) ||
      87            7 :       (!except.isEmpty() &&
      88          106 :        absolute == QDir::cleanPath(QFileInfo(except).absoluteFilePath())) ||
      89           13 :       (!within.isEmpty() && !absolute.startsWith(within))) {
      90              :     return false;
      91              :   }
      92              :   QString why =
      93           22 :       QCoreApplication::translate("ImitatePass", "Signature for %1 is invalid.")
      94           22 :           .arg(gpgIdFile);
      95           22 :   QByteArray contents;
      96           22 :   if (!verifySignedList(signer, gpgIdFile, &contents, &why)) {
      97           11 :     emit critical(QCoreApplication::translate("ImitatePass",
      98              :                                               "Check .gpg-id file signature!"),
      99              :                   why);
     100           11 :     return true;
     101              :   }
     102           11 :   verified.insert(gpgIdFile);
     103           11 :   return false;
     104              : }
     105              : 
     106           28 : auto RealPass::refuseUnverifiedList(const QString &file) -> bool {
     107              :   // pass verifies the signature itself, but knows nothing of the generation
     108              :   // and folder lines: an older authentic list put back, or one signed for
     109              :   // another folder, would get the secret (#2077). The same checks as the
     110              :   // native backend, with gpg in the store's environment. pass reads the list
     111              :   // again afterwards; a swap in between is a concurrent writer, out of
     112              :   // scope (SECURITY.md).
     113           28 :   const GpgIdSigner signer = listSigner();
     114           28 :   if (!signer.enabled()) {
     115              :     return false;
     116              :   }
     117              :   QSet<QString> verified;
     118           22 :   return refuseList(file, signer, verified);
     119           28 : }
     120              : 
     121              : namespace {
     122              : /// @p dir and every folder under it as pass's reencrypt_path() walks them
     123              : /// (find without -L): hidden ones included, .git left out, no link
     124              : /// followed. Each entry pass re-encrypts lies in one of them, and its list
     125              : /// is that folder's nearest.
     126           19 : auto passFoldersUnder(const QString &dir) -> QStringList {
     127           38 :   QStringList folders{QDir::cleanPath(dir)};
     128           38 :   const QFileInfoList children = QDir(dir).entryInfoList(
     129           19 :       QDir::Dirs | QDir::Hidden | QDir::System | QDir::NoDotAndDotDot);
     130           26 :   for (const QFileInfo &child : children) {
     131            7 :     if (child.isSymLink() || child.isJunction() ||
     132           14 :         child.fileName() == QLatin1String(".git")) {
     133            0 :       continue;
     134              :     }
     135           14 :     folders << passFoldersUnder(child.absoluteFilePath());
     136              :   }
     137           19 :   return folders;
     138           19 : }
     139              : } // namespace
     140              : 
     141           16 : auto RealPass::refuseUnverifiedTree(const QString &dir, bool onlyListsWithin,
     142              :                                     const QString &except) -> bool {
     143           16 :   const GpgIdSigner signer = listSigner();
     144           16 :   if (!signer.enabled()) {
     145              :     return false;
     146              :   }
     147              :   // One gpg call per list, before pass runs: a store with many lists makes
     148              :   // that a noticeable pause, so say what it is.
     149           24 :   emit statusMsg(tr("Checking the recipient lists under %1…")
     150           24 :                      .arg(QDir::toNativeSeparators(QDir::cleanPath(dir))),
     151              :                  3000);
     152              :   // pass mv and cp re-encrypt every entry of the folder they write to, each
     153              :   // to its nearest list, walking as passFoldersUnder() does. For the folder
     154              :   // that moves, the lists inside it go along: checked where they lie now
     155              :   // (after the move their folder line names the old place until the
     156              :   // recipients are saved there again), while a list above it is replaced
     157              :   // by the destination's, which the caller checks. For entries already in
     158              :   // the destination, their lists stay where they are: all of them count.
     159              :   // Absolute, as getGpgIdPath() hands lists back: a relative or other
     160              :   // spelling would match nothing and check nothing.
     161              :   const QString within =
     162           42 :       onlyListsWithin ? QDir::cleanPath(QFileInfo(dir).absoluteFilePath()) +
     163              :                             QLatin1Char('/')
     164           33 :                       : QString();
     165              :   QSet<QString> verified;
     166              :   // Every folder's list counts, also where no entry uses it yet: cp -r
     167              :   // merges an incoming folder into one of the same name, so the entries it
     168              :   // brings get a list already there, and a list it brings replaces the one
     169              :   // there for the entries already in it.
     170           35 :   for (const QString &folder : passFoldersUnder(dir)) {
     171           19 :     if (refuseList(folder, signer, verified, within, except)) {
     172              :       return true;
     173              :     }
     174              :   }
     175            4 :   return false;
     176           16 : }
     177              : 
     178            7 : auto RealPass::storeGit(const QStringList &args, QString *out) -> bool {
     179            7 :   if (!m_settings.useGit) {
     180              :     return false;
     181              :   }
     182              :   // `pass git` runs git in the store with PASSWORD_STORE_DIR set, as the
     183              :   // store's other quick lookups do; its exit code is git's own.
     184            7 :   QString err;
     185            7 :   return Executor::executeBlocking(
     186           14 :              exec.environment(), m_settings.passExecutable,
     187           28 :              QStringList{QStringLiteral("git")} + args, out, &err) == 0;
     188              : }
     189              : 
     190           19 : void RealPass::Insert(QString file, QString newValue, bool overwrite) {
     191           38 :   if (refuseLinkedPath(file + ".gpg")) {
     192            6 :     return;
     193              :   }
     194              :   const QString entry =
     195              :       QDir::cleanPath(QDir::isAbsolutePath(file)
     196           34 :                           ? file + ".gpg"
     197           68 :                           : QDir(m_settings.passStore).filePath(file) + ".gpg");
     198           17 :   if (refuseUnverifiedList(entry)) {
     199              :     return;
     200              :   }
     201           15 :   if (refuseUnfinishedGitOperation() || refusePendingPull()) {
     202            2 :     return;
     203              :   }
     204              :   m_insertQueue.enqueue(entry);
     205              :   // Past every refusal: this insert is now queued and will report once.
     206              :   queueInsert();
     207           65 :   QStringList args = {"insert", "-m"};
     208           13 :   if (overwrite) {
     209           12 :     args.append("-f");
     210              :   }
     211              :   args.append(file);
     212           26 :   executePass(PASS_INSERT, args, newValue);
     213              : }
     214              : 
     215           32 : void RealPass::onDequeuing(int id, bool completes) {
     216           32 :   if (id != PASS_INSERT || m_insertQueue.isEmpty()) {
     217           19 :     return;
     218              :   }
     219              :   const QString target = m_insertQueue.dequeue();
     220           13 :   if (completes) {
     221           13 :     m_insertPending.enqueue({target, entryFingerprint(target)});
     222              :   }
     223           13 : }
     224              : 
     225            6 : auto RealPass::insertWritten(const QString &err) const -> bool {
     226            6 :   if (m_insertDone.target.isEmpty()) {
     227              :     return false;
     228              :   }
     229              :   // gpg broke off (a full disk, killed): pass says so, and what gpg left
     230              :   // at the entry's name, if anything, is no entry. The line is pass's own
     231              :   // (`die "Password encryption aborted."` after gpg in cmd_insert and
     232              :   // cmd_edit of password-store.sh), which pass does not translate.
     233            6 :   if (err.contains(QLatin1String("Password encryption aborted."))) {
     234              :     return false;
     235              :   }
     236              :   // pass writes the new ciphertext to the entry's name before it commits,
     237              :   // and an encryption never gives the same bytes twice (a fresh session
     238              :   // key), so an edited entry has changed as surely as a new one appeared.
     239            5 :   const QByteArray after = entryFingerprint(m_insertDone.target);
     240            5 :   return !after.isEmpty() && after != m_insertDone.before;
     241              : }
     242              : 
     243           18 : auto RealPass::entryFingerprint(const QString &path) -> QByteArray {
     244           18 :   QFile file;
     245           18 :   if (!Util::openRegularFile(path, file)) {
     246            9 :     return {};
     247              :   }
     248            9 :   QCryptographicHash hash(QCryptographicHash::Sha256);
     249            9 :   if (!hash.addData(&file)) {
     250            0 :     return {};
     251              :   }
     252            9 :   return hash.result();
     253           18 : }
     254              : 
     255            9 : void RealPass::Remove(QString file, bool isDir) {
     256              :   // Nothing behind a link is the store's to delete.
     257           24 :   if (refuseLinkedPath(isDir ? file : file + ".gpg", LinkScope::ParentsOnly)) {
     258            6 :     return;
     259              :   }
     260            8 :   if (refuseUnfinishedGitOperation() || refusePendingPull()) {
     261            0 :     return;
     262              :   }
     263              :   // pass rm turns a folder into "<folder>/" and rm -rf then follows a link
     264              :   // and empties its target, so a linked folder is unlinked here and pass's
     265              :   // git is only asked to forget it. pass rm -f on a linked .gpg unlinks.
     266            8 :   const QString full = QDir::cleanPath(m_settings.passStore + file);
     267            8 :   if (isDir && Util::isLinkedFolder(full)) {
     268            5 :     if (!Util::removeTree(full)) {
     269            1 :       const QString why = tr("Could not remove the link %1.").arg(full);
     270            1 :       emit critical(tr("Delete failed"), why);
     271            1 :       emit processErrorExit(1, why);
     272              :       return;
     273              :     }
     274            4 :     if (m_settings.useGit) {
     275              :       // `pass git` exits 0 whatever git did, so ask by output whether git
     276              :       // knew the link (these quick lookups block; the commit is queued).
     277            4 :       const QString rel = QDir(m_settings.passStore).relativeFilePath(full);
     278              :       // With the store's environment (PASSWORD_STORE_DIR), as executePass.
     279            4 :       QString known;
     280            4 :       QString err;
     281              :       // The link is gone from disk either way. A failed lookup or rm leaves
     282              :       // git tracking it: say so rather than report a clean removal.
     283           28 :       bool recorded = Executor::executeBlocking(
     284            8 :                           exec.environment(), m_settings.passExecutable,
     285            4 :                           {"git", "ls-files", "--", rel}, &known, &err) == 0;
     286           12 :       if (recorded && !known.trimmed().isEmpty()) {
     287              :         recorded =
     288           27 :             Executor::executeBlocking(
     289            6 :                 exec.environment(), m_settings.passExecutable,
     290              :                 {"git", "rm", "-q", "--cached", "--", rel}, &known, &err) == 0;
     291            3 :         if (recorded) {
     292              :           // `pass git commit` is git's commit as it is: pass adds -S only
     293              :           // to the commits it makes itself, so ask as its git_commit() does.
     294            2 :           QString signs;
     295              :           const bool sign =
     296           26 :               Executor::executeBlocking(
     297            4 :                   exec.environment(), m_settings.passExecutable,
     298              :                   {"git", "config", "--bool", "--get", "pass.signcommits"},
     299            2 :                   &signs, nullptr) == 0 &&
     300            4 :               signs.trimmed() == QLatin1String("true");
     301           14 :           QStringList commit{"git", "commit", "-q"};
     302            2 :           if (sign) {
     303            2 :             commit << "-S";
     304              :           }
     305              :           // Queued, as every other commit, so a signer or hook that waits
     306              :           // (a pinentry, an agent) does not freeze the window; finished()
     307              :           // judges it by what is left staged, since `pass git` exits 0
     308              :           // whatever git did.
     309            2 :           m_linkCommits.enqueue(
     310              :               {rel, m_settings.passExecutable, exec.environment()});
     311            4 :           executePass(
     312              :               GIT_COMMIT,
     313            2 :               commit + QStringList{"-m", "Remove for " + rel + " using QtPass.",
     314           14 :                                    "--", rel});
     315              :           return;
     316              :         }
     317              :       }
     318            2 :       if (!recorded) {
     319              :         const QString why =
     320            1 :             tr("The link %1 was removed, but git could not record it: %2")
     321            2 :                 .arg(rel, err.trimmed());
     322            1 :         emit critical(tr("Delete incomplete"), why);
     323            1 :         emit processErrorExit(1, why);
     324              :       }
     325              :     }
     326              :     // Nothing ran for pass to finish: the removal is done here, and the
     327              :     // store changed.
     328            4 :     emit finishedRemove(QString(), QString());
     329            2 :     return;
     330              :   }
     331           15 :   executePass(PASS_REMOVE, {"rm", (isDir ? "-rf" : "-f"), file});
     332           18 : }
     333              : 
     334           39 : void RealPass::finished(int id, int exitCode, const QString &out,
     335              :                         const QString &err) {
     336           39 :   if (static_cast<PROCESS>(id) == GIT_COMMIT && !m_linkCommits.isEmpty()) {
     337              :     // Whatever the exit code (pass itself failing, or 0 whatever git
     338              :     // did), what is left staged decides.
     339            2 :     finishLinkCommit(m_linkCommits.dequeue(), out, err);
     340            2 :     return;
     341              :   }
     342           37 :   if (static_cast<PROCESS>(id) == PASS_INSERT) {
     343              :     // This completion's own insert, not whichever left the queue last.
     344              :     m_insertDone =
     345           41 :         m_insertPending.isEmpty() ? InsertRecord{} : m_insertPending.dequeue();
     346              :   }
     347           37 :   Pass::finished(id, exitCode, out, err);
     348              : }
     349              : 
     350            2 : void RealPass::finishLinkCommit(const LinkCommit &commit, const QString &out,
     351              :                                 const QString &err) {
     352              :   // In the store the commit ran in, which a profile switch meanwhile does
     353              :   // not change.
     354            2 :   const QString &rel = commit.rel;
     355            2 :   QString staged;
     356              :   const bool committed =
     357           16 :       Executor::executeBlocking(
     358            2 :           commit.environment, commit.passExecutable,
     359              :           {"git", "diff", "--cached", "--name-only", "--", rel}, &staged,
     360            4 :           nullptr) == 0 &&
     361            2 :       staged.trimmed().isEmpty();
     362            2 :   if (committed) {
     363              :     // As Pass::finished() would: git's output (a hook's warning, say) for
     364              :     // the output panel, then the removal.
     365            1 :     emit finishedAnyWithPid(out, err, GIT_COMMIT);
     366            1 :     emit finishedRemove(out, err);
     367              :     return;
     368              :   }
     369            0 :   const QString why = tr("The link %1 was removed and git no longer tracks "
     370              :                          "it, but the commit failed: %2")
     371            2 :                           .arg(rel, err.trimmed());
     372            1 :   emit critical(tr("Delete incomplete"), why);
     373            1 :   emit processErrorExit(1, why);
     374            2 :   emit finishedRemove(QString(), QString());
     375            2 : }
     376              : 
     377           11 : void RealPass::Init(QString path, const QList<UserInfo> &users) {
     378              :   // A relative path names a folder in the store, as --path does for pass:
     379              :   // resolved once, so the checks below look where pass writes, not in the
     380              :   // working directory.
     381              :   const QString folder = QDir::cleanPath(
     382           13 :       QDir::isAbsolutePath(path) ? path
     383           24 :                                  : QDir(m_settings.passStore).filePath(path));
     384              :   // pass init writes .gpg-id with a shell redirection and gpg writes the
     385              :   // signature with --output; both follow a link planted under those names.
     386              :   // --path is store-relative, or pass creates passStore/passStore/dir: the
     387              :   // store check hands it over as it judged it, so a store root reached
     388              :   // through a link and a folder spelled through its target agree (a lexical
     389              :   // relativeFilePath() made that ../../target/dir, which pass refuses).
     390           11 :   QString dirWithoutPassdir;
     391              :   const auto refusePlaced = [this](const QString &placed) {
     392           30 :     return refuseLinkedPath(placed, LinkScope::EntryAndParents,
     393              :                             PathSpelling::PlacedByCaller);
     394              :   };
     395           10 :   if (refusePlaced(folder) ||
     396           40 :       refusePlaced(folder + QStringLiteral("/.gpg-id")) ||
     397           48 :       refusePlaced(folder + QStringLiteral("/.gpg-id.sig")) ||
     398            9 :       refuseFolderOutsideStore(folder, &dirWithoutPassdir)) {
     399              :     return;
     400              :   }
     401              :   // pass init writes this folder's list and then re-encrypts everything
     402              :   // under it to each entry's nearest list, a deeper folder's own included:
     403              :   // those are checked as for a move. This folder's list is the one being
     404              :   // written, so it is neither verified nor missed. Walked in the store's
     405              :   // own spelling: a folder spelled through the target of a linked store
     406              :   // root is not under the store as named, and the lookup of each nearest
     407              :   // list would stop at once and miss the deeper ones.
     408              :   const QString inStore =
     409           14 :       QDir::cleanPath(QDir(m_settings.passStore).filePath(dirWithoutPassdir));
     410            7 :   if (refuseUnverifiedTree(inStore, true,
     411           14 :                            QDir(inStore).filePath(QStringLiteral(".gpg-id")))) {
     412              :     return;
     413              :   }
     414            4 :   if (refuseUnfinishedGitOperation() || refusePendingPull()) {
     415            0 :     return;
     416              :   }
     417           12 :   QStringList args = {"init", "--path=" + dirWithoutPassdir};
     418           10 :   for (const UserInfo &user : users) {
     419            6 :     if (user.enabled) {
     420            5 :       args.append(user.key_id);
     421              :     }
     422              :   }
     423            8 :   executePass(PASS_INIT, args);
     424            4 : }
     425              : 
     426           10 : void RealPass::Move(const QString src, const QString dest, const bool force) {
     427           20 :   passMoveOrCopy(PASS_MOVE, QStringLiteral("mv"), src, dest, force);
     428           10 : }
     429              : 
     430            6 : void RealPass::Copy(const QString src, const QString dest, const bool force) {
     431           12 :   passMoveOrCopy(PASS_COPY, QStringLiteral("cp"), src, dest, force);
     432            6 : }
     433              : 
     434            8 : auto RealPass::passName(const QString &path, bool stripGpg) const -> QString {
     435              :   QString name =
     436            8 :       QDir(QDir::cleanPath(m_settings.passStore))
     437           16 :           .relativeFilePath(QDir::cleanPath(QDir(path).absolutePath()));
     438            8 :   if (stripGpg) {
     439            8 :     name.replace(Util::endsWithGpg(), "");
     440              :   }
     441            8 :   return name;
     442              : }
     443              : 
     444           16 : void RealPass::passMoveOrCopy(PROCESS id, const QString &subcommand,
     445              :                               const QString &src, const QString &dest,
     446              :                               const bool force) {
     447           16 :   if (refuseLinkedPath(src) || refuseLinkedPath(dest)) {
     448           12 :     return;
     449              :   }
     450           13 :   QFileInfo srcFileInfo = QFileInfo(src);
     451           13 :   QFileInfo destFileInfo = QFileInfo(dest);
     452              :   // A drop hands over the folder; pass then writes <folder>/<name>, and cp
     453              :   // writes through a link planted under that name.
     454           13 :   if (destFileInfo.isDir() &&
     455           29 :       refuseLinkedPath(QDir(dest).filePath(srcFileInfo.fileName()))) {
     456              :     return;
     457              :   }
     458              : 
     459              :   // pass always forces when called from QtPass, so refuse file-onto-file.
     460           12 :   if (!force && srcFileInfo.isFile() && destFileInfo.isFile()) {
     461              :     return;
     462              :   }
     463              : 
     464              :   // pass re-encrypts what it moves or copies to the destination's list, a
     465              :   // folder's entries also to the lists that go along with it, and, when it
     466              :   // writes into an existing folder, everything already in that folder too
     467              :   // (reencrypt_path on the whole destination).
     468           22 :   if (refuseUnverifiedList(destFileInfo.isDir()
     469           29 :                                ? QDir(dest).filePath(srcFileInfo.fileName())
     470            9 :                                : dest) ||
     471           34 :       (srcFileInfo.isDir() && refuseUnverifiedTree(src, true)) ||
     472           20 :       (destFileInfo.isDir() && refuseUnverifiedTree(dest, false))) {
     473              :     return;
     474              :   }
     475            4 :   if (refuseUnfinishedGitOperation() || refusePendingPull()) {
     476            0 :     return;
     477              :   }
     478              : 
     479              :   // pass appends .gpg itself, so an entry named with it would come out as
     480              :   // "name.gpg.gpg"; the destination usually does not exist yet.
     481              :   const QString passSrc =
     482            4 :       passName(src, srcFileInfo.isFile() &&
     483            6 :                         srcFileInfo.suffix() == QLatin1String("gpg"));
     484              :   const QString passDest =
     485            4 :       passName(dest, !destFileInfo.isDir() &&
     486            7 :                          destFileInfo.suffix() == QLatin1String("gpg"));
     487              : 
     488            4 :   QStringList args;
     489              :   args << subcommand;
     490            4 :   if (force) {
     491            4 :     args << "-f";
     492              :   }
     493              :   args << passSrc;
     494              :   args << passDest;
     495            8 :   executePass(id, args);
     496           13 : }
     497              : 
     498              : // Not `pass grep` (find -L follows links out of the store): the native
     499              : // search walks real files only (Util::regularFilesUnder), so it needs gpg.
     500            2 : void RealPass::Grep(QString pattern, bool caseInsensitive) {
     501            2 :   if (m_settings.gpgExecutable.isEmpty()) {
     502            1 :     emit statusMsg(tr("Search needs the GPG executable to be configured."),
     503              :                    5000);
     504            1 :     emit finishedGrep({});
     505            1 :     return;
     506              :   }
     507            2 :   m_grep.search(pattern, caseInsensitive, m_settings.gpgExecutable,
     508            1 :                 m_settings.passStore, exec.environment());
     509              : }
     510              : 
     511           32 : void RealPass::executePass(PROCESS id, const QStringList &args, QString input,
     512              :                            bool readStdout, bool readStderr) {
     513           32 :   executeWrapper(id, m_settings.passExecutable, args, std::move(input),
     514              :                  readStdout, readStderr);
     515           32 : }
        

Generated by: LCOV version 2.0-1