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 ImitatePass, the backend that does what `pass` does by running gpg
6 : * and git itself.
7 : */
8 : #ifndef SRC_IMITATEPASS_H_
9 : #define SRC_IMITATEPASS_H_
10 :
11 : #include "gpgidsigner.h"
12 : #include "nativegrep.h"
13 : #include "pass.h"
14 : #include "simpletransaction.h"
15 : #include <QQueue>
16 : #include <QTemporaryDir>
17 :
18 : #include <atomic>
19 : #include <memory>
20 : #include <optional>
21 :
22 : class QRegularExpression;
23 : class QThread;
24 :
25 : /**
26 : * @class ImitatePass
27 : * @ingroup backend
28 : * @brief Implementation that imitates 'pass' when the real tool is unavailable.
29 : *
30 : * ImitatePass provides a complete password store implementation using direct
31 : * GPG operations when 'pass' is not installed or not enabled. It handles:
32 : * - Direct GPG encryption/decryption
33 : * - Git operations via direct subprocess calls
34 : * - GPG key management and recipient lists
35 : * - File-based password storage (.gpg files)
36 : * - Re-encryption when recipients change
37 : *
38 : * This is used as a fallback when RealPass cannot be initialized.
39 : */
40 : class ImitatePass : public Pass {
41 413 : Q_OBJECT
42 :
43 : friend class tst_util;
44 : friend class tst_imitatepass;
45 :
46 : protected:
47 : /**
48 : * @brief The signer for this store's `.gpg-id`, bound to the configured gpg
49 : * and signing keys and to execBlocking(), so a verification on the
50 : * re-encryption worker can be interrupted like every other gpg run there.
51 : * @return A signer; cheap to build, valid while m_settings is.
52 : */
53 : auto gpgIdSigner() -> GpgIdSigner;
54 : /**
55 : * @brief Read a `.gpg-id`, verify its detached signature and parse the
56 : * recipients from the bytes that were verified.
57 : *
58 : * One read serves both the check and the parse, so a file swapped in
59 : * between cannot smuggle in a recipient the signature never covered.
60 : * @param gpgIdFile The `.gpg-id`.
61 : * @param recipients Receives the recipients; empty on failure.
62 : * @param why Receives, if not null, what to tell the user on failure: a
63 : * bad signature, or a signed list older than one accepted before
64 : * (GpgIdGeneration).
65 : * @return false when the file cannot be read, the signature is bad or the
66 : * list is a rollback (with a signing key configured). true
67 : * otherwise, also for an empty list; the caller decides what an
68 : * empty list means.
69 : */
70 : auto loadVerifiedRecipients(const QString &gpgIdFile, QStringList *recipients,
71 : QString *why = nullptr) -> bool;
72 : /**
73 : * @brief Write the enabled recipients to a .gpg-id, atomically: a write
74 : * that fails halfway (full disk, dead network share) leaves the previous
75 : * file in place instead of a truncated list that would then be signed
76 : * and encrypted to.
77 : * @param gpgIdFile Path to .gpg-id file.
78 : * @param users List of recipients.
79 : * @param written Receives the exact bytes written, if not null: what the
80 : * signature is then made over.
81 : * @return true when the file was written; false after reporting through
82 : * critical().
83 : */
84 : auto writeGpgIdFile(const QString &gpgIdFile, const QList<UserInfo> &users,
85 : QByteArray *written = nullptr) -> bool;
86 : /**
87 : * @brief Sign a `.gpg-id` with the configured key and verify the result;
88 : * failures are reported through critical().
89 : * @param gpgIdFile Path to .gpg-id file.
90 : * @param contents The bytes just written as that file: the signature is
91 : * made over these, not over whatever is under the name by then.
92 : * @return true on success, false on failure.
93 : */
94 : auto signGpgIdFile(const QString &gpgIdFile, const QByteArray &contents)
95 : -> bool;
96 : /**
97 : * @brief Refuse a folder, or a `.gpg-id`/`.gpg-id.sig` in it, that is a
98 : * link: writing there would reach outside the store.
99 : * @param path The folder, with its trailing separator.
100 : * @return Whether it was refused (and the user told).
101 : */
102 : auto refuseLinkedGpgIdFolder(const QString &path) -> bool;
103 : /**
104 : * @brief The recipients to encrypt @p file to: its folder's `.gpg-id`,
105 : * verified when a signing key is set.
106 : * @param file The entry, absolute.
107 : * @param recipients Receives the key IDs.
108 : * @return false when there are none to use (the user was told why).
109 : */
110 : auto recipientsForEntry(const QString &file, QStringList *recipients) -> bool;
111 : /**
112 : * @brief The file a copy of @p src to @p dest writes: like `pass cp`, into
113 : * an existing folder with `.gpg` appended. Refused (the user told) when it
114 : * is a link, the source itself, or taken and @p force is off.
115 : * @param src The entry to copy.
116 : * @param dest Where to, a file or a folder.
117 : * @param force Whether an existing entry may be replaced.
118 : * @return The file, or an empty string when refused.
119 : */
120 : auto copyDestination(const QString &src, const QString &dest, bool force)
121 : -> QString;
122 : /**
123 : * @brief Put the generation header on a list that is about to be signed:
124 : * reserve a generation above what this device accepted and what the
125 : * verified list on disk says, and bind the list to its folder, so no older
126 : * or relocated signed list comes back.
127 : * @param gpgIdFile The `.gpg-id` being written.
128 : * @param signer Its signer, which verifies the list on disk.
129 : * @param contents The key IDs, one per line; receives the header too.
130 : * @return false when no generation could be reserved (the user was told);
131 : * an unrecordable list must not be written, it would let the old one back.
132 : */
133 : auto addGenerationHeader(const QString &gpgIdFile, const GpgIdSigner &signer,
134 : QByteArray *contents) -> bool;
135 : /**
136 : * @brief Bring the signature in line with the list just written: a new one
137 : * when signing is on, the old one removed when it is off (pass and other
138 : * clients would reject the new list under it).
139 : * @param gpgIdFile The `.gpg-id` that was written.
140 : * @param written Its bytes, which are what gets signed.
141 : * @param signer Whether and with which keys to sign.
142 : * @param useGit Whether the store is a Git repository QtPass manages.
143 : * @param sigToCommit Receives the signature file to commit, if any.
144 : * @return false when signing or removing failed (the user was told).
145 : */
146 : auto settleSignature(const QString &gpgIdFile, const QByteArray &written,
147 : const GpgIdSigner &signer, bool useGit,
148 : QString *sigToCommit) -> bool;
149 : /**
150 : * @brief Stage a `.gpg-id` and, when given, its `.sig`, and commit both in
151 : * one commit, so no commit in the history has a recipient list without
152 : * the signature that covers it. Nothing is committed when neither file
153 : * changed.
154 : * @param gpgIdFile Absolute path of the `.gpg-id`.
155 : * @param gpgIdSigFile Absolute path of the `.gpg-id.sig`, or empty when no
156 : * signing key is configured.
157 : * @param out Receives the concatenated stdout of the git commands.
158 : * @param err Receives the concatenated stderr of the git commands.
159 : * @return Exit code of the first failing git command, 0 on success.
160 : */
161 : auto gitAddGpgId(const QString &gpgIdFile, const QString &gpgIdSigFile,
162 : QString *out, QString *err) -> int;
163 : /**
164 : * @brief Check whether git already tracks a file in the store.
165 : * @param file Absolute path inside the password store.
166 : * @return true when the file is in the git index.
167 : */
168 : auto gitTracks(const QString &file) -> bool;
169 : /**
170 : * @brief The start of a `git commit` for the store: "commit", and "-S"
171 : * when git config pass.signcommits is true there, as pass signs its own.
172 : * @return The arguments, to which the message and paths are appended.
173 : */
174 : auto commitArgs() -> QStringList;
175 : /**
176 : * @brief Recipients for the directory of @p file, from its verified
177 : * `.gpg-id`.
178 : * @param file Password file path.
179 : * @param verified Cache of `.gpg-id` path to the recipients its verified
180 : * contents held; a hit skips gpg and reuses exactly that list.
181 : * @param gpgId Output parameter for recipient key IDs, sorted.
182 : * @return true on success, false when the file is unreadable or its
183 : * signature is bad (reported through critical()).
184 : */
185 : auto verifyGpgIdForDir(const QString &file,
186 : QHash<QString, QStringList> &verified,
187 : QStringList &gpgId) -> bool;
188 : /**
189 : * @brief Create git backup commit before re-encryption.
190 : * @return true if backup created or not needed, false if backup failed.
191 : */
192 : auto createBackupCommit() -> bool;
193 : /**
194 : * @brief Read recipients from file.
195 : * @param fileName Path to file.
196 : * @return List of key IDs.
197 : */
198 : auto getKeysFromFile(const QString &fileName) -> QStringList;
199 : /**
200 : * @brief Re-encrypt single file with new recipients.
201 : * @param fileName File to re-encrypt.
202 : * @param recipients New recipient key IDs.
203 : * @param why Receives, for a failure the user can act on (the new
204 : * ciphertext could not be put under the entry's name), the reason for the
205 : * run's summary; left empty for a gpg failure, which is logged.
206 : * @return true on success, false on failure.
207 : */
208 : auto reencryptSingleFile(const QString &fileName,
209 : const QStringList &recipients, QString *why) -> bool;
210 : /**
211 : * @brief Decrypt @p fileName for re-encryption.
212 : * @param fileName The entry to read.
213 : * @param plaintext Receives what gpg printed, with a trailing newline.
214 : * @return Whether gpg decrypted it to something.
215 : */
216 : auto decryptEntry(const QString &fileName, QString *plaintext) -> bool;
217 : /**
218 : * @brief Encrypt @p plaintext to @p recipients, into @p output.
219 : * @param output Where gpg writes the ciphertext; outside the store.
220 : * @param recipients The keys to encrypt to; gpg.conf adds none.
221 : * @param plaintext What to encrypt, fed on stdin.
222 : * @return Whether gpg wrote the ciphertext.
223 : */
224 : auto encryptFor(const QString &output, const QStringList &recipients,
225 : const QString &plaintext) -> bool;
226 : /**
227 : * @brief Decrypt @p ciphertext again and compare it with @p plaintext, so
228 : * nothing replaces an entry until it is known to hold what it held.
229 : * @param ciphertext The file gpg just wrote.
230 : * @param plaintext What it must decrypt to.
231 : * @return Whether it does.
232 : */
233 : auto ciphertextHolds(const QString &ciphertext, const QString &plaintext)
234 : -> bool;
235 : /**
236 : * @brief Stage and commit a re-encrypted entry, when the store is a Git
237 : * repository QtPass manages.
238 : * @param fileName The entry that was replaced.
239 : * @return Whether the repository is up to date with the file; false makes
240 : * the run count the file as failed, so a partial store is not pushed.
241 : */
242 : auto commitReencrypted(const QString &fileName) -> bool;
243 : /**
244 : * @brief Resolve destination for move operation.
245 : * @param src Source path.
246 : * @param dest Destination path.
247 : * @param force Overwrite existing.
248 : * @return Resolved destination path.
249 : */
250 : auto resolveMoveDestination(const QString &src, const QString &dest,
251 : bool force) -> QString;
252 :
253 : public:
254 : /**
255 : * @brief Execute git move operation.
256 : * @param src Source path.
257 : * @param destFile Destination path.
258 : * @param force Overwrite existing.
259 : */
260 : void executeMoveGit(const QString &src, const QString &destFile, bool force);
261 :
262 : /**
263 : * @brief Commit changes to git.
264 : * @param file Changed file path.
265 : * @param msg Commit message.
266 : */
267 : void gitCommit(const QString &file, const QString &msg);
268 :
269 : /**
270 : * @brief Execute git command.
271 : * @param id Process identifier.
272 : * @param args Git arguments.
273 : * @param input Input to stdin.
274 : * @param readStdout Capture stdout.
275 : * @param readStderr Capture stderr.
276 : */
277 : void executeGit(PROCESS id, const QStringList &args,
278 : QString input = QString(), bool readStdout = true,
279 : bool readStderr = true);
280 : /**
281 : * @brief Git is enabled and an executable is configured.
282 : * @return true when git commands can actually run.
283 : */
284 : auto gitConfigured() const -> bool;
285 : /**
286 : * @brief gitConfigured() plus a status message when git is enabled but
287 : * no executable is configured.
288 : * @return true when git commands can actually run.
289 : */
290 : auto gitReady() -> bool;
291 : /**
292 : * @brief Execute GPG command.
293 : * @param id Process identifier.
294 : * @param args GPG arguments.
295 : * @param input Input to stdin.
296 : * @param readStdout Capture stdout.
297 : * @param readStderr Capture stderr.
298 : */
299 : void executeGpg(PROCESS id, const QStringList &args,
300 : QString input = QString(), bool readStdout = true,
301 : bool readStderr = true);
302 :
303 : /**
304 : * @class TransactionHelper
305 : * @brief RAII helper for wrapping operations in transactions.
306 : */
307 : class TransactionHelper {
308 : /** @brief The transaction being bracketed; not owned. */
309 : SimpleTransaction *m_transaction;
310 : /** @brief What the transaction reports as once its last step ends. */
311 : PROCESS m_result;
312 :
313 : public:
314 : /**
315 : * @brief Start transaction.
316 : * @param trans Transaction object.
317 : * @param result Result code on commit.
318 : */
319 : TransactionHelper(SimpleTransaction *trans, PROCESS result)
320 : : m_transaction(trans), m_result(result) {
321 101 : m_transaction->transactionStart();
322 64 : }
323 : /**
324 : * @brief End transaction on destruction.
325 : */
326 101 : ~TransactionHelper() { m_transaction->transactionEnd(m_result); }
327 : };
328 :
329 : protected:
330 : /// storeEnvironment() as it was when the worker started; the worker's
331 : /// processes run in it.
332 : QProcessEnvironment m_reencryptEnv;
333 : /**
334 : * @brief Executor::executeBlocking() for the re-encryption helpers.
335 : *
336 : * Runs in the store's environment (storeEnvironment(); on the worker, the
337 : * copy taken when it started), which carries the git ceiling QtPass's own
338 : * does not. On the owning thread this is a plain blocking run. On the worker
339 : * thread it hands m_reencryptCancel to the cancellable executeBlocking()
340 : * overload: nothing is started once the flag is set, and a process that is
341 : * running when it gets set is terminated (then killed) by the worker thread,
342 : * which is what lets cancelReencryptPath() and the destructor interrupt an
343 : * active gpg or git without touching its QProcess or pid.
344 : * @param app The program.
345 : * @param args Its arguments.
346 : * @param input What to write to its stdin.
347 : * @param process_out Receives stdout, if not null.
348 : * @param process_err Receives stderr, if not null.
349 : * @return Exit code, or -1 when refused, failed to start or interrupted.
350 : */
351 : [[nodiscard]] auto execBlocking(const QString &app, const QStringList &args,
352 : const QString &input = QString(),
353 : QString *process_out = nullptr,
354 : QString *process_err = nullptr) -> int;
355 : /**
356 : * @brief Overload capturing stdout (and stderr) without stdin input.
357 : * @param app The program.
358 : * @param args Its arguments.
359 : * @param process_out Receives stdout.
360 : * @param process_err Receives stderr, if not null.
361 : * @return As the overload above.
362 : */
363 : [[nodiscard]] auto execBlocking(const QString &app, const QStringList &args,
364 : QString *process_out,
365 : QString *process_err = nullptr) -> int;
366 : /**
367 : * @brief Run git with `-C <store>` in the store, blocking.
368 : * @param args The git arguments (without the executable or `-C`).
369 : * @param out Receives stdout; may be null.
370 : * @return true when git ran and exited 0 (git on and configured).
371 : */
372 : auto storeGit(const QStringList &args, QString *out) -> bool override;
373 : /**
374 : * @brief Handle process completion.
375 : * @param id Process identifier.
376 : * @param exitCode Process exit code.
377 : * @param out Standard output from the process.
378 : * @param err Standard error from the process.
379 : */
380 : void finished(int id, int exitCode, const QString &out,
381 : const QString &err) override;
382 :
383 : /**
384 : * @brief Whether the failed insert had placed its entry (see
385 : * placeEncryptedFile()) before a later git step failed.
386 : * @param err Not needed: the placement is known, not inferred.
387 : * @return true when the entry is in the store.
388 : */
389 0 : [[nodiscard]] auto insertWritten(const QString &err) const -> bool override {
390 : Q_UNUSED(err);
391 0 : return m_insertPlaced;
392 : }
393 :
394 : /**
395 : * @brief Open a transaction for every execution.
396 : *
397 : * In native (imitate) mode each wrapped command is a git/gpg transaction;
398 : * register it before the subprocess runs.
399 : * @param id Process identifier.
400 : */
401 : void beforeExecute(PROCESS id) override;
402 :
403 : public:
404 : /**
405 : * @brief Construct the backend and connect its content search.
406 : */
407 : ImitatePass();
408 : /**
409 : * @brief Cancel a running content search, and cancel and join a running
410 : * re-encryption worker, before the members they use go away.
411 : */
412 : ~ImitatePass() override;
413 :
414 : // Git operations
415 : /**
416 : * @brief Initialize Git repository.
417 : */
418 : void GitInit() override;
419 : /**
420 : * @brief Pull from remote.
421 : */
422 : void GitPull() override;
423 : /**
424 : * @brief Pull with rebase.
425 : */
426 : void GitPull_b() override;
427 : /**
428 : * @brief Push to remote.
429 : */
430 : void GitPush() override;
431 :
432 : // Password operations
433 : /**
434 : * @brief Show decrypted password.
435 : * @param file Path to the password file relative to store root.
436 : */
437 : void Show(QString file) override;
438 : /**
439 : * @brief Insert new password.
440 : * @param file Path to the password file.
441 : * @param newValue Password content to store.
442 : * @param overwrite true to overwrite an existing file.
443 : */
444 : void Insert(QString file, QString newValue, bool overwrite) override;
445 : /**
446 : * @brief Remove password.
447 : * @param file Path to the file or directory to remove.
448 : * @param isDir true if removing a directory.
449 : */
450 : void Remove(QString file, bool isDir) override;
451 : /**
452 : * @brief Initialize store.
453 : *
454 : * Writes (and signs) the folder's `.gpg-id`, commits it when Git is used
455 : * and re-encrypts the folder to it. When that commit fails the entries are
456 : * still re-encrypted, without any Git step, and a critical() afterwards
457 : * says the change is not committed and whether the re-encryption
458 : * completed.
459 : * @param path Root path of the password store.
460 : * @param users List of recipient GPG keys.
461 : */
462 : void Init(QString path, const QList<UserInfo> &users) override;
463 :
464 : /**
465 : * @brief Re-encrypt entire directory.
466 : *
467 : * Emits startReencryptPath() synchronously, then runs the scan and the
468 : * per-file gpg calls on a worker thread. Progress is reported through
469 : * reencryptProgress(); per-file failures are collected and reported in a
470 : * single critical() once the run is over, followed by endReencryptPath().
471 : * A second call while a run is active is ignored.
472 : * @param dir Directory path.
473 : */
474 : void reencryptPath(const QString &dir);
475 : /**
476 : * @brief Re-encrypt @p dir under @p settings, the current profile's.
477 : *
478 : * This backend re-encrypts also when pass is the active one, and then is
479 : * not re-initialised on a profile switch: without this it would decrypt
480 : * the current profile's entries and encrypt them to the recipients and
481 : * signing policy of the profile it last ran for. The settings are taken
482 : * only when no run is active, so a running one keeps its own.
483 : * @param dir Directory path.
484 : * @param settings The current settings.
485 : */
486 : void reencryptPath(const QString &dir, const AppSettings &settings);
487 : /**
488 : * @brief Ask a running reencryptPath() to stop.
489 : *
490 : * Only sets a flag. The worker starts no further gpg or git process, and
491 : * the one it is blocked on (gpg waiting on pinentry, say) is terminated by
492 : * the worker itself, and killed if it is still running after a short grace
493 : * period, so the cancel takes effect promptly rather than after the current
494 : * file. The store stays
495 : * consistent: reencryptSingleFile() only replaces a file once the new
496 : * ciphertext is verified, so an interrupted file is left untouched, or
497 : * already re-encrypted but not yet committed when git was the process that
498 : * was interrupted. The remaining files are left untouched and the
499 : * interrupted file is not reported as a failure. endReencryptPath() is
500 : * still emitted when the worker has stopped.
501 : */
502 : void cancelReencryptPath();
503 :
504 : signals:
505 : /**
506 : * @brief A reencryptPath() run is about to start its worker thread.
507 : *
508 : * The main window disables the interface and shows a progress dialog
509 : * with a Cancel button on it. Always followed by endReencryptPath().
510 : */
511 : void startReencryptPath();
512 : /**
513 : * @brief A reencryptPath() run is over, whether it finished, failed, was
514 : * cancelled or was refused; the main window releases the interface.
515 : *
516 : * Also emitted without a preceding startReencryptPath() when the folder
517 : * is refused before anything starts.
518 : */
519 : void endReencryptPath();
520 : /**
521 : * @brief Re-encryption progress: @p current of @p total files checked.
522 : *
523 : * Emitted from the worker thread; connect with a queued (or automatic)
524 : * connection.
525 : * @param current Number of files checked so far, including this one.
526 : * @param total Total number of files found by the scan.
527 : */
528 : void reencryptProgress(int current, int total);
529 :
530 : // Pass interface
531 : public:
532 : /**
533 : * @brief Move password file.
534 : * @param src Source path.
535 : * @param dest Destination path.
536 : * @param force true to overwrite existing destination.
537 : */
538 : void Move(const QString src, const QString dest, const bool force) override;
539 : /**
540 : * @brief Copy password file.
541 : * @param src Source path.
542 : * @param dest Destination path.
543 : * @param force true to overwrite existing destination.
544 : */
545 : void Copy(const QString src, const QString dest, const bool force) override;
546 : /**
547 : * @brief Search all password content by GPG-decrypting each .gpg file.
548 : *
549 : * Pattern is interpreted as a QRegularExpression (PCRE-like); RealPass::Grep
550 : * uses the same search, so both backends match alike.
551 : *
552 : * @param pattern Search pattern (QRegularExpression).
553 : * @param caseInsensitive true for case-insensitive search.
554 : */
555 : void Grep(QString pattern, bool caseInsensitive) override;
556 :
557 : private:
558 : /// Groups the git/gpg processes of one operation (Insert, Remove, Move,
559 : /// Copy) into a single completion; see finished() and TransactionHelper.
560 : SimpleTransaction m_transaction;
561 : /// Background search over the decrypted store; relays to finishedGrep.
562 : NativeGrep m_grep;
563 : /**
564 : * @brief stdout of the steps of the transaction in flight, collected by
565 : * finished() and handed on, then cleared, when its last step ends.
566 : */
567 : QString m_transactionOutput;
568 :
569 : /// An Insert() whose gpg is running or queued: the private directory gpg
570 : /// writes into (removed with it), the entry it is for, and whether an
571 : /// existing entry may be replaced. Consumed by finished() when the gpg
572 : /// step ends; one per Insert(), in order.
573 : struct PendingInsert {
574 : /** @brief The private directory gpg writes into; removed with this. */
575 : std::shared_ptr<QTemporaryDir> scratch;
576 : /** @brief The ciphertext file gpg writes, inside @ref scratch. */
577 : QString output;
578 : /** @brief The entry's path in the store, where @ref output goes. */
579 : QString file;
580 : /** @brief Whether an entry already at @ref file may be replaced. */
581 : bool overwrite = false;
582 : };
583 : /** @brief The inserts whose gpg step has not ended yet, oldest first. */
584 : QQueue<PendingInsert> m_pendingInserts;
585 : /// The insert whose gpg step ended last placed its entry in the store; its
586 : /// git steps may still fail. Set per gpg completion in finished(), not per
587 : /// Insert() call, so a second queued insert's failure is not reported with
588 : /// an earlier insert's answer.
589 : bool m_insertPlaced = false;
590 :
591 : /**
592 : * @brief Bring the ciphertext gpg wrote to @p output into the store as
593 : * @p file (Util::copyFileReplacing): through a temporary created next to
594 : * the entry and written by its open handle, then the operating system's
595 : * rename, which replaces the entry under that name (or, without
596 : * @p overwrite, fails when one has appeared) and follows nothing. Reports
597 : * nothing itself: Insert()'s finished() routes the reason through the
598 : * failed-operation path once the queued git steps are cancelled, and
599 : * re-encryption lists it in the run's summary.
600 : * @param output The file gpg wrote, in a directory of QtPass's own.
601 : * @param file The entry's path.
602 : * @param overwrite Whether an entry already there may be replaced.
603 : * @param error Receives why @p file could not be written, if not.
604 : * @return Whether @p file now holds the new ciphertext.
605 : */
606 : auto placeEncryptedFile(const QString &output, const QString &file,
607 : bool overwrite, QString *error) -> bool;
608 :
609 : /**
610 : * @brief Outcome of one reencryptPath() run; filled on the worker thread and
611 : * consumed by finishReencrypt() on the owning thread.
612 : */
613 : struct ReencryptResult;
614 : /// Set while a re-encryption run is pending or on the worker thread.
615 : bool m_reencryptActive = false;
616 : /// Set by cancelReencryptPath() and the destructor; read by the worker
617 : /// between files and polled by Executor::executeBlocking() while the worker
618 : /// waits on a process, which the worker then ends itself. The flag is the
619 : /// only thing shared with the worker: its QProcess is not thread-safe and
620 : /// is never touched from another thread.
621 : std::atomic<bool> m_reencryptCancel{false};
622 : /**
623 : * @brief The re-encryption worker while it runs, null otherwise. The
624 : * destructor sets m_reencryptCancel and joins it.
625 : */
626 : QThread *m_reencryptThread = nullptr;
627 : /**
628 : * @brief Start the worker thread once the Executor queue is idle.
629 : * @param dir Directory passed to reencryptPath().
630 : */
631 : void startReencryptWorker(const QString &dir);
632 : /**
633 : * @brief Worker-thread body: scan @p dir and re-encrypt stale files.
634 : * @param dir Directory passed to reencryptPath().
635 : * @return Counts and the list of files that could not be re-encrypted.
636 : */
637 : auto reencryptFiles(const QString &dir) -> ReencryptResult;
638 : /**
639 : * @brief Pull before a run, when the store is a Git repository and
640 : * automatic pulling is on.
641 : * @return Whether re-encrypting may go ahead: false when a failed pull
642 : * left unmerged files or the check for unmerged files fails.
643 : */
644 : auto pullBeforeReencrypt() -> bool;
645 : /**
646 : * @brief The entries under @p dir a run considers, telling the user about
647 : * anything skipped for not being a regular file.
648 : * @param dir Directory to walk, recursively.
649 : * @return The `.gpg` files, links and special files left out.
650 : */
651 : auto entriesToReencrypt(const QString &dir) -> QStringList;
652 : /**
653 : * @brief The verified recipients for the folder @p fileName sits in.
654 : * @param fileName An entry in the folder.
655 : * @param verified Cache of the `.gpg-id` files already checked.
656 : * @param gpgId Receives the recipients to encrypt that folder to.
657 : * @param result Marked cancelled or aborted when the answer is no.
658 : * @return Whether the run may go on with this folder.
659 : */
660 : auto recipientsForDir(const QString &fileName,
661 : QHash<QString, QStringList> &verified,
662 : QStringList &gpgId, ReencryptResult &result) -> bool;
663 : /**
664 : * @brief Deal with what a crashed or failed earlier run left under @p dir
665 : * before touching anything: stale temporaries (today's staged
666 : * `.qtpass-XXXXXX.tmp`, 1.8.x's `<entry>.reencrypt.tmp`, the
667 : * `<entry>.XXXXXX.tmp` of builds in between) are removed unless they were
668 : * modified within the hour, where they may be another QtPass's write in
669 : * flight on a shared store; a 1.8.x `<entry>.reencrypt.bak` whose original
670 : * is missing is put back, one next to a present original is reported and
671 : * left alone (both are valid ciphertexts, the choice is the user's).
672 : * @param dir Directory to scan, recursively.
673 : * @return false when something was reported that needs a human first.
674 : */
675 : auto recoverReencryptLeftovers(const QString &dir) -> bool;
676 : /**
677 : * @brief Remove a stale temporary a crashed run left, unless it was
678 : * modified within the hour: on a shared store it may be another QtPass's
679 : * write in flight under the same name.
680 : * @param path The temporary, a regular file.
681 : */
682 : void dropStaleTemporary(const QString &path);
683 : /**
684 : * @brief Put a 1.8.x backup back in its entry's place when the entry is
685 : * missing; report it when both exist (both are valid ciphertexts, the
686 : * choice is the user's) or when the rename fails.
687 : * @param path The `<entry>.reencrypt.bak` file.
688 : * @return Whether nothing needs a human first.
689 : */
690 : auto restoreBackup(const QString &path) -> bool;
691 : /**
692 : * @brief Tell the user a leftover is not a regular file and was left.
693 : * @param path The leftover.
694 : */
695 : void reportUnrestorable(const QString &path);
696 : /**
697 : * @brief Report the outcome of a run and emit endReencryptPath().
698 : * @param result Outcome produced by reencryptFiles().
699 : */
700 : void finishReencrypt(const ReencryptResult &result);
701 :
702 : /**
703 : * @brief A `.gpg-id` Init() wrote but could not commit.
704 : */
705 6 : struct UncommittedGpgId {
706 : QString folder; ///< The folder whose recipients changed.
707 : QString gitError; ///< What Git said when the commit failed.
708 : };
709 : /// Set by reencryptWithoutGit() for the run it starts and cleared by
710 : /// finishReencrypt(): that run does no Git at all (no pull, backup
711 : /// commit, per-entry commit or push) and ends with
712 : /// reportUncommittedGpgId(). Written only on the owning thread while no
713 : /// worker runs; the worker only reads it, through reencryptUsesGit().
714 : std::optional<UncommittedGpgId> m_uncommittedGpgId;
715 : /**
716 : * @brief gitConfigured(), unless the current run is one
717 : * reencryptWithoutGit() started.
718 : * @return Whether the re-encryption run may call Git.
719 : */
720 : auto reencryptUsesGit() const -> bool;
721 : /**
722 : * @brief Re-encrypt @p dir to the `.gpg-id` Init() wrote although its
723 : * commit failed, without any Git step (they would fail the same way), and
724 : * tell the user afterwards that the change is not committed.
725 : * @param dir The folder whose recipients changed.
726 : * @param gitError What Git said when the commit failed.
727 : */
728 : void reencryptWithoutGit(const QString &dir, const QString &gitError);
729 : /**
730 : * @brief Tell the user that the recipients of @p dir changed on disk but
731 : * the change is not committed, and whether the entries were re-encrypted.
732 : * @param dir The folder whose recipients changed.
733 : * @param gitError What Git said when the commit failed.
734 : * @param reencrypted Whether every entry was re-encrypted to the new
735 : * recipients; false means some or all are still encrypted to the
736 : * previous ones.
737 : */
738 : void reportUncommittedGpgId(const QString &dir, const QString &gitError,
739 : bool reencrypted);
740 :
741 : /**
742 : * @brief Translate @p path for the given @p exe when WSL-routed: wraps in
743 : * wslpath substitution, otherwise returns @p path unchanged.
744 : * @param path Native filesystem path.
745 : * @param exe Executable path (checked for "wsl " prefix).
746 : * @return Path suitable for the given executable.
747 : */
748 : auto translatePathForWsl(const QString &path, const QString &exe) const
749 : -> QString;
750 : /**
751 : * @brief Translate @p path for git when WSL-routed: wraps in wslpath
752 : * substitution, otherwise returns @p path unchanged.
753 : * @param path Native filesystem path.
754 : * @return Path suitable for the configured git executable.
755 : */
756 : auto pgit(const QString &path) const -> QString;
757 : /**
758 : * @brief Translate @p path for gpg when WSL-routed: wraps in wslpath
759 : * substitution, otherwise returns @p path unchanged.
760 : * @param path Native filesystem path.
761 : * @return Path suitable for the configured gpg executable.
762 : */
763 : auto pgpg(const QString &path) const -> QString;
764 : };
765 :
766 : #endif // SRC_IMITATEPASS_H_
|