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 RealPass, the backend that hands every store operation to the `pass`
6 : * command-line tool.
7 : */
8 : #ifndef SRC_REALPASS_H_
9 : #define SRC_REALPASS_H_
10 :
11 : #include "nativegrep.h"
12 : #include "pass.h"
13 :
14 : class GpgIdSigner;
15 : #include <QQueue>
16 : #include <QSet>
17 :
18 : /**
19 : * @class RealPass
20 : * @ingroup backend
21 : * @brief Implementation of Pass that wraps the 'pass' command-line tool.
22 : *
23 : * RealPass delegates all password store operations to the external 'pass'
24 : * utility. It provides a Qt-native interface while handling:
25 : * - Git integration (init, pull, push)
26 : * - Password CRUD (show, insert, remove, move, copy)
27 : * - OTP generation
28 : * - Store initialization with GPG keys
29 : *
30 : * This is the primary implementation when 'pass' is available on the system.
31 : */
32 : class RealPass : public Pass {
33 : /// The store's own search: `pass grep` runs `find -L`, which follows a
34 : /// link out of the store and decrypts what it finds there.
35 : NativeGrep m_grep;
36 : /// A linked folder whose removal commit is queued, with the store it
37 : /// was queued for: the profile may change before the commit ends.
38 : struct LinkCommit {
39 : QString rel; ///< The folder, relative to the store.
40 : QString passExecutable; ///< The pass that runs the commit.
41 : /// Its environment (PASSWORD_STORE_DIR). Not "environ": a macro on
42 : /// Windows (stdlib.h).
43 : QProcessEnvironment environment;
44 : };
45 : /// Queued linked-folder removal commits (as GIT_COMMIT), oldest first;
46 : /// finished() judges each.
47 : QQueue<LinkCommit> m_linkCommits;
48 :
49 : /**
50 : * @brief Judge a queued linked-folder removal commit by what is left
51 : * staged in the store it ran in: `pass git` exits 0 whatever git did.
52 : * @param commit The folder and the store it was queued for.
53 : * @param out The commit's standard output.
54 : * @param err The commit's standard error.
55 : */
56 : void finishLinkCommit(const LinkCommit &commit, const QString &out,
57 : const QString &err);
58 :
59 : /**
60 : * @brief Execute pass command with arguments.
61 : * @param id Process identifier.
62 : * @param args Command arguments.
63 : * @param input Input to pass to stdin.
64 : * @param readStdout Capture stdout.
65 : * @param readStderr Capture stderr.
66 : */
67 : void executePass(PROCESS id, const QStringList &args,
68 : QString input = QString(), bool readStdout = true,
69 : bool readStderr = true);
70 : /**
71 : * @brief Shared implementation of Move and Copy via `pass mv`/`pass cp`.
72 : * @param id Process id (PASS_MOVE or PASS_COPY).
73 : * @param subcommand The pass subcommand, "mv" or "cp".
74 : * @param src Source path.
75 : * @param dest Destination path.
76 : * @param force Overwrite an existing destination; without it a
77 : * file-onto-file request is a no-op (pass would otherwise force).
78 : */
79 : void passMoveOrCopy(PROCESS id, const QString &subcommand, const QString &src,
80 : const QString &dest, bool force);
81 : /**
82 : * @brief @p path as `pass` names it: relative to the store, without the
83 : * `.gpg` pass appends itself.
84 : * @param path The file or folder, absolute or relative to the working
85 : * directory.
86 : * @param stripGpg Whether to drop a trailing `.gpg`.
87 : * @return The store-relative name.
88 : */
89 : auto passName(const QString &path, bool stripGpg) const -> QString;
90 :
91 : /**
92 : * @brief Whether the failed `pass insert` wrote its entry before its git
93 : * step failed. pass does not say, so: the entry's bytes differ from what
94 : * they were when that insert started to run (a new entry appeared, or an
95 : * existing one was rewritten), and pass did not say that the encryption
96 : * broke off (gpg can leave a partial file behind then).
97 : * @param err What pass reported on stderr.
98 : * @return true when the entry was written.
99 : */
100 : [[nodiscard]] auto insertWritten(const QString &err) const -> bool override;
101 :
102 : /**
103 : * @brief With signing on, refuse to have pass encrypt @p file when the
104 : * signed list it would use is not one QtPass accepts: its folder's own
105 : * list was passed over, the signature does not verify, or the list is
106 : * older than one this device accepted or signed for another folder.
107 : * @param file The entry (or the place a move or copy writes to).
108 : * @return Whether it was refused (and the user told).
109 : */
110 : auto refuseUnverifiedList(const QString &file) -> bool;
111 :
112 : /**
113 : * @brief Run a git command in the store through `pass git`, blocking.
114 : * @param args The git arguments (without `git`).
115 : * @param out Receives stdout; may be null.
116 : * @return true when `pass git` ran and exited 0 (git on).
117 : */
118 : auto storeGit(const QStringList &args, QString *out) -> bool override;
119 :
120 : /**
121 : * @brief For a folder pass is about to re-encrypt in a move or copy:
122 : * refuse when the signed list of one of its entries (every one pass's
123 : * walk takes, hidden ones included) is not one QtPass accepts where it
124 : * lies now (as refuseUnverifiedList()).
125 : * @param dir The folder moved or copied, or the existing folder written
126 : * into. Each of its folders' lists counts, used by an entry yet or
127 : * not: a copy merges into a folder of the same name, so lists on
128 : * either side meet the other side's entries.
129 : * @param onlyListsWithin Whether only lists inside @p dir count: for the
130 : * folder that moves, whose entries get the destination's list in
131 : * place of any list above it.
132 : * @param except A list about to be written (`pass init`'s own), neither
133 : * verified nor refused for being missing.
134 : * @return Whether it was refused (and the user told).
135 : */
136 : auto refuseUnverifiedTree(const QString &dir, bool onlyListsWithin,
137 : const QString &except = QString()) -> bool;
138 :
139 : /**
140 : * @brief The verifier for signed lists, gpg in the store's environment.
141 : * @return A GpgIdSigner with the configured signing keys.
142 : */
143 : auto listSigner() -> GpgIdSigner;
144 :
145 : /**
146 : * @brief The checks of refuseUnverifiedList() for @p file's nearest list,
147 : * once per list.
148 : * @param file The entry.
149 : * @param signer The verifier.
150 : * @param verified The lists already found good; extended.
151 : * @param within When set, only a list under this folder (absolute,
152 : * cleaned, with a trailing "/") is verified; the skipped-list check
153 : * applies regardless.
154 : * @param except A list about to be written, neither verified nor refused
155 : * for being missing.
156 : * @return Whether it was refused (and the user told).
157 : */
158 : auto refuseList(const QString &file, const GpgIdSigner &signer,
159 : QSet<QString> &verified, const QString &within = QString(),
160 : const QString &except = QString()) -> bool;
161 :
162 : /**
163 : * @brief The executor is about to run (or drop) its next item: for an
164 : * insert, take its entry from m_insertQueue and fingerprint it now, not
165 : * when it was queued, so an earlier queued save of the same entry counts
166 : * as "before". Kept in m_insertPending for that insert's own completion.
167 : * @param id The item's process id.
168 : * @param completes Whether a completion follows (not for a dropped one).
169 : */
170 : void onDequeuing(int id, bool completes);
171 :
172 : /**
173 : * @brief A SHA-256 of the regular file @p path (opened without following
174 : * a link or blocking on a FIFO).
175 : * @param path The entry's file.
176 : * @return The hash; empty when @p path is no readable regular file.
177 : */
178 : static auto entryFingerprint(const QString &path) -> QByteArray;
179 :
180 : /// An insert's entry and its fingerprint when the insert started to run
181 : /// (empty when the entry did not exist).
182 77 : struct InsertRecord {
183 : QString target; ///< The entry's file.
184 : QByteArray before; ///< Its fingerprint when the insert started.
185 : };
186 : /// The entries of the queued inserts, oldest first.
187 : QQueue<QString> m_insertQueue;
188 : /// The inserts that left the queue and have yet to complete, in the
189 : /// order their completions come: a failed start's error is posted, and
190 : /// the next insert may start before it arrives.
191 : QQueue<InsertRecord> m_insertPending;
192 : /// The insert whose completion is being handled.
193 : InsertRecord m_insertDone;
194 :
195 : public:
196 : /**
197 : * @brief Construct a RealPass instance.
198 : */
199 : RealPass();
200 : /**
201 : * @brief Destructor; lets the search workers wind down.
202 : */
203 : ~RealPass() override;
204 :
205 : // Git operations
206 : /**
207 : * @brief Initialize Git repository in password store.
208 : */
209 : void GitInit() override;
210 : /**
211 : * @brief Pull changes from remote.
212 : */
213 : void GitPull() override;
214 : /**
215 : * @brief Pull with rebase.
216 : */
217 : void GitPull_b() override;
218 : /**
219 : * @brief Push changes to remote.
220 : */
221 : void GitPush() override;
222 :
223 : // Password operations
224 : /**
225 : * @brief Show decrypted password.
226 : * @param file Path to password file.
227 : */
228 : void Show(QString file) override;
229 : /**
230 : * @brief Insert new password.
231 : * @param file Path to password file.
232 : * @param newValue Password content.
233 : * @param overwrite true to overwrite existing.
234 : */
235 : void Insert(QString file, QString newValue, bool overwrite) override;
236 : /**
237 : * @brief Remove an entry or folder with `pass rm`.
238 : *
239 : * A folder that is a link is unlinked here instead, without deleting its
240 : * target; that removal is committed only when Git is enabled and the
241 : * tracking lookup and index removal both report success.
242 : * @param file Store-relative entry path, without `.gpg` for a password
243 : * file.
244 : * @param isDir true to remove a directory; false to remove a password
245 : * file.
246 : */
247 : void Remove(QString file, bool isDir) override;
248 : /**
249 : * @brief Initialize password store.
250 : * @param path Store root path.
251 : * @param users GPG recipients.
252 : */
253 : void Init(QString path, const QList<UserInfo> &users) override;
254 :
255 : protected:
256 : /**
257 : * @brief Finish a process; a linked-folder removal commit (GIT_COMMIT,
258 : * which RealPass uses for nothing else) is judged by finishLinkCommit().
259 : * @param id Process identifier.
260 : * @param exitCode Exit code.
261 : * @param out Standard output.
262 : * @param err Standard error.
263 : */
264 : void finished(int id, int exitCode, const QString &out,
265 : const QString &err) override;
266 :
267 : // Pass interface
268 : public:
269 : /**
270 : * @brief Move password file or directory.
271 : * @param src Source path.
272 : * @param dest Destination path.
273 : * @param force Overwrite existing.
274 : */
275 : void Move(const QString src, const QString dest, const bool force) override;
276 : /**
277 : * @brief Copy password file or directory.
278 : * @param src Source path.
279 : * @param dest Destination path.
280 : * @param force Overwrite existing.
281 : */
282 : void Copy(const QString src, const QString dest, const bool force) override;
283 : /**
284 : * @brief Search all password content by GPG-decrypting each real `.gpg`
285 : * file, as ImitatePass does.
286 : *
287 : * Not `pass grep`: that enumerates with `find -L`, which follows a link
288 : * out of the store and decrypts what it finds there. Same pattern
289 : * semantics as ImitatePass::Grep (QRegularExpression).
290 : * @param pattern Search pattern (QRegularExpression).
291 : * @param caseInsensitive true for case-insensitive search.
292 : */
293 : void Grep(QString pattern, bool caseInsensitive) override;
294 : };
295 :
296 : #endif // SRC_REALPASS_H_
|