QtPass Developer documentation
QtPass test coverage
Current view: top level - src/pass.h (source / functions) Coverage Total Hit
Test: QtPass Lines: 69.2 % 13 9
Test Date: 2026-11-14 13:19:18 Functions: 50.0 % 4 2

            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 Pass, the abstract password-store backend that RealPass and
       6              :  * ImitatePass implement, and gpgErrorMessage(), which turns gpg's stderr into
       7              :  * a message for the user.
       8              :  */
       9              : #ifndef SRC_PASS_H_
      10              : #define SRC_PASS_H_
      11              : 
      12              : #include "appsettings.h"
      13              : #include "enums.h"
      14              : #include "executor.h"
      15              : #include "gpgidgeneration.h"
      16              : #include "userinfo.h"
      17              : 
      18              : #include <QProcess>
      19              : #include <QProcessEnvironment>
      20              : #include <QQueue>
      21              : #include <QString>
      22              : #include <QStringList>
      23              : 
      24              : class GpgIdSigner;
      25              : #include <cassert>
      26              : #include <map>
      27              : #include <optional>
      28              : 
      29              : /**
      30              :  * @struct ResolvedGpgconfCommand
      31              :  * @ingroup backend
      32              :  * @brief Represents a command invocation for gpgconf operations.
      33              :  *
      34              :  * On Unix this is typically just the gpgconf binary; on Windows/WSL it may be
      35              :  * a wrapper such as wsl.exe with gpgconf as a prefixed argument.
      36              :  */
      37            2 : struct ResolvedGpgconfCommand {
      38              :   /**
      39              :    * @brief Executable to run — may be a bare name resolved via PATH or a
      40              :    * wrapper (e.g. wsl.exe) rather than a direct gpgconf path.
      41              :    */
      42              :   QString program;
      43              :   /**
      44              :    * @brief Full argv list, including any wrapper/prefix args followed by the
      45              :    * actual gpgconf operation flags (e.g. ["--kill", "gpg-agent"]).
      46              :    */
      47              :   QStringList arguments;
      48              : };
      49              : 
      50              : /**
      51              :  * @class Pass
      52              :  * @ingroup backend
      53              :  * @brief Abstract base class for password store operations.
      54              :  *
      55              :  * Pass provides an abstraction layer for password management, supporting both
      56              :  * the native 'pass' utility and gopass/imitation backends. It handles:
      57              :  * - Password file operations (show, insert, remove, move, copy)
      58              :  * - GPG key management and Git integration
      59              :  * - OTP generation
      60              :  * - Random password generation
      61              :  *
      62              :  * Subclasses must implement Git and password operations.
      63              :  *
      64              :  * Paths: the operations take a path relative to the store, or an absolute
      65              :  * path spelled from the store as configured (`passStore`), as the store
      66              :  * tree and the dialogs build them. The link checks, `pass` names and Git
      67              :  * paths are worked out against that spelling. A path spelled through the
      68              :  * target of a store root that is itself a link is not under the store as
      69              :  * named. Init() places such a folder from its canonical path; the other
      70              :  * operations would check only the entry itself for a link and give `pass`
      71              :  * and Git `../` names, so callers must not hand one over. Resolving every
      72              :  * link instead is no way out: a removal would then reach a link's target,
      73              :  * not the link.
      74              :  */
      75              : class Pass : public QObject {
      76          232 :   Q_OBJECT
      77              : 
      78              :   /** @brief Unused; kept from the time Pass tracked the running wrapper. */
      79              :   bool wrapperRunning{false};
      80              :   /**
      81              :    * @brief The environment the store's processes run in: the system
      82              :    * environment plus what updateEnv() sets (PASSWORD_STORE_ variables,
      83              :    * GNUPGHOME, PATH additions, WSLENV). Read through storeEnvironment().
      84              :    */
      85              :   QProcessEnvironment env;
      86              : 
      87              : protected:
      88              :   /**
      89              :    * @brief The environment QtPass runs the store's processes in.
      90              :    *
      91              :    * What updateEnv() made: the git ceiling and the PASSWORD_STORE_ variables
      92              :    * QtPass's own environment does not carry. For blocking calls that start
      93              :    * their own process.
      94              :    * @return A copy of the environment.
      95              :    */
      96          318 :   [[nodiscard]] auto storeEnvironment() const -> QProcessEnvironment {
      97          318 :     return withCurrentGitPin(env);
      98              :   }
      99              : 
     100              :   /**
     101              :    * @brief @p base with GIT_DIR and GIT_WORK_TREE as the store is now
     102              :    * (Util::gitStoreVariables()), not as it was at updateEnv(): a `.git` that
     103              :    * appeared or changed hands since then is judged when Git runs.
     104              :    * @param base The environment to update.
     105              :    * @return @p base, the two variables recomputed (left as they are when
     106              :    *         the store keeps to a parent repository or there is none).
     107              :    */
     108              :   [[nodiscard]] auto withCurrentGitPin(QProcessEnvironment base) const
     109              :       -> QProcessEnvironment;
     110              : 
     111              :   /**
     112              :    * @brief Application settings snapshot injected at init() time.
     113              :    *
     114              :    * Populated by init(AppSettings) and valid for the lifetime of this backend
     115              :    * instance. Subclasses read it in place of calling QtPassSettings directly.
     116              :    */
     117              :   AppSettings m_settings;
     118              :   /**
     119              :    * @brief Internal command executor for queuing and running subprocesses.
     120              :    */
     121              :   Executor exec;
     122              : 
     123              :   /**
     124              :    * @brief Alias for Enums::PROCESS used throughout this class.
     125              :    */
     126              :   using PROCESS = Enums::PROCESS;
     127              : 
     128              : public:
     129              :   /**
     130              :    * @brief Construct a Pass instance.
     131              :    */
     132              :   Pass();
     133              :   /**
     134              :    * @brief Initialize the Pass instance with a settings snapshot.
     135              :    * @param settings Application settings to use for this backend lifetime.
     136              :    */
     137              :   void init(const AppSettings &settings);
     138              :   /**
     139              :    * @brief Check if GPG supports Ed25519 encryption.
     140              :    * @param gpgExecutable Path to the gpg binary (defaults to "gpg").
     141              :    * @return true if Ed25519 is supported.
     142              :    */
     143              :   static bool gpgSupportsEd25519(const QString &gpgExecutable = {});
     144              :   /**
     145              :    * @brief Get default key template for new GPG keys.
     146              :    * @param gpgExecutable Path to the gpg binary (defaults to "gpg").
     147              :    * @return Default template string.
     148              :    */
     149              :   static QString getDefaultKeyTemplate(const QString &gpgExecutable = {});
     150              : 
     151          222 :   ~Pass() override = default;
     152              : 
     153              :   // Git operations
     154              :   /**
     155              :    * @brief Initialize Git repository in password store.
     156              :    */
     157              :   virtual void GitInit() = 0;
     158              :   /**
     159              :    * @brief Pull changes from remote Git repository.
     160              :    */
     161              :   virtual void GitPull() = 0;
     162              :   /**
     163              :    * @brief Pull with rebase from remote.
     164              :    */
     165              :   virtual void GitPull_b() = 0;
     166              :   /**
     167              :    * @brief Push changes to remote Git repository.
     168              :    */
     169              :   virtual void GitPush() = 0;
     170              : 
     171              :   // Password operations
     172              :   /**
     173              :    * @brief Show decrypted password file.
     174              :    * @param file Path to password file relative to store root.
     175              :    */
     176              :   virtual void Show(QString file) = 0;
     177              :   /**
     178              :    * @brief Insert or update password.
     179              :    * @param file Path to password file.
     180              :    * @param value Password content to store.
     181              :    * @param force Overwrite existing file.
     182              :    */
     183              :   virtual void Insert(QString file, QString value, bool force) = 0;
     184              :   /**
     185              :    * @brief How many inserts have started queueing but not yet reported.
     186              :    *
     187              :    * One Insert() still in the command queue, or running, counts here until its
     188              :    * PASS_INSERT result goes out. A PasswordDialog reads this just before its
     189              :    * own Insert() so it can tell an earlier insert's report (which it is also
     190              :    * connected to, once a hanging queue let it open while its save was still
     191              :    * pending) from its own.
     192              :    * @return The number of inserts awaiting their report.
     193              :    */
     194           23 :   [[nodiscard]] auto insertsPending() const -> int { return m_insertsPending; }
     195              :   /**
     196              :    * @brief Remove password file or directory.
     197              :    * @param file Path to remove.
     198              :    * @param isDir true if removing a directory.
     199              :    */
     200              :   virtual void Remove(QString file, bool isDir) = 0;
     201              :   /**
     202              :    * @brief Move password file or directory.
     203              :    * @param srcDir Source path.
     204              :    * @param dest Destination path.
     205              :    * @param force Overwrite existing.
     206              :    */
     207              :   virtual void Move(const QString srcDir, const QString dest,
     208              :                     const bool force) = 0;
     209              :   /**
     210              :    * @brief Copy password file or directory.
     211              :    * @param srcDir Source path.
     212              :    * @param dest Destination path.
     213              :    * @param force Overwrite existing.
     214              :    */
     215              :   virtual void Copy(const QString srcDir, const QString dest,
     216              :                     const bool force) = 0;
     217              :   /**
     218              :    * @brief Initialize new password store.
     219              :    * @param path Root of password store.
     220              :    * @param users List of recipient GPG keys.
     221              :    */
     222              :   virtual void Init(QString path, const QList<UserInfo> &users) = 0;
     223              :   /**
     224              :    * @brief Search password content for a pattern.
     225              :    *
     226              :    * Both backends decrypt each entry with gpg and match it in-process
     227              :    * (NativeGrep), never through `pass grep`, so the pattern is one dialect
     228              :    * for both: QRegularExpression, Perl-compatible (PCRE).
     229              :    *
     230              :    * @param pattern Search pattern, a Perl-compatible regular expression.
     231              :    * @param caseInsensitive true for case-insensitive search.
     232              :    */
     233              :   virtual void Grep(QString pattern, bool caseInsensitive) = 0;
     234              : 
     235              :   /**
     236              :    * @brief Generate random password.
     237              :    * @param length Password length.
     238              :    * @param charset Character set to use.
     239              :    * @return Generated password.
     240              :    */
     241              :   virtual auto generatePassword(unsigned int length, const QString &charset)
     242              :       -> QString;
     243              : 
     244              :   // GPG operations
     245              :   /**
     246              :    * @brief Generate GPG keys using batch script.
     247              :    * @param batch GPG batch script content.
     248              :    */
     249              :   void GenerateGPGKeys(QString batch);
     250              :   /**
     251              :    * @brief List GPG keys matching patterns.
     252              :    * @param keystrings List of search patterns.
     253              :    * @param secret Include secret keys.
     254              :    * @return List of matching keys.
     255              :    */
     256              :   auto listKeys(QStringList keystrings, bool secret = false) -> QList<UserInfo>;
     257              :   /**
     258              :    * @brief List GPG keys.
     259              :    * @param keystring Search pattern.
     260              :    * @param secret Include secret keys.
     261              :    * @return List of matching keys.
     262              :    */
     263           86 :   auto listKeys(const QString &keystring = "", bool secret = false)
     264              :       -> QList<UserInfo>;
     265              :   /**
     266              :    * @brief Update environment for subprocesses.
     267              :    */
     268              :   void updateEnv();
     269              : 
     270              :   /**
     271              :    * @brief Most bytes of plaintext a gpg decryption may produce.
     272              :    *
     273              :    * An OpenPGP message is compressed, so a small entry in a shared store can
     274              :    * decompress to gigabytes; gpg stops at this many (--max-output) instead of
     275              :    * filling QtPass's memory. Far above any real entry.
     276              :    */
     277              :   static constexpr qint64 kMaxPlaintextBytes = 64LL * 1024 * 1024;
     278              : 
     279              :   /**
     280              :    * @brief The gpg options that enforce kMaxPlaintextBytes.
     281              :    * @return "--max-output" and the limit, for a gpg decryption's arguments.
     282              :    */
     283              :   static auto maxOutputArgs() -> QStringList;
     284              : 
     285              :   // Static helpers
     286              :   /**
     287              :    * @brief Resolve the gpgconf command to kill agents.
     288              :    * @param gpgPath Path to gpg executable.
     289              :    * @return Resolved command with program and arguments.
     290              :    */
     291              :   static auto resolveGpgconfCommand(const QString &gpgPath)
     292              :       -> ResolvedGpgconfCommand;
     293              :   /**
     294              :    * @brief Get .gpg-id file path for a password file.
     295              :    * @param for_file Path to password file.
     296              :    * @param passStore Root directory of the password store.
     297              :    * @return Path to .gpg-id file.
     298              :    */
     299              :   static auto getGpgIdPath(const QString &for_file, const QString &passStore)
     300              :       -> QString;
     301              :   /**
     302              :    * @brief The folder list getGpgIdPath() passed over on its way up to the
     303              :    * one it returns, if it passed one over that a signed store must not.
     304              :    *
     305              :    * getGpgIdPath() skips a `.gpg-id` that is missing, a link or a junction
     306              :    * and falls back to a parent folder's list, as `pass` does. In a signed
     307              :    * store that lets anyone who can write to it, without the signing key,
     308              :    * move a folder's entries to the parent's (also validly signed)
     309              :    * recipients by removing or linking the folder's own list. This names the
     310              :    * first such list between the start and the folder of the list found
     311              :    * (that folder and the store root excluded): one that is under the name
     312              :    * but not a regular file, or one that is gone although this device has
     313              :    * accepted a list at that path before (GpgIdGeneration::remembered()).
     314              :    * When the generation record cannot be read, whether a list was accepted
     315              :    * at a path cannot be told: the first folder without its own list is
     316              :    * named then too (fail closed), and @p recordError says why.
     317              :    * @param forFile Entry or folder, as for getGpgIdPath().
     318              :    * @param passStore Root directory of the password store.
     319              :    * @param recordError Receives the record's error when it could not be
     320              :    *        read, if not null; left untouched otherwise.
     321              :    * @return The skipped `.gpg-id` path, or nothing when the list found is
     322              :    *         the one that applies.
     323              :    */
     324              :   static auto skippedGpgId(const QString &forFile, const QString &passStore,
     325              :                            QString *recordError = nullptr)
     326              :       -> std::optional<QString>;
     327              :   /**
     328              :    * @brief Get list of recipients for a password file.
     329              :    * @param for_file Path to password file.
     330              :    * @param passStore Root directory of the password store.
     331              :    * @return List of recipient key IDs.
     332              :    */
     333              :   static auto getRecipientList(const QString &for_file,
     334              :                                const QString &passStore) -> QStringList;
     335              : 
     336              :   /**
     337              :    * @brief What a dialog may do with the list of a folder
     338              :    * (recipientsForEditing()).
     339              :    */
     340           82 :   struct RecipientsForEditing {
     341              :     /** @brief How the list on disk stood up. */
     342              :     enum class State {
     343              :       /** @brief No signing key: the list as it is. */
     344              :       Unsigned,
     345              :       /** @brief Signature and generation and folder all accepted. */
     346              :       Verified,
     347              :       /**
     348              :        * @brief Signed, authentic and bound to this folder, but older than a
     349              :        * generation this device has accepted: preselected for the recovery,
     350              :        * flagged for review.
     351              :        */
     352              :       VerifiedRollback,
     353              :       /**
     354              :        * @brief Not to be used: signature invalid, written for another folder
     355              :        * or for no folder in particular while a generation was accepted here,
     356              :        * malformed header, or no freshness record to judge by.
     357              :        */
     358              :       Rejected,
     359              :     };
     360              :     /** @brief How the list on disk stood up. */
     361              :     State state = State::Unsigned;
     362              :     /** @brief The recipients to preselect; empty when Rejected. */
     363              :     QStringList recipients;
     364              :     /** @brief What the dialog should show, if anything. */
     365              :     QString warning;
     366              :   };
     367              : 
     368              :   /**
     369              :    * @brief The recipients a dialog may preselect for editing the list of
     370              :    * @p dir.
     371              :    *
     372              :    * With a signing key configured, only a list whose signature, generation
     373              :    * and folder binding are all accepted: the Users dialog preselects what it
     374              :    * loads and OK writes and signs the selection, so a list read
     375              :    * unauthenticated would turn an attacker's unsigned edit into a genuinely
     376              :    * signed one with one click. The one exception is an authentic list the
     377              :    * generation record calls older (the recovery from a rollback goes through
     378              :    * this dialog): preselected, with the reason shown for review. Anything
     379              :    * else that fails preselects nothing and says why. Without a signing key,
     380              :    * the list as it is.
     381              :    * @param dir Folder whose list is edited; empty for the store root.
     382              :    * @param passStore The store the folder belongs to: the dialog's, which
     383              :    *        for a profile being set up is not this backend's.
     384              :    * @return The state, the recipients to preselect and the warning to show.
     385              :    */
     386              :   auto recipientsForEditing(const QString &dir, const QString &passStore)
     387              :       -> RecipientsForEditing;
     388              :   /**
     389              :    * @brief Parse the contents of a .gpg-id: one recipient per line, `#`
     390              :    * starts a comment, unusable lines are logged and skipped.
     391              :    * @param contents The file's bytes.
     392              :    * @param sourceName Where they came from, for the log.
     393              :    * @return The recipient key IDs in file order.
     394              :    */
     395              :   static auto parseRecipients(const QByteArray &contents,
     396              :                               const QString &sourceName) -> QStringList;
     397              :   /**
     398              :    * @brief The arguments as they may appear in the debug log: the value of
     399              :    * `--passphrase` and friends, and otpauth URIs, are replaced.
     400              :    * @param args The argv about to be run.
     401              :    * @return The same list with secret-shaped values redacted.
     402              :    */
     403              :   static auto loggableArgs(const QStringList &args) -> QStringList;
     404              :   /**
     405              :    * @brief Seed a new folder's .gpg-id from the recipients it inherits.
     406              :    *
     407              :    * Writes the recipient list that is in effect for @p newDir (found by
     408              :    * walking up to the nearest parent .gpg-id, see getGpgIdPath) into
     409              :    * @p newDir/.gpg-id atomically with owner-only permissions. Refuses to
     410              :    * overwrite an existing .gpg-id and writes nothing when no recipient is
     411              :    * inherited, so a zero-byte file never shadows the parent's list.
     412              :    * @param newDir Absolute path of the freshly created folder.
     413              :    * @param passStore Root directory of the password store.
     414              :    * @return true when the file was written, false otherwise.
     415              :    */
     416              :   static auto seedGpgIdFile(const QString &newDir, const QString &passStore)
     417              :       -> bool;
     418              : 
     419              :   /**
     420              :    * @brief Whether no gpg, git or pass of this backend runs or waits.
     421              :    * @return true when its executor is idle.
     422              :    */
     423              :   [[nodiscard]] auto isIdle() const -> bool { return exec.isIdle(); }
     424              : 
     425              : protected:
     426              :   /**
     427              :    * @brief With signing on, refuse to encrypt @p file when the lookup passed
     428              :    * over its folder's own list (skippedGpgId()): the parent's list is not
     429              :    * what was signed for it.
     430              :    * @param file The entry or folder about to be encrypted to.
     431              :    * @param signing Whether a signing key is set (signed lists in use).
     432              :    * @param except A list about to be written (by `pass init`), which may
     433              :    *        be missing now without that being refused.
     434              :    * @return Whether it was refused (and the user told).
     435              :    */
     436              :   auto refuseSkippedGpgId(const QString &file, bool signing,
     437              :                           const QString &except = QString()) -> bool;
     438              : 
     439              :   /**
     440              :    * @brief Whether @p gpgIdFile is a signed list QtPass accepts: its
     441              :    * signature verifies (GpgIdSigner::verifyFile()), and it is current and
     442              :    * bound to the folder it lies in (GpgIdGeneration::accept()), so an older
     443              :    * authentic list put back, or one signed for another folder, is refused.
     444              :    * @param signer The verifier, with the configured signing keys.
     445              :    * @param gpgIdFile The `.gpg-id`.
     446              :    * @param contents Receives the verified bytes.
     447              :    * @param why Receives why a current-and-bound check refused the list;
     448              :    *        left as it is when the signature itself does not verify.
     449              :    * @return Whether the list may be encrypted to.
     450              :    */
     451              :   auto verifySignedList(const GpgIdSigner &signer, const QString &gpgIdFile,
     452              :                         QByteArray *contents, QString *why) const -> bool;
     453              : 
     454              :   /**
     455              :    * @brief Whether the insert that just failed had put its entry in the
     456              :    * store before a later step failed. The backend that writes the entry
     457              :    * knows; this default says no.
     458              :    * @param err What the failed insert reported on stderr.
     459              :    * @return true when the failed insert's entry was written.
     460              :    */
     461            0 :   [[nodiscard]] virtual auto insertWritten(const QString &err) const -> bool {
     462              :     Q_UNUSED(err);
     463            0 :     return false;
     464              :   }
     465              : 
     466              :   /**
     467              :    * @brief Remember what a Show() asked for, so its completion can say so.
     468              :    *
     469              :    * Backends call this first thing in Show(); Pass::finished() pops the
     470              :    * entry again on success or failure. The Executor runs one queue in
     471              :    * order, so the two stay aligned.
     472              :    * @param file The Show() argument.
     473              :    */
     474              :   void queueShow(const QString &file) { m_pendingShows.enqueue(file); }
     475              :   /**
     476              :    * @brief Count one insert that has started queueing but not yet reported.
     477              :    *
     478              :    * Backends call this where Insert() queues its work, past every refusal;
     479              :    * Pass::finished() counts it off again when the insert reports. Each queued
     480              :    * insert reports once, as a PASS_INSERT, so the count tracks the inserts in
     481              :    * flight. A dialog reads insertsPending() before its own Insert() to learn
     482              :    * how many earlier reports are not its own.
     483              :    */
     484           77 :   void queueInsert() { ++m_insertsPending; }
     485              :   /** @brief What refuseLinkedPath() judges. */
     486              :   enum class LinkScope {
     487              :     /** @brief The entry itself and every folder above it in the store. */
     488              :     EntryAndParents,
     489              :     /**
     490              :      * @brief Only the folders above it: a removal unlinks a link and
     491              :      * touches nothing behind it.
     492              :      */
     493              :     ParentsOnly,
     494              :   };
     495              :   /** @brief How a path handed to refuseLinkedPath() is spelled. */
     496              :   enum class PathSpelling {
     497              :     /**
     498              :      * @brief As the store is configured or relative to it, the class's path
     499              :      * rule. A path spelled through the target of a linked store root is
     500              :      * then logged as a warning, so tests catch a caller that hands one over.
     501              :      */
     502              :     Configured,
     503              :     /**
     504              :      * @brief Through the target of a linked store root, by a caller that
     505              :      * places such a folder itself (Init()): not logged.
     506              :      */
     507              :     PlacedByCaller,
     508              :   };
     509              :   /**
     510              :    * @brief Refuse an operation on something that is, or lies behind, a
     511              :    * symbolic link or NTFS junction inside the store.
     512              :    *
     513              :    * The store tree shows a linked entry or folder like any other, and gpg,
     514              :    * pass and QFile all follow links, so without this a planted link would
     515              :    * have an entry shown, edited, copied or written outside the store, with
     516              :    * the store's recipients. The configured store root itself may be a link
     517              :    * (a synced folder); that is not judged.
     518              :    * @param path Store-relative (as Show/Insert/Remove get it, `.gpg`
     519              :    *        included where it applies) or absolute (as Move/Copy get it).
     520              :    * @param scope Whether the entry itself counts, or only the folders it
     521              :    *        lies in (a removal).
     522              :    * @param spelling Whether @p path follows the class's path rule, or the
     523              :    *        caller places a target-spelled path itself (Init()).
     524              :    * @return true when critical() and processErrorExit() were emitted and
     525              :    *         the caller must stop.
     526              :    */
     527              :   auto refuseLinkedPath(const QString &path,
     528              :                         LinkScope scope = LinkScope::EntryAndParents,
     529              :                         PathSpelling spelling = PathSpelling::Configured)
     530              :       -> bool;
     531              :   /**
     532              :    * @brief Refuse recipients for a folder outside the store, as pass
     533              :    * refuses a path with `..` in it.
     534              :    *
     535              :    * ImitatePass would write a `.gpg-id` that the store never reads, and
     536              :    * `pass init --path` would take the folder's absolute path as a folder
     537              :    * under the store. Links on the way are resolved, as for the store root.
     538              :    * @param folder The folder, absolute.
     539              :    * @param relative Where the folder lies in the store, as
     540              :    *        PathValidator::storeRelativePath() gives it (empty for the root),
     541              :    *        when it is not refused; may be null.
     542              :    * @return true when critical() and processErrorExit() were emitted and
     543              :    *         the caller must stop.
     544              :    */
     545              :   auto refuseFolderOutsideStore(const QString &folder,
     546              :                                 QString *relative = nullptr) -> bool;
     547              : 
     548              :   /**
     549              :    * @brief Run a git command in the store's repository, blocking.
     550              :    *
     551              :    * The two backends reach git differently: ImitatePass runs the configured
     552              :    * git with `-C <store>`, RealPass runs `pass git`. The default (no git)
     553              :    * answers false. Used by refuseUnfinishedGitOperation() to ask the
     554              :    * repository its state before an operation changes the store.
     555              :    * @param args The git arguments (without the executable or `-C`).
     556              :    * @param out Receives the command's standard output; may be null.
     557              :    * @return true when git ran and exited 0, false otherwise (git off, the
     558              :    *         store is not a repository, or git failed).
     559              :    */
     560            0 :   virtual auto storeGit(const QStringList &args, QString *out) -> bool {
     561              :     Q_UNUSED(args);
     562              :     Q_UNUSED(out);
     563            0 :     return false;
     564              :   }
     565              : 
     566              :   /**
     567              :    * @brief Refuse to change the store while git is mid-rebase, -merge,
     568              :    * -cherry-pick or -revert.
     569              :    *
     570              :    * A pull that stopped in a conflict leaves the repository part-way through
     571              :    * an operation. Committing a save or a re-encryption into it lands the
     572              :    * change on a detached HEAD (a later `git rebase --abort` drops it) or
     573              :    * concludes the merge in favour of the local copy (pushing over a
     574              :    * colleague's change). Every store-changing operation calls this at its
     575              :    * entry, next to refuseLinkedPath(), so the change is refused before gpg or
     576              :    * pass runs and the dialog keeps what was typed. Nothing is refused when git
     577              :    * is off, the store is not a repository, or git gives no answer. A HEAD
     578              :    * detached by hand (no operation in progress) is deliberately accepted.
     579              :    * @return true when critical() and processErrorExit() were emitted and the
     580              :    *         caller must stop.
     581              :    */
     582              :   auto refuseUnfinishedGitOperation() -> bool;
     583              : 
     584              :   /**
     585              :    * @brief The unfinished git operation in progress in the store, if any.
     586              :    *
     587              :    * The detection behind refuseUnfinishedGitOperation(), without emitting a
     588              :    * refusal: a backend re-checks it when a save's gpg step has finished (a
     589              :    * pull queued before the save, from the asynchronous Update, may have
     590              :    * finished and left a conflict meanwhile) so the entry is not placed or
     591              :    * committed into the operation.
     592              :    * @return A ready-to-show reason naming the operation (rebase, merge,
     593              :    *         cherry-pick or revert), or an empty string when none is in
     594              :    *         progress, git is off, or the store is not a repository.
     595              :    */
     596              :   auto unfinishedGitOperationReason() -> QString;
     597              : 
     598              :   /**
     599              :    * @brief Refuse a store change while a Git pull is still queued or running.
     600              :    *
     601              :    * An operation checks the repository when it is requested, but a pull queued
     602              :    * before it (the asynchronous Update) has not run yet, so a conflict it will
     603              :    * cause is not visible. For operations that commit without a later point to
     604              :    * re-check (every delete, move and copy, and the `pass` backend, whose
     605              :    * single `pass insert` commits by itself), refuse while the pull is in
     606              :    * flight so the change cannot land in the conflict it leaves. Nothing is
     607              :    * refused when Git is off or no pull is pending.
     608              :    * @return true when critical() and processErrorExit() were emitted and the
     609              :    *         caller must stop.
     610              :    */
     611              :   auto refusePendingPull() -> bool;
     612              : 
     613              :   /**
     614              :    * @brief Execute external wrapper command.
     615              :    * @param id Process identifier.
     616              :    * @param app Executable path.
     617              :    * @param args Command arguments.
     618              :    * @param readStdout Capture stdout.
     619              :    * @param readStderr Capture stderr.
     620              :    */
     621              :   void executeWrapper(PROCESS id, const QString &app, const QStringList &args,
     622              :                       bool readStdout = true, bool readStderr = true);
     623              :   /**
     624              :    * @brief Generate random password from charset.
     625              :    * @param charset Character set.
     626              :    * @param length Password length.
     627              :    * @return Generated password.
     628              :    */
     629              :   auto generateRandomPassword(const QString &charset, unsigned int length)
     630              :       -> QString;
     631              :   /**
     632              :    * @brief Generate random number in range.
     633              :    * @param bound Upper bound (exclusive).
     634              :    * @return Random number.
     635              :    */
     636              :   auto boundedRandom(quint32 bound) -> quint32;
     637              :   /**
     638              :    * @brief Set or remove an environment variable.
     639              :    *
     640              :    * @param name Variable name (e.g. "PASSWORD_STORE_DIR"), no trailing '='.
     641              :    * @param value New value; an empty string removes the variable entirely.
     642              :    */
     643              :   void setEnvVar(const QString &name, const QString &value);
     644              : 
     645              :   /**
     646              :    * @brief Execute wrapper with input.
     647              :    * @param id Process identifier.
     648              :    * @param app Executable path.
     649              :    * @param args Command arguments.
     650              :    * @param input Input to pass to stdin.
     651              :    * @param readStdout Capture stdout.
     652              :    * @param readStderr Capture stderr.
     653              :    */
     654              :   void executeWrapper(PROCESS id, const QString &app, const QStringList &args,
     655              :                       QString input, bool readStdout, bool readStderr);
     656              :   /**
     657              :    * @brief Hook invoked just before a wrapped command is dispatched.
     658              :    *
     659              :    * Default is a no-op; backends override it to wrap execution (e.g.
     660              :    * ImitatePass registers a transaction). Keeps the dispatch surface a
     661              :    * single non-virtual executeWrapper instead of an overridden one.
     662              :    * @param id Process identifier of the command about to run.
     663              :    */
     664              :   virtual void beforeExecute(PROCESS id);
     665              : 
     666              : protected slots:
     667              :   /**
     668              :    * @brief Handle process completion.
     669              :    * @param id Process identifier.
     670              :    * @param exitCode Process exit code.
     671              :    * @param out Standard output.
     672              :    * @param err Standard error.
     673              :    */
     674              :   virtual void finished(int id, int exitCode, const QString &out,
     675              :                         const QString &err);
     676              : 
     677              : private:
     678              :   /// Files of the Show() calls still in flight, oldest first.
     679              :   QQueue<QString> m_pendingShows;
     680              :   /// Inserts that have started queueing but not yet reported; see queueInsert.
     681              :   int m_insertsPending = 0;
     682              :   /**
     683              :    * @brief Report a process that exited non-zero; called by finished().
     684              :    *
     685              :    * Always ends in processErrorExit(). A failed insert first emits
     686              :    * insertFailed(), and critical() too when the entry was written and only
     687              :    * a git step after it failed; failed key generation first emits
     688              :    * generateGPGKeysFailed().
     689              :    * @param pid Which kind of process failed.
     690              :    * @param exitCode Its exit code.
     691              :    * @param out Its stdout; not used.
     692              :    * @param err Its stderr, or the Executor's message for a failed start.
     693              :    */
     694              :   void handleProcessError(PROCESS pid, int exitCode, const QString &out,
     695              :                           const QString &err);
     696              :   /**
     697              :    * @brief Put the translated reason for a failed insert above what gpg
     698              :    * itself printed, leaving out the machine-readable `[GNUPG:]` lines.
     699              :    * @param friendly Message from gpgErrorMessage().
     700              :    * @param err gpg's stderr.
     701              :    * @return @p friendly, followed by the human-readable rest of @p err when
     702              :    *         there is any.
     703              :    */
     704              :   auto formatInsertError(const QString &friendly, const QString &err)
     705              :       -> QString;
     706              :   /**
     707              :    * @brief Emit the signals for a process that exited with 0; called by
     708              :    * finished() for everything but PASS_SHOW.
     709              :    *
     710              :    * finishedAnyWithPid() goes out only for a process whose output cannot
     711              :    * hold a secret (Enums::processInfo()), then the signal of that kind of
     712              :    * process (finishedGitPull() and so on), if it has one.
     713              :    * @param pid Which kind of process finished.
     714              :    * @param out Its stdout.
     715              :    * @param err Its stderr.
     716              :    */
     717              :   void emitProcessFinishedSignal(PROCESS pid, const QString &out,
     718              :                                  const QString &err);
     719              :   /**
     720              :    * @brief Refuse an operation on something that is not part of the store.
     721              :    *
     722              :    * Emits startingExecuteWrapper(), critical() and processErrorExit(), so
     723              :    * an interface that arms itself when an operation starts is released
     724              :    * again although nothing was started. refuseLinkedPath() and
     725              :    * refuseFolderOutsideStore() say why.
     726              :    * @param why Translated reason, shown to the user.
     727              :    */
     728              :   void refuseAsNotPartOfStore(const QString &why);
     729              : 
     730              : signals:
     731              :   /**
     732              :    * @brief Nothing of the store's runs or waits any more (isIdle()).
     733              :    *
     734              :    * Emitted when the command queue drains. The main window re-enables the
     735              :    * profile selector on it: switching stores is held back while commands
     736              :    * for the current one are still queued.
     737              :    */
     738              :   void idle();
     739              :   /**
     740              :    * @brief Emitted each time a queued process is about to start, and by
     741              :    * the refusals that go on to emit processErrorExit().
     742              :    *
     743              :    * The main window shows the busy state on it. An operation made of
     744              :    * several processes emits it once per process; the operation then ends
     745              :    * with a `finished...` signal or with processErrorExit().
     746              :    */
     747              :   void startingExecuteWrapper();
     748              :   /**
     749              :    * @brief Progress or a non-fatal problem worth a line in the status bar
     750              :    * ("Creating backup commit", "Git pull failed: ...").
     751              :    *
     752              :    * Nothing depends on it being shown; an error the user must see goes out
     753              :    * as critical() instead.
     754              :    * @param msg Translated status text.
     755              :    * @param timeout How long to show it, in milliseconds.
     756              :    */
     757              :   void statusMsg(const QString &msg, int timeout);
     758              :   /**
     759              :    * @brief Something went wrong that the user has to be told about in a
     760              :    * dialog, such as a refused path or a failed backup commit.
     761              :    *
     762              :    * Says only what happened; it does not by itself mean an operation is
     763              :    * over. Every failure reported through the command queue goes on to
     764              :    * processErrorExit(), and so do some refusals. Other checks made before
     765              :    * anything is queued, such as an entry that already exists or a
     766              :    * recipient list that cannot be used, emit this signal and nothing
     767              :    * else.
     768              :    * @param title Translated dialog title.
     769              :    * @param message Translated description.
     770              :    */
     771              :   void critical(const QString &title, const QString &message);
     772              : 
     773              :   /**
     774              :    * @brief An operation ended in failure: its process exited non-zero,
     775              :    * could not be started or crashed, or the operation was refused.
     776              :    *
     777              :    * The counterpart of the `finished...` signals for an operation that
     778              :    * queued a command: it ends in one or the other. The main window
     779              :    * releases the busy state and shows @p err in the password pane. One
     780              :    * case emits both: when RealPass has removed a linked folder but could
     781              :    * not commit that, this signal is followed by finishedRemove(), because
     782              :    * the store did change.
     783              :    *
     784              :    * Before anything is queued the paths differ, so a caller must not wait
     785              :    * for this signal after every call. A path behind a link and an
     786              :    * unfinished Git operation are refused with startingExecuteWrapper(),
     787              :    * critical() and this signal. Other early checks, such as an entry that
     788              :    * already exists, emit critical() alone and return.
     789              :    *
     790              :    * insertFailed() and generateGPGKeysFailed() are emitted before this
     791              :    * signal whenever their operation fails through a queued command, be it
     792              :    * a non-zero exit, a crash or a process that could not be started, so
     793              :    * their dialogs need not guess whose failure it is.
     794              :    * generateGPGKeysFailed() also comes first when no gpg executable is
     795              :    * configured and nothing is queued at all. A refusal that reaches this
     796              :    * signal without queuing anything emits neither of them.
     797              :    * @param exitCode Exit code of the process; -1 when it did not exit
     798              :    *        normally, 1 for a refusal.
     799              :    * @param err What to show the user: stderr, or a translated message.
     800              :    */
     801              :   void processErrorExit(int exitCode, const QString &err);
     802              : 
     803              :   /**
     804              :    * @brief A process of any kind finished successfully and its output
     805              :    * cannot contain a secret.
     806              :    *
     807              :    * Feeds the process output panel. Processes whose output may hold a
     808              :    * decrypted entry (Enums::processInfo()) never emit it, so a receiver
     809              :    * may log or display @p out and @p err as they are.
     810              :    * @param out Standard output from the process.
     811              :    * @param err Standard error from the process.
     812              :    * @param pid Which kind of process it was.
     813              :    */
     814              :   void finishedAnyWithPid(const QString &out, const QString &err,
     815              :                           Enums::PROCESS pid);
     816              :   /**
     817              :    * @brief `git init` in the store succeeded; the store is a repository
     818              :    * now and the tree is re-read.
     819              :    * @param out Standard output.
     820              :    * @param err Standard error.
     821              :    */
     822              :   void finishedGitInit(const QString &out, const QString &err);
     823              :   /**
     824              :    * @brief `git pull` succeeded; entries may have changed on disk.
     825              :    * @param out Standard output.
     826              :    * @param err Standard error.
     827              :    */
     828              :   void finishedGitPull(const QString &out, const QString &err);
     829              :   /**
     830              :    * @brief `git push` succeeded.
     831              :    * @param out Standard output.
     832              :    * @param err Standard error.
     833              :    */
     834              :   void finishedGitPush(const QString &out, const QString &err);
     835              :   /**
     836              :    * @brief Emitted when a Show() succeeds.
     837              :    *
     838              :    * Requests complete in the order they were queued, so @p file is the
     839              :    * argument of the Show() this result belongs to. Every receiver checks it
     840              :    * against what it asked for; a failed decrypt emits processErrorExit()
     841              :    * instead and never finishedShow().
     842              :    * @param out Decrypted password file content.
     843              :    * @param file The Show() argument this content is for.
     844              :    */
     845              :   void finishedShow(const QString &out, const QString &file);
     846              :   /**
     847              :    * @brief An Insert() succeeded: the entry is in the store and, with Git
     848              :    * in use, committed.
     849              :    *
     850              :    * In ImitatePass this ends the whole transaction (gpg, then git add and
     851              :    * commit), not just the gpg step. The password dialog closes on it. A
     852              :    * failure reported through the command queue goes out as insertFailed()
     853              :    * and processErrorExit() instead; an insert refused before anything is
     854              :    * queued emits neither this signal nor insertFailed().
     855              :    * @param out Standard output of the operation's processes.
     856              :    * @param err Standard error.
     857              :    */
     858              :   void finishedInsert(const QString &out, const QString &err);
     859              :   /**
     860              :    * @brief A Remove() succeeded; the entry or folder is gone from the store.
     861              :    * @param out Standard output.
     862              :    * @param err Standard error.
     863              :    */
     864              :   void finishedRemove(const QString &out, const QString &err);
     865              :   /**
     866              :    * @brief An Init() succeeded: the folder's `.gpg-id` holds the new
     867              :    * recipient list.
     868              :    * @param out Standard output.
     869              :    * @param err Standard error.
     870              :    */
     871              :   void finishedInit(const QString &out, const QString &err);
     872              :   /**
     873              :    * @brief A Move() succeeded; the entry or folder is under its new name.
     874              :    * @param out Standard output.
     875              :    * @param err Standard error.
     876              :    */
     877              :   void finishedMove(const QString &out, const QString &err);
     878              :   /**
     879              :    * @brief A Copy() succeeded; the copy exists beside the original.
     880              :    * @param out Standard output.
     881              :    * @param err Standard error.
     882              :    */
     883              :   void finishedCopy(const QString &out, const QString &err);
     884              :   /**
     885              :    * @brief GenerateGPGKeys() succeeded; the new key pair is in the keyring.
     886              :    *
     887              :    * The key generation dialog closes on it. A failure emits
     888              :    * generateGPGKeysFailed() instead.
     889              :    * @param out Standard output.
     890              :    * @param err Standard error.
     891              :    */
     892              :   void finishedGenerateGPGKeys(const QString &out, const QString &err);
     893              :   /**
     894              :    * @brief Emitted when GPG key generation fails, alongside the generic
     895              :    *        processErrorExit(); lets the keygen dialog react to its own
     896              :    *        failure only, not to any other command failing meanwhile.
     897              :    * @param err What gpg (or the setup) reported.
     898              :    */
     899              :   void generateGPGKeysFailed(const QString &err);
     900              :   /**
     901              :    * @brief Emitted when an Insert() fails through a queued command, before
     902              :    *        the generic processErrorExit(): the process exited non-zero,
     903              :    *        crashed or could not be started, or the entry could not be put in
     904              :    *        place afterwards. Lets the password dialog keep what was typed
     905              :    *        and say why, without reacting to any other command failing
     906              :    *        meanwhile. An insert refused before anything is queued does not
     907              :    *        emit it.
     908              :    * @param err What gpg, pass or the placement of the entry reported.
     909              :    * @param written true when this insert did put the entry in the store and
     910              :    *        only a later step (git add or commit) failed.
     911              :    */
     912              :   void insertFailed(const QString &err, bool written);
     913              :   /**
     914              :    * @brief The newest Grep() is over; an empty list means no entry matched.
     915              :    *
     916              :    * Comes from NativeGrep on both backends and bypasses finished(), since
     917              :    * the matching lines are decrypted content. A search superseded by a
     918              :    * newer one never reports.
     919              :    * @param results List of (entry path, matching lines) pairs.
     920              :    */
     921              :   void finishedGrep(const QList<QPair<QString, QStringList>> &results);
     922              : };
     923              : 
     924              : /**
     925              :  * @brief Maps GPG stderr (which may include --status-fd 2 tokens) to a
     926              :  * translated user-friendly encryption error string.
     927              :  *
     928              :  * Checks machine-readable [GNUPG:] status tokens first (locale-independent),
     929              :  * then falls back to case-insensitive substring matching for GPG builds that
     930              :  * do not emit status tokens. Returns an empty string when no known pattern
     931              :  * matches.
     932              :  *
     933              :  * Declared here so it can be exercised by unit tests without linking the full
     934              :  * Pass object.
     935              :  * @param err gpg's stderr, with or without `[GNUPG:]` status lines.
     936              :  * @return A translated message, or an empty string for an error it does
     937              :  *         not recognise.
     938              :  */
     939              : QString gpgErrorMessage(const QString &err);
     940              : 
     941              : #endif // SRC_PASS_H_
        

Generated by: LCOV version 2.0-1