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_
|