QtPass 2.0.0
Developer documentation
Loading...
Searching...
No Matches
pass.cpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2016 Anne Jan Brouwer
2// SPDX-License-Identifier: GPL-3.0-or-later
10#include "pass.h"
11
12#include "gpgidgeneration.h"
13#include "gpgidsigner.h"
14#include "gpgkeystate.h"
15#include "pathvalidator.h"
16#include "processinfo.h"
17#include "util.h"
18#include <QCoreApplication>
19#include <QDebug>
20#include <QDir>
21#include <QFile>
22#include <QFileInfo>
23#include <QHash>
24#include <QLocale>
25#include <QProcess>
26#include <QRandomGenerator>
27#include <QRegularExpression>
28#include <QTextStream>
29#include <array>
30#include <optional>
31#include <utility>
32
33#include "qtpasslogging.h"
34
35using Enums::GIT_INIT;
36using Enums::GIT_PULL;
37using Enums::GIT_PUSH;
45
46namespace {
47auto fallbackCharset(const QString &input, const QString &fallback) -> QString {
48 return input.isEmpty() ? fallback : input;
49}
50
51auto effectiveCharset(const PasswordConfiguration &passConfig) -> QString {
52 int sel = passConfig.selected;
53 if (sel < 0 || sel >= PasswordConfiguration::CHARSETS_COUNT)
55 return fallbackCharset(
56 passConfig.Characters[sel],
57 passConfig.Characters[PasswordConfiguration::ALLCHARS]);
58}
59} // namespace
60
61Pass::Pass() : env(QProcessEnvironment::systemEnvironment()) {
62 connect(&exec, &Executor::finished, this, &Pass::finished);
63 connect(&exec, &Executor::error, this, &Pass::finished);
64
66 connect(&exec, &Executor::drained, this, [this] {
67 // Idle means the queue is empty and nothing runs, so no insert is in
68 // flight: force the pending count to zero. A report that never arrived
69 // (the "No such transaction" path in ImitatePass::finished(), or any
70 // later change that drops an insert without reporting) would otherwise
71 // leave the count one too high for the rest of the session, and every
72 // later dialog would take its own report for an earlier one and wait for
73 // ever. Idle cannot fall between a dialog's insertsPending() read and its
74 // own report, since that insert keeps the executor busy.
76 emit idle();
77 });
78 // Each queued process starts in the environment as the store is then:
79 // the Git pin is judged at that moment (dequeuing() comes just before the
80 // start, on this thread).
81 connect(&exec, &Executor::dequeuing, this, [this](int, bool completes) {
82 if (completes) {
84 }
85 });
86 // Merge our vars into WSLENV rather than blindly appending a duplicate
87 // entry. WSLENV flags: /p translates the path, /u forwards from Windows
88 // into WSL only; /w would be the other direction, from WSL out, and leaves
89 // pass inside WSL without the value. Everything updateEnv() sets for pass
90 // has to cross, the signing key above all: without it pass does not check
91 // the .gpg-id signature.
92 const QStringList wslenvVars = {
93 QStringLiteral("PASSWORD_STORE_DIR/p"),
94 QStringLiteral("PASSWORD_STORE_SIGNING_KEY/u"),
95 QStringLiteral("PASSWORD_STORE_GENERATED_LENGTH/u"),
96 QStringLiteral("PASSWORD_STORE_CHARACTER_SET/u"),
97 QStringLiteral("PASSWORD_STORE_GPG_OPTS/u"),
98 QStringLiteral("GIT_CEILING_DIRECTORIES/p"),
99 QStringLiteral("GIT_LITERAL_PATHSPECS/u")};
100 const QString existing = env.value(QStringLiteral("WSLENV"));
101 if (existing.isEmpty()) {
102 env.insert(QStringLiteral("WSLENV"), wslenvVars.join(':'));
103 } else {
104 QStringList parts = existing.split(':', Qt::SkipEmptyParts);
105 for (const QString &v : wslenvVars) {
106 if (!parts.contains(v))
107 parts.append(v);
108 }
109 env.insert(QStringLiteral("WSLENV"), parts.join(':'));
110 }
111}
112
113void Pass::executeWrapper(PROCESS id, const QString &app,
114 const QStringList &args, bool readStdout,
115 bool readStderr) {
116 executeWrapper(id, app, args, QString(), readStdout, readStderr);
117}
118
119void Pass::executeWrapper(PROCESS id, const QString &app,
120 const QStringList &args, QString input,
121 bool readStdout, bool readStderr) {
122 beforeExecute(id);
123 qCDebug(lcQtPass) << app << loggableArgs(args);
124 exec.execute(id, m_settings.passStore, app, args, std::move(input),
125 readStdout, readStderr);
126}
127
128auto Pass::loggableArgs(const QStringList &args) -> QStringList {
129 // Secrets travel on stdin (Insert's content, KeygenDialog's batch file),
130 // never in argv; this keeps the log honest should that ever change.
131 static const QStringList secretOptions{
132 QStringLiteral("--passphrase"), QStringLiteral("--override-session-key")};
133 QStringList out;
134 bool hide = false;
135 for (const QString &arg : args) {
136 if (hide) {
137 out << QStringLiteral("<redacted>");
138 hide = false;
139 continue;
140 }
141 if (arg.startsWith(QStringLiteral("otpauth://"))) {
142 out << QStringLiteral("<redacted>");
143 continue;
144 }
145 if (arg.startsWith(QStringLiteral("--passphrase=")) ||
146 arg.startsWith(QStringLiteral("--override-session-key="))) {
147 out << arg.left(arg.indexOf(u'=') + 1) + QStringLiteral("<redacted>");
148 continue;
149 }
150 hide = secretOptions.contains(arg);
151 out << arg;
152 }
153 return out;
154}
155
157
158void Pass::init(const AppSettings &settings) {
159 m_settings = settings;
160#ifdef __APPLE__
161 // If it exists, prepend gpgtools to PATH
162 if (QFile(QStringLiteral("/usr/local/MacGPG2/bin")).exists())
163 env.insert(QStringLiteral("PATH"),
164 QStringLiteral("/usr/local/MacGPG2/bin:") +
165 env.value(QStringLiteral("PATH")));
166 // Add missing /usr/local/bin (exact component match, no leading colon)
167 const QString currentPath = env.value(QStringLiteral("PATH"));
168 if (!currentPath.split(':', Qt::SkipEmptyParts)
169 .contains(QStringLiteral("/usr/local/bin"))) {
170 env.insert(QStringLiteral("PATH"),
171 currentPath.isEmpty()
172 ? QStringLiteral("/usr/local/bin")
173 : QStringLiteral("/usr/local/bin:") + currentPath);
174 }
175#endif
176
177 // The configured gpgHome wins over GNUPGHOME only when it exists: a stale
178 // one (#1711) fails every gpg call with "No secret key". Clearing the
179 // setting restores the inherited value rather than keeping the old path.
180 const QString inheritedHome = QProcessEnvironment::systemEnvironment().value(
181 QStringLiteral("GNUPGHOME"));
182 const auto useInheritedHome = [this, &inheritedHome]() {
183 if (inheritedHome.isEmpty()) {
184 env.remove(QStringLiteral("GNUPGHOME"));
185 } else {
186 env.insert(QStringLiteral("GNUPGHOME"), inheritedHome);
187 }
188 };
189 if (m_settings.gpgHome.isEmpty()) {
190 useInheritedHome();
191 } else {
192 QDir absHome(m_settings.gpgHome);
193 absHome.makeAbsolute();
194 if (absHome.exists()) {
195 env.insert(QStringLiteral("GNUPGHOME"), absHome.path());
196 } else {
197 if (inheritedHome.isEmpty()) {
198 qCWarning(lcQtPass) << "gpgHome" << absHome.path()
199 << "does not exist; using the default GnuPG home";
200 emit statusMsg(tr("Configured GPG home %1 does not exist, using the "
201 "default keyring")
202 .arg(absHome.path()),
203 5000);
204 } else {
205 qCWarning(lcQtPass)
206 << "gpgHome" << absHome.path() << "does not exist; using GNUPGHOME"
207 << inheritedHome << "from the environment";
208 emit statusMsg(tr("Configured GPG home %1 does not exist, using "
209 "GNUPGHOME %2 from the environment")
210 .arg(absHome.path(), inheritedHome),
211 5000);
212 }
213 useInheritedHome();
214 }
215 }
216}
217
218auto Pass::generatePassword(unsigned int length, const QString &charset)
219 -> QString {
220 if (length == 0) {
221 emit critical(tr("Invalid password length"),
222 tr("Can't generate password with zero length."));
223 return {};
224 }
225 QString passwd;
226 if (m_settings.usePwgen) {
227 // --secure goes first as it overrides --no-* otherwise
228 QStringList args;
229 args.append("-1");
230 if (!m_settings.lessRandom) {
231 args.append("--secure");
232 }
233 args.append(m_settings.avoidCapitals ? "--no-capitalize" : "--capitalize");
234 args.append(m_settings.avoidNumbers ? "--no-numerals" : "--numerals");
235 if (m_settings.useSymbols) {
236 args.append("--symbols");
237 }
238 args.append(QString::number(length));
239 if (Executor::executeBlocking(m_settings.pwgenExecutable, args, &passwd) ==
240 0) {
241 static const QRegularExpression literalNewLines{"[\\n\\r]"};
242 passwd.remove(literalNewLines);
243 } else {
244 passwd.clear();
245 qCDebug(lcQtPass) << "pwgen fail";
246 // Report it, as the other generation failures do: otherwise Generate
247 // does nothing silently and OK then saves an empty (or, on Edit, the
248 // old) password.
249 emit critical(
250 tr("Password generation failed"),
251 tr("Could not run pwgen (\"%1\"). Check the pwgen program in the "
252 "settings, or turn \"Use pwgen\" off.")
253 .arg(m_settings.pwgenExecutable));
254 }
255 } else {
256 // An empty CUSTOM charset falls back to ALLCHARS, not a weak password
257 // (#780).
258 const QString cs = fallbackCharset(
259 charset, m_settings.passwordConfiguration
261 if (cs.length() > 0) {
262 passwd = generateRandomPassword(cs, length);
263 } else {
264 emit critical(
265 tr("No characters chosen"),
266 tr("Can't generate password, there are no characters to choose from "
267 "set in the configuration!"));
268 }
269 }
270 return passwd;
271}
272
273bool Pass::gpgSupportsEd25519(const QString &gpgExecutable) {
274 const QString exe =
275 gpgExecutable.isEmpty() ? QStringLiteral("gpg") : gpgExecutable;
276 QString out, err;
277 if (Executor::executeBlocking(exe, {"--version"}, &out, &err) != 0) {
278 return false;
279 }
280 QRegularExpression versionRegex(R"(gpg \‍(GnuPG\) (\d+)\.(\d+))");
281 QRegularExpressionMatch match = versionRegex.match(out);
282 if (!match.hasMatch()) {
283 return false;
284 }
285 int major = match.captured(1).toInt();
286 int minor = match.captured(2).toInt();
287 return major > 2 || (major == 2 && minor >= 1);
288}
289
290// ed25519 (GnuPG 2.1+) generates much faster than RSA.
291QString Pass::getDefaultKeyTemplate(const QString &gpgExecutable) {
292 if (gpgSupportsEd25519(gpgExecutable)) {
293 return QStringLiteral("%echo Generating a default key\n"
294 "Key-Type: EdDSA\n"
295 "Key-Curve: Ed25519\n"
296 "Subkey-Type: ECDH\n"
297 "Subkey-Curve: Curve25519\n"
298 "Name-Real: \n"
299 "Name-Comment: QtPass\n"
300 "Name-Email: \n"
301 "Expire-Date: 0\n"
302 "%no-protection\n"
303 "%commit\n"
304 "%echo done");
305 }
306 return QStringLiteral("%echo Generating a default key\n"
307 "Key-Type: RSA\n"
308 "Subkey-Type: RSA\n"
309 "Name-Real: \n"
310 "Name-Comment: QtPass\n"
311 "Name-Email: \n"
312 "Expire-Date: 0\n"
313 "%no-protection\n"
314 "%commit\n"
315 "%echo done");
316}
317
318namespace {
319auto resolveWslGpgconfPath(const QString &lastPart) -> QString {
320 qsizetype lastSep = lastPart.lastIndexOf('/');
321 if (lastSep < 0) {
322 lastSep = lastPart.lastIndexOf('\\');
323 }
324 if (lastSep >= 0) {
325 return lastPart.left(lastSep + 1) + "gpgconf";
326 }
327 return QStringLiteral("gpgconf");
328}
329
330QString findGpgconfInGpgDir(const QString &gpgPath) {
331 QFileInfo gpgInfo(gpgPath);
332 if (!gpgInfo.isAbsolute()) {
333 return {};
334 }
335
336 QDir dir(gpgInfo.absolutePath());
337
338#ifdef Q_OS_WIN
339 QFileInfo candidateExe(dir.filePath("gpgconf.exe"));
340 if (candidateExe.isExecutable()) {
341 return candidateExe.filePath();
342 }
343#endif
344
345 QFileInfo candidate(dir.filePath("gpgconf"));
346 if (candidate.isExecutable()) {
347 return candidate.filePath();
348 }
349 return {};
350}
351
352} // namespace
353
354auto Pass::resolveGpgconfCommand(const QString &gpgPath)
356 if (gpgPath.trimmed().isEmpty()) {
357 return {"gpgconf", {}};
358 }
359
360 // WSL: gpgconf next to the configured gpg, same distribution, via --exec
361 // not the shell. An unparsed form (`wsl sh -c ...`, bare `wsl`) falls back
362 // to gpgconf on the Windows PATH.
363 if (const auto wsl = Executor::parseWslCommand(gpgPath)) {
364 if (!QFileInfo(wsl->command).fileName().startsWith("gpg")) {
365 return {"gpgconf", {}};
366 }
367 const auto gpgconf = wsl->with(resolveWslGpgconfPath(wsl->command));
368 return {gpgconf.launcher, gpgconf.argv({})};
369 }
370
371 const QStringList parts = QProcess::splitCommand(gpgPath);
372 if (parts.isEmpty()) {
373 return {"gpgconf", {}};
374 }
375 const QString first = parts.first();
376 if (QFileInfo(first).fileName().compare("wsl", Qt::CaseInsensitive) == 0 ||
377 QFileInfo(first).fileName().compare("wsl.exe", Qt::CaseInsensitive) ==
378 0) {
379 return {"gpgconf", {}};
380 }
381
382 if (!first.contains('/') && !first.contains('\\')) {
383 return {"gpgconf", {}};
384 }
385
386 QString gpgconfPath = findGpgconfInGpgDir(first);
387 if (!gpgconfPath.isEmpty()) {
388 return {gpgconfPath, {}};
389 }
390
391 return {"gpgconf", {}};
392}
393
394void Pass::GenerateGPGKeys(QString batch) {
395 const QString gpgPath = m_settings.gpgExecutable;
396 if (gpgPath.isEmpty()) {
397 // The executor would fail an empty executable with a generic process
398 // error; the keygen dialog waits for generateGPGKeysFailed and a reason
399 // it can show. Queued so we do not re-enter KeygenDialog::done(), which
400 // drives key generation synchronously.
401 QMetaObject::invokeMethod(
402 this,
403 [this]() {
404 emit generateGPGKeysFailed(tr("No GPG executable configured"));
405 emit processErrorExit(1, tr("No GPG executable configured"));
406 },
407 Qt::QueuedConnection);
408 return;
409 }
410
411 // A stale gpg-agent holding the key database lock causes "database locked"
412 // timeouts during key generation.
413 ResolvedGpgconfCommand resolvedGpgconf = resolveGpgconfCommand(gpgPath);
414 QStringList killArgs = resolvedGpgconf.arguments;
415 killArgs << "--kill";
416 killArgs << "gpg-agent";
417 // Use same environment as key generation to target correct gpg-agent
418 if (Executor::executeBlocking(env, resolvedGpgconf.program, killArgs) != 0) {
419 qCWarning(lcQtPass) << "Failed to kill gpg-agent";
420 }
421
422 executeWrapper(GPG_GENKEYS, gpgPath, {"--gen-key", "--no-tty", "--batch"},
423 std::move(batch), true, true);
424}
425
426auto Pass::listKeys(QStringList keystrings, bool secret) -> QList<UserInfo> {
427 QStringList args = {"--no-tty", "--with-colons", "--with-fingerprint"};
428 args.append(secret ? "--list-secret-keys" : "--list-keys");
429
430 for (const QString &keystring : std::as_const(keystrings)) {
431 if (!keystring.isEmpty()) {
432 args.append(keystring);
433 }
434 }
435 QString p_out;
436 if (Executor::executeBlocking(m_settings.gpgExecutable, args, &p_out) != 0) {
437 return {};
438 }
439 return parseGpgColonOutput(p_out, secret);
440}
441
442auto Pass::listKeys(const QString &keystring, bool secret) -> QList<UserInfo> {
443 return listKeys(QStringList(keystring), secret);
444}
445
446namespace {
447
448auto containsAny(const QString &str, const QStringList &patterns) -> bool {
449 for (const QString &p : patterns) {
450 if (str.contains(p)) {
451 return true;
452 }
453 }
454 return false;
455}
456
457// @p patterns must already be lowercase; only @p str is lowered.
458auto containsAnyCaseInsensitive(const QString &str, const QStringList &patterns)
459 -> bool {
460 const QString lower = str.toLower();
461 for (const QString &p : patterns) {
462 if (lower.contains(p)) {
463 return true;
464 }
465 }
466 return false;
467}
468
469} // namespace
470
471auto gpgErrorMessage(const QString &err) -> QString {
472 // Machine-readable status tokens added by --status-fd 2
473 if (containsAny(err, {QStringLiteral("[GNUPG:] KEYEXPIRED"),
474 QStringLiteral("[GNUPG:] INV_RECP 5 ")}))
475 return QCoreApplication::translate(
476 "Pass", "Encryption failed: GPG key has expired. Please renew or "
477 "replace it.");
478 if (containsAny(err, {QStringLiteral("[GNUPG:] KEYREVOKED"),
479 QStringLiteral("[GNUPG:] INV_RECP 4 ")}))
480 return QCoreApplication::translate(
481 "Pass", "Encryption failed: GPG key has been revoked.");
482 if (containsAny(err, {QStringLiteral("[GNUPG:] NO_PUBKEY"),
483 QStringLiteral("[GNUPG:] INV_RECP")}))
484 return QCoreApplication::translate(
485 "Pass", "Encryption failed: recipient GPG key not found or invalid. "
486 "Check that the key ID in .gpg-id is correct and imported.");
487 if (err.contains(QStringLiteral("[GNUPG:] FAILURE")))
488 return QCoreApplication::translate(
489 "Pass", "Encryption failed. Check that your GPG key is valid.");
490
491 // Locale-dependent fallbacks
492 if (containsAnyCaseInsensitive(err, {QLatin1String("key has expired"),
493 QLatin1String("key expired")}))
494 return QCoreApplication::translate(
495 "Pass", "Encryption failed: GPG key has expired. Please renew or "
496 "replace it.");
497 if (containsAnyCaseInsensitive(err, {QLatin1String("key has been revoked"),
498 QLatin1String("revoked")}))
499 return QCoreApplication::translate(
500 "Pass", "Encryption failed: GPG key has been revoked.");
501 // pass passes no --status-fd, so this is all gpg says about a key that is
502 // present but cannot encrypt: expired, revoked, without an encryption
503 // subkey, or not trusted ("There is no assurance this key belongs to the
504 // named user"). "Not found ... imported" would send the user the wrong
505 // way; gpg's own lines, shown below this, say which it is.
506 if (containsAnyCaseInsensitive(err, {QLatin1String("unusable public key")}))
507 return QCoreApplication::translate(
508 "Pass", "Encryption failed: a recipient's GPG key cannot be used. It "
509 "may have expired or been revoked, have no encryption subkey, "
510 "or not be trusted; gpg's message below says which.");
511 if (containsAnyCaseInsensitive(err, {QLatin1String("no public key"),
512 QLatin1String("no secret key")}))
513 return QCoreApplication::translate(
514 "Pass", "Encryption failed: recipient GPG key not found or invalid. "
515 "Check that the key ID in .gpg-id is correct and imported.");
516 if (containsAnyCaseInsensitive(err, {QLatin1String("encryption failed")}))
517 return QCoreApplication::translate(
518 "Pass", "Encryption failed. Check that your GPG key is valid.");
519
520 return {};
521}
522
523void Pass::finished(int id, int exitCode, const QString &out,
524 const QString &err) {
525 auto pid = static_cast<PROCESS>(id);
526
527 // Pop the Show() this completion belongs to whether it worked or not, or
528 // the next result would be attributed to the wrong file.
529 QString shownFile;
530 if (pid == PASS_SHOW && !m_pendingShows.isEmpty()) {
531 shownFile = m_pendingShows.dequeue();
532 }
533 // Count this insert off, success or failure, before its result goes out, so
534 // insertsPending() tracks the inserts still awaiting a report. Each insert
535 // reaches here once as PASS_INSERT.
536 if (pid == PASS_INSERT && m_insertsPending > 0) {
538 }
539
540 if (exitCode != 0) {
541 handleProcessError(pid, exitCode, out, err);
542 return;
543 }
544
545 if (pid == PASS_SHOW) {
546 emit finishedShow(out, shownFile);
547 return;
548 }
549 emitProcessFinishedSignal(pid, out, err);
550}
551
552void Pass::handleProcessError(PROCESS pid, int exitCode, const QString &out,
553 const QString &err) {
554 Q_UNUSED(out);
555
556 if (pid == PASS_INSERT) {
557 const QString friendly = gpgErrorMessage(err);
558 const QString why =
559 friendly.isEmpty() ? err : formatInsertError(friendly, err);
560 const bool written = insertWritten(err);
561 emit insertFailed(why, written);
562 if (written) {
563 // The entry is in the store; a git step after it failed (a commit
564 // that could not be signed, with pass.signcommits, say). The dialog
565 // closes on this, so say it here rather than only in the output
566 // panel.
567 emit critical(tr("Saved, not committed"),
568 tr("The entry is saved in the store, but recording it "
569 "in git failed. What was reported:\n\n%1")
570 .arg(err.trimmed()));
571 }
572 emit processErrorExit(exitCode, why);
573 return;
574 }
575
576 if (pid == GPG_GENKEYS) {
577 emit generateGPGKeysFailed(err);
578 }
579 emit processErrorExit(exitCode, err);
580}
581
582auto Pass::formatInsertError(const QString &friendly, const QString &err)
583 -> QString {
584 QStringList humanLines;
585 for (const QString &line : err.split('\n')) {
586 QString cleanedLine = line;
587 cleanedLine.remove('\r');
588 if (!cleanedLine.startsWith(QLatin1String("[GNUPG:]")))
589 humanLines.append(cleanedLine);
590 }
591 const QString humanErr = humanLines.join('\n').trimmed();
592 return humanErr.isEmpty() ? friendly : friendly + "\n\n" + humanErr;
593}
594
595void Pass::emitProcessFinishedSignal(PROCESS pid, const QString &out,
596 const QString &err) {
597 // Only output that cannot contain a secret reaches the generic listeners
598 // (the output panel among them); Enums::processInfo is the one table.
599 if (pid >= 0 && pid < Enums::PROCESS_COUNT &&
600 !Enums::processInfo(pid).secret) {
601 emit finishedAnyWithPid(out, err, pid);
602 }
603
604 // The kind-specific signal. PASS_SHOW is emitted by finished(), which
605 // knows the file. Content search runs in-process (NativeGrep), not
606 // through `pass grep`, and emits finishedGrep itself.
607 using Signal = void (Pass::*)(const QString &, const QString &);
608 static const QHash<int, Signal> kSignals{
618 };
619 if (const Signal signal = kSignals.value(pid, nullptr)) {
620 (this->*signal)(out, err);
621 }
622}
623
624void Pass::setEnvVar(const QString &name, const QString &value) {
625 if (value.isEmpty())
626 env.remove(name);
627 else
628 env.insert(name, value);
629}
630
632 setEnvVar(QStringLiteral("PASSWORD_STORE_SIGNING_KEY"),
634 setEnvVar(QStringLiteral("PASSWORD_STORE_DIR"), m_settings.passStore);
635 // Every path QtPass hands git names one file or folder: an entry called
636 // "a*.gpg" must not make `git rm -f -- a*.gpg` remove every entry that
637 // matches. Also reaches the git that pass runs, which passes names the
638 // same way.
639 setEnvVar(QStringLiteral("GIT_LITERAL_PATHSPECS"), QStringLiteral("1"));
640
642 setEnvVar(QStringLiteral("PASSWORD_STORE_GENERATED_LENGTH"),
643 QString::number(passConfig.length));
644
645 setEnvVar(QStringLiteral("PASSWORD_STORE_CHARACTER_SET"),
646 effectiveCharset(passConfig));
647
648 // pass hands PASSWORD_STORE_GPG_OPTS to every gpg it runs: the user's own
649 // options (split as pass's shell splits them: spaces, tabs, newlines),
650 // then the output limit ImitatePass passes directly.
651 QStringList gpgOpts = QProcessEnvironment::systemEnvironment()
652 .value(QStringLiteral("PASSWORD_STORE_GPG_OPTS"))
653 .split(QRegularExpression(QStringLiteral("\\s+")),
654 Qt::SkipEmptyParts);
655 gpgOpts << maxOutputArgs();
656 setEnvVar(QStringLiteral("PASSWORD_STORE_GPG_OPTS"),
657 gpgOpts.join(QLatin1Char(' ')));
658
659 // As pass does: git never looks for a repository above the store's own
660 // parent. A store that is not a repository itself but lies inside one (a
661 // dotfiles repository in the home folder) would otherwise have its entries
662 // committed and pushed there by native git mode. Only in the environment
663 // of the processes QtPass runs for the store, never QtPass's own, which a
664 // file manager or terminal it opens would inherit. pass sets its own
665 // ceiling whatever this says. A store kept inside a larger repository on
666 // purpose opts out (gitUseParentRepository), and then, as without a
667 // store, whatever ceiling QtPass was started with applies.
668 // A ceiling git cannot take as one entry (the parent's path holds the
669 // list separator) is backed by GIT_DIR and GIT_WORK_TREE naming the
670 // store's own repository (Util::gitStoreVariables()); pass unsets those
671 // for itself and keeps its own ceiling.
672 setEnvVar(QStringLiteral("GIT_DIR"), QString());
673 setEnvVar(QStringLiteral("GIT_WORK_TREE"), QString());
675 setEnvVar(QStringLiteral("GIT_CEILING_DIRECTORIES"),
676 qEnvironmentVariable("GIT_CEILING_DIRECTORIES"));
677 } else {
678 for (const auto &[name, value] : Util::gitStoreVariables(
680 setEnvVar(name, value);
681 }
682 }
683
685}
686
687auto Pass::withCurrentGitPin(QProcessEnvironment base) const
688 -> QProcessEnvironment {
689 if (m_settings.gitUseParentRepository || m_settings.passStore.isEmpty()) {
690 return base;
691 }
692 base.remove(QStringLiteral("GIT_DIR"));
693 base.remove(QStringLiteral("GIT_WORK_TREE"));
694 for (const auto &[name, value] : Util::gitStoreVariables(
695 m_settings.passStore, m_settings.gitExecutable)) {
696 base.insert(name, value);
697 }
698 return base;
699}
700
701auto Pass::maxOutputArgs() -> QStringList {
702 return {QStringLiteral("--max-output"), QString::number(kMaxPlaintextBytes)};
703}
704
705namespace {
708#ifdef Q_OS_WIN
709constexpr auto kPathCase = Qt::CaseInsensitive;
710#else
711constexpr auto kPathCase = Qt::CaseSensitive;
712#endif
713
715auto isAtOrUnder(const QString &path, const QString &dir,
716 const QString &dirPrefix) -> bool {
717 return path.compare(dir, kPathCase) == 0 ||
718 path.startsWith(dirPrefix, kPathCase);
719}
720
722struct GpgIdLookup {
723 QDir start;
724 QString storeDir;
725 QString storePrefix;
726};
727
730auto gpgIdLookup(const QString &for_file, const QString &passStore)
731 -> GpgIdLookup {
732 const QString normalizedFile = QDir::fromNativeSeparators(for_file);
733 // Inside the store means at a path boundary: "/store-other/x" is not
734 // under "/store", however the prefix compares.
735 const QString storeDir =
736 QDir::cleanPath(QDir::fromNativeSeparators(passStore));
737 const QString storePrefix = storeDir.endsWith(QLatin1Char('/'))
738 ? storeDir
739 : storeDir + QLatin1Char('/');
740 const QString cleanFile = QDir::cleanPath(normalizedFile);
741 const bool insideStore = isAtOrUnder(cleanFile, storeDir, storePrefix);
742 // A relative name is one inside the store; an absolute path outside it
743 // stays what it is and the walk stops at once.
744 const QString fullPath = insideStore || QDir::isAbsolutePath(cleanFile)
745 ? cleanFile
746 : storePrefix + cleanFile;
747 // A folder (trailing separator, which cleanPath() dropped, or an existing
748 // directory) wants its own list; an entry wants its folder's.
749 const bool isFolder =
750 normalizedFile.endsWith(QLatin1Char('/')) || QFileInfo(fullPath).isDir();
751 // A folder that does not exist yet (an entry saved into a new folder, a
752 // move to one) starts the walk at its nearest existing ancestor in the
753 // store, as pass's set_gpg_recipients does: otherwise the walk ended at
754 // once and the store root's list was taken over the one in between.
755 QString start = isFolder ? fullPath : QFileInfo(fullPath).absolutePath();
756 while (!QFileInfo(start).exists() &&
757 isAtOrUnder(QDir::cleanPath(start), storeDir, storePrefix) &&
758 QDir::cleanPath(start) != storeDir) {
759 start = QFileInfo(start).absolutePath();
760 }
761 return {QDir(start), storeDir, storePrefix};
762}
763
766auto isOwnGpgId(const QFileInfo &candidate) -> bool {
767 return candidate.exists() && !candidate.isSymLink() &&
768 !candidate.isJunction();
769}
770
774auto nearestGpgId(const GpgIdLookup &lookup) -> std::optional<QString> {
775 QDir dir = lookup.start;
776 while (dir.exists() && isAtOrUnder(QDir::cleanPath(dir.absolutePath()),
777 lookup.storeDir, lookup.storePrefix)) {
778 const QFileInfo candidate(dir.absoluteFilePath(".gpg-id"));
779 if (isOwnGpgId(candidate)) {
780 return candidate.absoluteFilePath();
781 }
782 if (!dir.cdUp()) {
783 break;
784 }
785 }
786 return std::nullopt;
787}
788
789} // namespace
790
791auto Pass::refuseLinkedPath(const QString &path, LinkScope scope,
792 PathSpelling spelling) -> bool {
793 const QString full = QDir::isAbsolutePath(path)
794 ? path
795 : QDir(m_settings.passStore).filePath(path);
796 // The link check below walks only under the store as configured: a path
797 // spelled through the root's link target would get no more than the
798 // entry itself judged. No caller makes one; say so if one starts to.
799 if (spelling == PathSpelling::Configured &&
801 qCWarning(lcQtPass)
802 << "Path spelled through the store root's link target, not as the "
803 "store is configured; Pass operations expect the configured "
804 "spelling:"
805 << full;
806 }
807 if (!Util::isUnderLink(full, m_settings.passStore,
808 scope == LinkScope::EntryAndParents)) {
809 return false;
810 }
811 const QString why =
812 tr("%1 is, or lies behind, a symbolic link or junction. What that "
813 "points to is not part of the password store and is left alone.")
814 .arg(QDir::toNativeSeparators(QDir::cleanPath(full)));
816 return true;
817}
818
819auto Pass::refuseFolderOutsideStore(const QString &folder, QString *relative)
820 -> bool {
821 if (const std::optional<QString> inStore =
822 PathValidator::storeRelativePath(m_settings.passStore, folder)) {
823 if (relative != nullptr) {
824 *relative = *inStore;
825 }
826 return false;
827 }
829 tr("%1 is not inside the password store. Recipients are set for the "
830 "store or a folder in it.")
831 .arg(QDir::toNativeSeparators(QDir::cleanPath(folder))));
832 return true;
833}
834
836 if (!m_settings.useGit) {
837 return {};
838 }
839 // One git call names the path of each marker file or directory, whether or
840 // not it exists; the backend runs it in the store (ImitatePass with
841 // `-C <store>`, RealPass through `pass git`). The markers, paired with the
842 // operation each one means, in the order git prints them back.
843 const std::array<std::pair<QLatin1String, QString>, 6> markers{{
844 {QLatin1String("rebase-merge"), tr("rebase")},
845 {QLatin1String("rebase-apply"), tr("rebase")},
846 {QLatin1String("MERGE_HEAD"), tr("merge")},
847 {QLatin1String("CHERRY_PICK_HEAD"), tr("cherry-pick")},
848 {QLatin1String("REVERT_HEAD"), tr("revert")},
849 // A multi-commit cherry-pick or revert keeps its sequencer state after a
850 // conflict resolution is committed, when CHERRY_PICK_HEAD/REVERT_HEAD is
851 // already gone but `--continue` has more commits to apply. Checked last,
852 // so the specific head above names the operation when it is still there.
853 {QLatin1String("sequencer"), tr("cherry-pick or revert")},
854 }};
855 QStringList args{QStringLiteral("rev-parse")};
856 for (const auto &marker : markers) {
857 args << QStringLiteral("--git-path") << marker.first;
858 }
859 QString out;
860 if (!storeGit(args, &out)) {
861 // Git is off, the store is not a repository, or git could not be asked:
862 // nothing is refused, as the operation would run anyway.
863 return {};
864 }
865 const QStringList paths = out.split(QLatin1Char('\n'), Qt::SkipEmptyParts);
866 for (int i = 0; i < paths.size() && i < static_cast<int>(markers.size());
867 ++i) {
868 const QString path = paths.at(i).trimmed();
869 const QString full = QDir::isAbsolutePath(path)
870 ? path
871 : QDir(m_settings.passStore).filePath(path);
872 if (!QFileInfo::exists(full)) {
873 continue;
874 }
875 return tr("The store's Git repository is in the middle of a %1. Finish or "
876 "abort it with git before changing the store, or what you change "
877 "may be lost.")
878 .arg(markers.at(static_cast<size_t>(i)).second);
879 }
880 return {};
881}
882
884 if (!m_settings.useGit || !exec.hasQueued(Enums::GIT_PULL)) {
885 return false;
886 }
887 const QString why =
888 tr("The password store is being updated (a Git pull is in progress). "
889 "Wait until it has finished, then try again.");
890 // Like refuseAsNotPartOfStore(): the interface arms when an operation starts
891 // and is released by processErrorExit(); a refusal starts nothing.
893 emit critical(tr("Update in progress"), why);
894 emit processErrorExit(1, why);
895 return true;
896}
897
899 const QString why = unfinishedGitOperationReason();
900 if (why.isEmpty()) {
901 return false;
902 }
903 // Like refuseAsNotPartOfStore(): the interface arms when an operation starts
904 // and is released by finished()/processErrorExit(); a refusal starts
905 // nothing, so it emits both ends.
907 emit critical(tr("Unfinished Git operation"), why);
908 emit processErrorExit(1, why);
909 return true;
910}
911
912void Pass::refuseAsNotPartOfStore(const QString &why) {
913 // The interface arms itself when an operation starts and is released by
914 // finished or processErrorExit; a refusal starts nothing, so emits both.
916 emit critical(tr("Not part of the store"), why);
917 emit processErrorExit(1, why);
918}
919
920auto Pass::getGpgIdPath(const QString &for_file, const QString &passStore)
921 -> QString {
922 return nearestGpgId(gpgIdLookup(for_file, passStore))
923 .value_or(
924 QDir(QDir::fromNativeSeparators(passStore)).filePath(".gpg-id"));
925}
926
927auto Pass::refuseSkippedGpgId(const QString &file, bool signing,
928 const QString &except) -> bool {
929 // Without a signing key the parent's list applies, as it does for pass.
930 if (!signing) {
931 return false;
932 }
933 QString recordError;
934 const std::optional<QString> skipped =
935 skippedGpgId(file, m_settings.passStore, &recordError);
936 if (!skipped || (!except.isEmpty() &&
937 QDir::cleanPath(*skipped) == QDir::cleanPath(except))) {
938 return false;
939 }
940 // The strings keep the context they were translated in.
941 if (!recordError.isEmpty()) {
942 emit critical(QCoreApplication::translate(
943 "ImitatePass", "Cannot check the recipient list"),
944 QCoreApplication::translate(
945 "ImitatePass", "%1 Whether %2 was removed cannot be "
946 "told, so nothing was encrypted.")
947 .arg(recordError, QDir::toNativeSeparators(*skipped)));
948 return true;
949 }
950 emit critical(
951 QCoreApplication::translate("ImitatePass", "Recipient list missing"),
952 QCoreApplication::translate(
953 "ImitatePass",
954 "The signed recipient list %1 is missing or is not a regular file. "
955 "QtPass does not fall back to a parent folder's list for it: that "
956 "list was not signed for this folder. If a link is under that name, "
957 "remove it. Then open Users for that folder and save its "
958 "recipients, which writes and signs a fresh list.")
959 .arg(QDir::toNativeSeparators(*skipped)));
960 return true;
961}
962
963auto Pass::verifySignedList(const GpgIdSigner &signer, const QString &gpgIdFile,
964 QByteArray *contents, QString *why) const -> bool {
965 if (!signer.verifyFile(gpgIdFile, contents)) {
966 return false;
967 }
968 // Authentic and unmodified is not the same as current: an older signed
969 // list put back into the store is refused once this device has seen a
970 // newer one, and one signed for another folder is refused here.
971 QString reason;
972 if (GpgIdGeneration::accept(gpgIdFile, *contents, m_settings.passStore,
974 if (why != nullptr) {
975 *why = reason;
976 }
977 return false;
978 }
979 return true;
980}
981
982auto Pass::skippedGpgId(const QString &forFile, const QString &passStore,
983 QString *recordError) -> std::optional<QString> {
984 const GpgIdLookup lookup = gpgIdLookup(forFile, passStore);
985 QDir dir = lookup.start;
986 // The same walk as nearestGpgId(), stopping at the list it finds, and
987 // short of the store root: the root's own list has no parent to fall
988 // back to, and the signature check refuses it when it is missing.
989 while (dir.exists() && QDir::cleanPath(dir.absolutePath())
990 .startsWith(lookup.storePrefix, kPathCase)) {
991 const QFileInfo candidate(dir.absoluteFilePath(".gpg-id"));
992 if (isOwnGpgId(candidate)) {
993 return std::nullopt;
994 }
995 // A link under the name (dangling or not), or nothing under it where
996 // this device accepted a list before: both are the folder's list gone,
997 // not a folder that never had one.
998 if (candidate.isSymLink() || candidate.isJunction()) {
999 return candidate.absoluteFilePath();
1000 }
1001 // An unreadable record is not "never seen": whether a list was accepted
1002 // here cannot be told, so this folder is refused too (fail closed), with
1003 // the record's own reason for the caller to show.
1004 QString why;
1005 const std::optional<qint64> seen =
1006 GpgIdGeneration::remembered(candidate.absoluteFilePath(), &why);
1007 if (!seen) {
1008 if (recordError != nullptr) {
1009 *recordError =
1010 why.isEmpty()
1011 ? tr("The generation record of the recipient lists cannot "
1012 "be read.")
1013 : why;
1014 }
1015 return candidate.absoluteFilePath();
1016 }
1017 if (*seen > 0) {
1018 return candidate.absoluteFilePath();
1019 }
1020 if (!dir.cdUp()) {
1021 break;
1022 }
1023 }
1024 return std::nullopt;
1025}
1026
1027namespace {
1028
1031auto judgedRecipients(GpgIdGeneration::Verdict verdict,
1032 const QByteArray &contents, const QString &gpgIdPath,
1033 const QString &why) -> Pass::RecipientsForEditing {
1034 using RecipientsForEditing = Pass::RecipientsForEditing;
1035 RecipientsForEditing result;
1036 switch (verdict) {
1038 result.state = RecipientsForEditing::State::Verified;
1039 result.recipients = Pass::parseRecipients(contents, gpgIdPath);
1040 break;
1042 // Authentic and this folder's, only older: the recovery goes through
1043 // this dialog, so it is preselected, with the reason in view.
1044 result.state = RecipientsForEditing::State::VerifiedRollback;
1045 result.recipients = Pass::parseRecipients(contents, gpgIdPath);
1046 result.warning = why;
1047 break;
1054 // Signed, but not shown to be this folder's list, not a list to trust,
1055 // or nothing to judge it by: preselecting it would sign it in.
1056 result.state = RecipientsForEditing::State::Rejected;
1057 result.warning =
1058 Pass::tr("%1 Nothing is preselected: saving would sign whatever is "
1059 "in it. Select the recipients yourself.")
1060 .arg(why);
1061 break;
1062 }
1063 return result;
1064}
1065
1066} // namespace
1067
1068auto Pass::recipientsForEditing(const QString &dir, const QString &passStore)
1070 RecipientsForEditing result;
1071 const QString folder = dir.isEmpty() ? QString() : dir;
1072 const QString gpgIdPath = getGpgIdPath(folder, passStore);
1073 const GpgIdSigner signer(
1074 m_settings.gpgExecutable,
1076 if (!signer.enabled()) {
1077 // A list that is there but cannot be read as one (too large, not a
1078 // regular file, unreadable) is not an empty list: preselecting nothing
1079 // without a word would let OK write a new list over it, and later
1080 // re-encryptions drop whoever it named.
1081 // Read once and parsed from those bytes: a second read could find
1082 // something else under the name. A link counts as there, dangling too.
1083 const QFileInfo info(gpgIdPath);
1084 if (!info.exists() && !info.isSymLink()) {
1086 return result;
1087 }
1088 const std::optional<QByteArray> bytes =
1090 if (!bytes) {
1092 result.warning =
1093 tr("%1 cannot be read as a recipient list (a regular file of at "
1094 "most %2), so nothing is preselected. Check it before choosing "
1095 "recipients here: OK writes a new list over it.")
1096 .arg(gpgIdPath,
1097 QLocale().formattedDataSize(Util::kMaxGpgIdBytes));
1098 return result;
1099 }
1101 result.recipients = parseRecipients(*bytes, gpgIdPath);
1102 return result;
1103 }
1104 QString recordError;
1105 if (const std::optional<QString> skipped =
1106 skippedGpgId(folder, passStore, &recordError)) {
1107 // The folder's own list was removed or replaced by a link (or the record
1108 // that would tell cannot be read): the list the walk found further up
1109 // was not signed for this folder, so preselecting it would sign it in
1110 // here with one click.
1112 result.warning =
1113 recordError.isEmpty()
1114 ? tr("The recipient list %1 is missing or is not a regular file, "
1115 "so nothing is preselected: the list of a parent folder was "
1116 "not signed for this one. Select this folder's recipients "
1117 "yourself; OK writes and signs a fresh list.")
1118 .arg(*skipped)
1119 : tr("%1 Nothing is preselected: whether %2 was removed cannot "
1120 "be told.")
1121 .arg(recordError, *skipped);
1122 return result;
1123 }
1124 if (!QFileInfo::exists(gpgIdPath)) {
1125 // A folder without a list yet (new store, new profile): nothing to
1126 // verify and nothing to preselect.
1128 return result;
1129 }
1130 QByteArray contents;
1131 if (!signer.verifyFile(gpgIdPath, &contents)) {
1133 result.warning =
1134 tr("The recipient list %1 does not verify against the signing key, "
1135 "so nothing is preselected: saving would sign whatever is in it. "
1136 "Select the recipients yourself; OK writes and signs a fresh list.")
1137 .arg(gpgIdPath);
1138 return result;
1139 }
1140 QString why;
1141 const GpgIdGeneration::Verdict verdict =
1142 GpgIdGeneration::accept(gpgIdPath, contents, passStore, &why);
1143 return judgedRecipients(verdict, contents, gpgIdPath, why);
1144}
1145
1146auto Pass::getRecipientList(const QString &for_file, const QString &passStore)
1147 -> QStringList {
1148 const QString gpgIdPath = getGpgIdPath(for_file, passStore);
1149 // The root .gpg-id comes back whatever it is; a link is not a list.
1150 if (Util::isLinkedFolder(gpgIdPath)) {
1151 return {};
1152 }
1153 const std::optional<QByteArray> bytes =
1155 if (!bytes) {
1156 return {};
1157 }
1158 return parseRecipients(*bytes, gpgIdPath);
1159}
1160
1161auto Pass::parseRecipients(const QByteArray &contents,
1162 const QString &sourceName) -> QStringList {
1163 QStringList recipients;
1164 const QString text = QString::fromUtf8(contents);
1165 for (const QString &line : text.split(QLatin1Char('\n'))) {
1166 QString recipient = line.split(QLatin1Char('#')).first().trimmed();
1167 if (recipient.isEmpty()) {
1168 continue;
1169 }
1170 if (!Util::isValidKeyId(recipient)) {
1171 // Never drop a recipient silently: the list is written back verbatim
1172 // by UsersDialog, so a skipped line disappears from .gpg-id.
1173 qCWarning(lcQtPass) << "Skipping unusable recipient in" << sourceName
1174 << ":" << recipient;
1175 continue;
1176 }
1177 recipients += recipient;
1178 }
1179 return recipients;
1180}
1181
1182auto Pass::seedGpgIdFile(const QString &newDir, const QString &passStore)
1183 -> bool {
1184 const QString gpgIdFile = QDir(newDir).absoluteFilePath(".gpg-id");
1185 if (QFileInfo::exists(gpgIdFile)) {
1186 return false;
1187 }
1188 // Resolving from the file to be created finds the parent's .gpg-id whether
1189 // or not newDir has a trailing separator.
1190 const QStringList recipients = getRecipientList(gpgIdFile, passStore);
1191 if (recipients.isEmpty()) {
1192 return false;
1193 }
1194 // Only reached without a signing key, so no generation header: a plain
1195 // list stays readable for clients that take a comment for a recipient.
1196 // Staged and replacing nothing: a link planted after the exists() above
1197 // (a dangling one passes it) fails the seed instead of being written through.
1199 gpgIdFile,
1200 (recipients.join(QLatin1Char('\n')) + QLatin1Char('\n')).toUtf8(), false);
1201}
1202
1203/* Copyright (C) 2017 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
1204 */
1205
1206auto Pass::boundedRandom(quint32 bound) -> quint32 {
1207 if (bound < 2) {
1208 return 0;
1209 }
1210
1211 quint32 randval;
1212 // arc4random_uniform-style rejection against modulo bias: drop values
1213 // below 2^32 % bound, which (1 + ~bound) % bound computes in quint32.
1214 const quint32 rejectionThreshold = (1 + ~bound) % bound;
1215
1216 do {
1217 randval = QRandomGenerator::system()->generate();
1218 } while (randval < rejectionThreshold);
1219
1220 return randval % bound;
1221}
1222
1223auto Pass::generateRandomPassword(const QString &charset, unsigned int length)
1224 -> QString {
1225 if (charset.isEmpty() || length == 0U) {
1226 return {};
1227 }
1228 QString out;
1229 for (unsigned int i = 0; i < length; ++i) {
1230 out.append(charset.at(static_cast<int>(
1231 boundedRandom(static_cast<quint32>(charset.length())))));
1232 }
1233 return out;
1234}
void dequeuing(int id, bool completes)
Emitted, synchronously, when the queue's head leaves it: just before it is started,...
static auto executeBlocking(const QString &app, const QStringList &args, const QString &input=QString(), QString *process_out=nullptr, QString *process_err=nullptr) -> int
Run a command synchronously and return its exit code.
Definition executor.cpp:422
void execute(int id, const QString &app, const QStringList &args, bool readStdout, bool readStderr=true)
Queue a command without stdin input.
Definition executor.cpp:264
void drained()
Emitted once the last queued item is done and nothing runs or waits any more (isIdle()),...
void error(int id, int exitCode, const QString &output, const QString &errout)
Emitted instead of finished() when a queued process could not be started, had no executable configure...
void finished(int id, int exitCode, const QString &output, const QString &errout)
Emitted when a queued process exits normally, whatever its exit code; a non-zero code is the receiver...
void starting()
Emitted just before a queued process is started; the main window shows the busy indicator on it.
void setEnvironment(const QProcessEnvironment &env)
Set the environment passed to all child processes.
Definition executor.cpp:453
static auto parseWslCommand(const QString &app) -> std::optional< WslCommand >
Parse a configured executable as a WSL command.
Definition executor.cpp:114
static auto accept(const QString &gpgIdFile, const QByteArray &contents, const QString &storeRoot, QString *error=nullptr) -> Verdict
Decide about a verified list: its folder against where it sits, its generation against the remembered...
Verdict
The outcome of accept(). Only Accepted means the list may be used; the others say why not,...
@ Malformed
A header line malformed or duplicated.
@ Accepted
Folder, generation and record all agree; remembered.
@ RecordUnavailable
The record could not be read or written.
@ Rollback
Authentic and bound to this folder, but older than a generation this device accepted.
@ WrongFolder
Written for another folder of the store.
@ WrongStore
Written for another store (its store line names another id than this device knows the store by),...
@ Unbound
Older than a generation this device accepted and without a folder line: authentic,...
@ Conflict
The generation this device accepted last, but not the bytes: two devices saved at once (Git shows tha...
static auto remembered(const QString &gpgIdFile, QString *error=nullptr) -> std::optional< qint64 >
The highest generation this device has accepted or written for the .gpg-id at gpgIdFile....
The detached signature on a store's .gpg-id: whether the configured signing key is usable,...
Definition gpgidsigner.h:28
auto enabled() const -> bool
Whether any signing key is configured at all.
Definition gpgidsigner.h:72
auto verifyFile(const QString &gpgIdFile, QByteArray *contents) const -> bool
Read gpgIdFile and verify it against <file>.sig.
static auto keysFromSetting(const QString &passSigningKey) -> QStringList
Split the settings value into keys, upper-cased the way gpg prints fingerprints.
void startingExecuteWrapper()
Emitted each time a queued process is about to start, and by the refusals that go on to emit processE...
virtual void beforeExecute(PROCESS id)
Hook invoked just before a wrapped command is dispatched.
Definition pass.cpp:156
void GenerateGPGKeys(QString batch)
Generate GPG keys using batch script.
Definition pass.cpp:394
static QString getDefaultKeyTemplate(const QString &gpgExecutable={})
Get default key template for new GPG keys.
Definition pass.cpp:291
void finishedAnyWithPid(const QString &out, const QString &err, Enums::PROCESS pid)
A process of any kind finished successfully and its output cannot contain a secret.
void finishedInsert(const QString &out, const QString &err)
An Insert() succeeded: the entry is in the store and, with Git in use, committed.
AppSettings m_settings
Application settings snapshot injected at init() time.
Definition pass.h:117
static auto getGpgIdPath(const QString &for_file, const QString &passStore) -> QString
Get .gpg-id file path for a password file.
Definition pass.cpp:920
void handleProcessError(PROCESS pid, int exitCode, const QString &out, const QString &err)
Report a process that exited non-zero; called by finished().
Definition pass.cpp:552
auto refusePendingPull() -> bool
Refuse a store change while a Git pull is still queued or running.
Definition pass.cpp:883
void finishedGenerateGPGKeys(const QString &out, const QString &err)
GenerateGPGKeys() succeeded; the new key pair is in the keyring.
Enums::PROCESS PROCESS
Alias for Enums::PROCESS used throughout this class.
Definition pass.h:126
virtual auto generatePassword(unsigned int length, const QString &charset) -> QString
Generate random password.
Definition pass.cpp:218
void finishedShow(const QString &out, const QString &file)
Emitted when a Show() succeeds.
void statusMsg(const QString &msg, int timeout)
Progress or a non-fatal problem worth a line in the status bar ("Creating backup commit",...
QProcessEnvironment env
The environment the store's processes run in: the system environment plus what updateEnv() sets (PASS...
Definition pass.h:85
auto refuseSkippedGpgId(const QString &file, bool signing, const QString &except=QString()) -> bool
With signing on, refuse to encrypt file when the lookup passed over its folder's own list (skippedGpg...
Definition pass.cpp:927
auto unfinishedGitOperationReason() -> QString
The unfinished git operation in progress in the store, if any.
Definition pass.cpp:835
PathSpelling
How a path handed to refuseLinkedPath() is spelled.
Definition pass.h:496
@ Configured
As the store is configured or relative to it, the class's path rule. A path spelled through the targe...
Definition pass.h:502
static auto skippedGpgId(const QString &forFile, const QString &passStore, QString *recordError=nullptr) -> std::optional< QString >
The folder list getGpgIdPath() passed over on its way up to the one it returns, if it passed one over...
Definition pass.cpp:982
auto boundedRandom(quint32 bound) -> quint32
Generate random number in range.
Definition pass.cpp:1206
void insertFailed(const QString &err, bool written)
Emitted when an Insert() fails through a queued command, before the generic processErrorExit(): the p...
void emitProcessFinishedSignal(PROCESS pid, const QString &out, const QString &err)
Emit the signals for a process that exited with 0; called by finished() for everything but PASS_SHOW.
Definition pass.cpp:595
static bool gpgSupportsEd25519(const QString &gpgExecutable={})
Check if GPG supports Ed25519 encryption.
Definition pass.cpp:273
void refuseAsNotPartOfStore(const QString &why)
Refuse an operation on something that is not part of the store.
Definition pass.cpp:912
static auto maxOutputArgs() -> QStringList
The gpg options that enforce kMaxPlaintextBytes.
Definition pass.cpp:701
static auto seedGpgIdFile(const QString &newDir, const QString &passStore) -> bool
Seed a new folder's .gpg-id from the recipients it inherits.
Definition pass.cpp:1182
virtual auto storeGit(const QStringList &args, QString *out) -> bool
Run a git command in the store's repository, blocking.
Definition pass.h:560
Pass()
Construct a Pass instance.
Definition pass.cpp:61
virtual auto insertWritten(const QString &err) const -> bool
Whether the insert that just failed had put its entry in the store before a later step failed....
Definition pass.h:461
void finishedCopy(const QString &out, const QString &err)
A Copy() succeeded; the copy exists beside the original.
void finishedMove(const QString &out, const QString &err)
A Move() succeeded; the entry or folder is under its new name.
void executeWrapper(PROCESS id, const QString &app, const QStringList &args, bool readStdout=true, bool readStderr=true)
Execute external wrapper command.
Definition pass.cpp:113
static auto resolveGpgconfCommand(const QString &gpgPath) -> ResolvedGpgconfCommand
Resolve the gpgconf command to kill agents.
Definition pass.cpp:354
auto refuseLinkedPath(const QString &path, LinkScope scope=LinkScope::EntryAndParents, PathSpelling spelling=PathSpelling::Configured) -> bool
Refuse an operation on something that is, or lies behind, a symbolic link or NTFS junction inside the...
Definition pass.cpp:791
auto listKeys(QStringList keystrings, bool secret=false) -> QList< UserInfo >
List GPG keys matching patterns.
Definition pass.cpp:426
void finishedGitPull(const QString &out, const QString &err)
git pull succeeded; entries may have changed on disk.
void finishedGitPush(const QString &out, const QString &err)
git push succeeded.
auto recipientsForEditing(const QString &dir, const QString &passStore) -> RecipientsForEditing
The recipients a dialog may preselect for editing the list of dir.
Definition pass.cpp:1068
Executor exec
Internal command executor for queuing and running subprocesses.
Definition pass.h:121
void processErrorExit(int exitCode, const QString &err)
An operation ended in failure: its process exited non-zero, could not be started or crashed,...
auto verifySignedList(const GpgIdSigner &signer, const QString &gpgIdFile, QByteArray *contents, QString *why) const -> bool
Whether gpgIdFile is a signed list QtPass accepts: its signature verifies (GpgIdSigner::verifyFile())...
Definition pass.cpp:963
void init(const AppSettings &settings)
Initialize the Pass instance with a settings snapshot.
Definition pass.cpp:158
auto formatInsertError(const QString &friendly, const QString &err) -> QString
Put the translated reason for a failed insert above what gpg itself printed, leaving out the machine-...
Definition pass.cpp:582
void finishedGitInit(const QString &out, const QString &err)
git init in the store succeeded; the store is a repository now and the tree is re-read.
auto withCurrentGitPin(QProcessEnvironment base) const -> QProcessEnvironment
base with GIT_DIR and GIT_WORK_TREE as the store is now (Util::gitStoreVariables()),...
Definition pass.cpp:687
void setEnvVar(const QString &name, const QString &value)
Set or remove an environment variable.
Definition pass.cpp:624
static auto loggableArgs(const QStringList &args) -> QStringList
The arguments as they may appear in the debug log: the value of --passphrase and friends,...
Definition pass.cpp:128
void updateEnv()
Update environment for subprocesses.
Definition pass.cpp:631
void finishedInit(const QString &out, const QString &err)
An Init() succeeded: the folder's .gpg-id holds the new recipient list.
void idle()
Nothing of the store's runs or waits any more (isIdle()).
virtual void finished(int id, int exitCode, const QString &out, const QString &err)
Handle process completion.
Definition pass.cpp:523
void critical(const QString &title, const QString &message)
Something went wrong that the user has to be told about in a dialog, such as a refused path or a fail...
void generateGPGKeysFailed(const QString &err)
Emitted when GPG key generation fails, alongside the generic processErrorExit(); lets the keygen dial...
static constexpr qint64 kMaxPlaintextBytes
Most bytes of plaintext a gpg decryption may produce.
Definition pass.h:277
int m_insertsPending
Inserts that have started queueing but not yet reported; see queueInsert.
Definition pass.h:681
static auto getRecipientList(const QString &for_file, const QString &passStore) -> QStringList
Get list of recipients for a password file.
Definition pass.cpp:1146
LinkScope
What refuseLinkedPath() judges.
Definition pass.h:486
@ EntryAndParents
The entry itself and every folder above it in the store.
Definition pass.h:488
void finishedRemove(const QString &out, const QString &err)
A Remove() succeeded; the entry or folder is gone from the store.
auto refuseUnfinishedGitOperation() -> bool
Refuse to change the store while git is mid-rebase, -merge, -cherry-pick or -revert.
Definition pass.cpp:898
QQueue< QString > m_pendingShows
Files of the Show() calls still in flight, oldest first.
Definition pass.h:679
static auto parseRecipients(const QByteArray &contents, const QString &sourceName) -> QStringList
Parse the contents of a .gpg-id: one recipient per line, # starts a comment, unusable lines are logge...
Definition pass.cpp:1161
auto refuseFolderOutsideStore(const QString &folder, QString *relative=nullptr) -> bool
Refuse recipients for a folder outside the store, as pass refuses a path with .. in it.
Definition pass.cpp:819
auto generateRandomPassword(const QString &charset, unsigned int length) -> QString
Generate random password from charset.
Definition pass.cpp:1223
static auto storeRelativePath(const QString &storeRoot, const QString &candidate) -> std::optional< QString >
Where candidate lies in the store, judged as isPathInStore() judges it.
static auto isTargetSpelled(const QString &storeRoot, const QString &path) -> bool
Whether path is spelled through the target of a store root that is itself a link, rather than from th...
static auto isUnderLink(const QString &path, const QString &storeRoot, bool includeSelf=true) -> bool
Whether the way from storeRoot down to path passes through a symbolic link or NTFS junction: path its...
Definition util.cpp:765
static auto writeFileReplacing(const QString &path, const QByteArray &bytes, bool replace, QString *error=nullptr) -> bool
Write bytes as the file path through stageFileReplacing().
Definition util.cpp:1165
static constexpr qint64 kMaxGpgIdBytes
Most bytes of a .gpg-id QtPass reads.
Definition util.h:387
static auto readRegularFile(const QString &path, qint64 maxBytes) -> std::optional< QByteArray >
Read path whole, as the regular file it is (openRegularFile()), when it holds at most maxBytes.
Definition util.cpp:973
static auto gitStoreVariables(const QString &store, const QString &git={}, std::optional< uint > asUser=std::nullopt) -> QList< std::pair< QString, QString > >
The variables that keep git in store's own repository, never one above it: GIT_CEILING_DIRECTORIES (g...
Definition util.cpp:376
static auto isLinkedFolder(const QString &path) -> bool
Whether path names a symbolic link or NTFS junction, with or without a trailing separator (which woul...
Definition util.cpp:761
static auto isValidKeyId(const QString &keyId) -> bool
Check whether a .gpg-id token may be passed to gpg as a key selector. Accepts any non-empty token tha...
Definition util.cpp:664
Rollback detection for signed .gpg-id files: the generation counter and the record of what was last s...
GpgIdSigner, which signs a store's .gpg-id and verifies an existing .gpg-id.sig against the configure...
auto parseGpgColonOutput(const QString &output, bool secret) -> QList< UserInfo >
Parse a gpg colon listing into one UserInfo per key.
Parsing of gpg --with-colons key listings into UserInfo records.
@ PASS_INIT
Definition enums.h:53
@ PASS_INSERT
Definition enums.h:51
@ GIT_INIT
Definition enums.h:44
@ PASS_COPY
Definition enums.h:56
@ PASS_MOVE
Definition enums.h:55
@ GPG_GENKEYS
Definition enums.h:54
@ PASS_REMOVE
Definition enums.h:52
@ PASS_SHOW
Definition enums.h:50
@ GIT_PULL
Definition enums.h:48
@ GIT_PUSH
Definition enums.h:49
@ PASS_INIT
Definition enums.h:53
@ PASS_INSERT
Definition enums.h:51
@ GIT_INIT
Definition enums.h:44
@ PASS_COPY
Definition enums.h:56
@ PASS_MOVE
Definition enums.h:55
@ GPG_GENKEYS
Definition enums.h:54
@ PASS_REMOVE
Definition enums.h:52
@ PASS_SHOW
Definition enums.h:50
@ PROCESS_COUNT
Definition enums.h:62
@ GIT_PULL
Definition enums.h:48
@ GIT_PUSH
Definition enums.h:49
auto processInfo(PROCESS pid) -> const ProcessInfo &
What is known about pid.
auto gpgErrorMessage(const QString &err) -> QString
Maps GPG stderr (which may include –status-fd 2 tokens) to a translated user-friendly encryption erro...
Definition pass.cpp:471
Pass, the abstract password-store backend that RealPass and ImitatePass implement,...
QString gpgErrorMessage(const QString &err)
Maps GPG stderr (which may include –status-fd 2 tokens) to a translated user-friendly encryption erro...
Definition pass.cpp:471
PathValidator: is a path inside the password store, and how is it spelled relative to the store root.
Enums::ProcessInfo, the one table that names each kind of subprocess and says whether its output may ...
The "qtpass" logging category.
Plain value-object holding all flat QtPass configuration.
Definition appsettings.h:36
QString gpgExecutable
Path to the gpg/gpg2 executable.
Definition appsettings.h:66
QString passSigningKey
GPG key used to sign .gpg-id files.
Definition appsettings.h:61
QString passStore
Password-store root path.
Definition appsettings.h:60
QString gitExecutable
Path to the git executable.
Definition appsettings.h:65
QString gpgHome
GPG home directory (read-only externally).
Definition appsettings.h:69
bool gitUseParentRepository
PasswordConfiguration passwordConfiguration
Length/charset/custom chars.
What a dialog may do with the list of a folder (recipientsForEditing()).
Definition pass.h:340
QStringList recipients
The recipients to preselect; empty when Rejected.
Definition pass.h:363
State state
How the list on disk stood up.
Definition pass.h:361
QString warning
What the dialog should show, if anything.
Definition pass.h:365
@ Verified
Signature and generation and folder all accepted.
Definition pass.h:346
@ Unsigned
No signing key: the list as it is.
Definition pass.h:344
@ Rejected
Not to be used: signature invalid, written for another folder or for no folder in particular while a ...
Definition pass.h:358
Holds the password generator settings: which character set is selected, the alphabet of every set,...
int length
Length of the generated password, in characters.
Represents a command invocation for gpgconf operations.
Definition pass.h:37
QStringList arguments
Full argv list, including any wrapper/prefix args followed by the actual gpgconf operation flags (e....
Definition pass.h:47
QString program
Executable to run — may be a bare name resolved via PATH or a wrapper (e.g. wsl.exe) rather than a di...
Definition pass.h:42
Util: static helpers for the process environment, binary discovery, store paths, URL safety and the l...