18#include <QCoreApplication>
26#include <QRandomGenerator>
27#include <QRegularExpression>
47auto fallbackCharset(
const QString &input,
const QString &fallback) -> QString {
48 return input.isEmpty() ? fallback : input;
52 int sel = passConfig.selected;
55 return fallbackCharset(
56 passConfig.Characters[sel],
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(
':'));
104 QStringList parts = existing.split(
':', Qt::SkipEmptyParts);
105 for (
const QString &v : wslenvVars) {
106 if (!parts.contains(v))
109 env.insert(QStringLiteral(
"WSLENV"), parts.join(
':'));
114 const QStringList &args,
bool readStdout,
120 const QStringList &args, QString input,
121 bool readStdout,
bool readStderr) {
125 readStdout, readStderr);
131 static const QStringList secretOptions{
132 QStringLiteral(
"--passphrase"), QStringLiteral(
"--override-session-key")};
135 for (
const QString &arg : args) {
137 out << QStringLiteral(
"<redacted>");
141 if (arg.startsWith(QStringLiteral(
"otpauth://"))) {
142 out << QStringLiteral(
"<redacted>");
145 if (arg.startsWith(QStringLiteral(
"--passphrase=")) ||
146 arg.startsWith(QStringLiteral(
"--override-session-key="))) {
147 out << arg.left(arg.indexOf(u
'=') + 1) + QStringLiteral(
"<redacted>");
150 hide = secretOptions.contains(arg);
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")));
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);
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"));
186 env.insert(QStringLiteral(
"GNUPGHOME"), inheritedHome);
193 absHome.makeAbsolute();
194 if (absHome.exists()) {
195 env.insert(QStringLiteral(
"GNUPGHOME"), absHome.path());
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 "
202 .arg(absHome.path()),
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),
221 emit
critical(tr(
"Invalid password length"),
222 tr(
"Can't generate password with zero length."));
231 args.append(
"--secure");
233 args.append(
m_settings.avoidCapitals ?
"--no-capitalize" :
"--capitalize");
234 args.append(
m_settings.avoidNumbers ?
"--no-numerals" :
"--numerals");
236 args.append(
"--symbols");
238 args.append(QString::number(length));
241 static const QRegularExpression literalNewLines{
"[\\n\\r]"};
242 passwd.remove(literalNewLines);
245 qCDebug(lcQtPass) <<
"pwgen fail";
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.")
258 const QString cs = fallbackCharset(
261 if (cs.length() > 0) {
265 tr(
"No characters chosen"),
266 tr(
"Can't generate password, there are no characters to choose from "
267 "set in the configuration!"));
275 gpgExecutable.isEmpty() ? QStringLiteral(
"gpg") : gpgExecutable;
280 QRegularExpression versionRegex(R
"(gpg \(GnuPG\) (\d+)\.(\d+))");
281 QRegularExpressionMatch match = versionRegex.match(out);
282 if (!match.hasMatch()) {
285 int major = match.captured(1).toInt();
286 int minor = match.captured(2).toInt();
287 return major > 2 || (major == 2 && minor >= 1);
293 return QStringLiteral(
"%echo Generating a default key\n"
295 "Key-Curve: Ed25519\n"
296 "Subkey-Type: ECDH\n"
297 "Subkey-Curve: Curve25519\n"
299 "Name-Comment: QtPass\n"
306 return QStringLiteral(
"%echo Generating a default key\n"
310 "Name-Comment: QtPass\n"
319auto resolveWslGpgconfPath(
const QString &lastPart) -> QString {
320 qsizetype lastSep = lastPart.lastIndexOf(
'/');
322 lastSep = lastPart.lastIndexOf(
'\\');
325 return lastPart.left(lastSep + 1) +
"gpgconf";
327 return QStringLiteral(
"gpgconf");
330QString findGpgconfInGpgDir(
const QString &gpgPath) {
331 QFileInfo gpgInfo(gpgPath);
332 if (!gpgInfo.isAbsolute()) {
336 QDir dir(gpgInfo.absolutePath());
339 QFileInfo candidateExe(dir.filePath(
"gpgconf.exe"));
340 if (candidateExe.isExecutable()) {
341 return candidateExe.filePath();
345 QFileInfo candidate(dir.filePath(
"gpgconf"));
346 if (candidate.isExecutable()) {
347 return candidate.filePath();
356 if (gpgPath.trimmed().isEmpty()) {
357 return {
"gpgconf", {}};
364 if (!QFileInfo(wsl->command).fileName().startsWith(
"gpg")) {
365 return {
"gpgconf", {}};
367 const auto gpgconf = wsl->with(resolveWslGpgconfPath(wsl->command));
368 return {gpgconf.launcher, gpgconf.argv({})};
371 const QStringList parts = QProcess::splitCommand(gpgPath);
372 if (parts.isEmpty()) {
373 return {
"gpgconf", {}};
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) ==
379 return {
"gpgconf", {}};
382 if (!first.contains(
'/') && !first.contains(
'\\')) {
383 return {
"gpgconf", {}};
386 QString gpgconfPath = findGpgconfInGpgDir(first);
387 if (!gpgconfPath.isEmpty()) {
388 return {gpgconfPath, {}};
391 return {
"gpgconf", {}};
396 if (gpgPath.isEmpty()) {
401 QMetaObject::invokeMethod(
407 Qt::QueuedConnection);
414 QStringList killArgs = resolvedGpgconf.
arguments;
415 killArgs <<
"--kill";
416 killArgs <<
"gpg-agent";
419 qCWarning(lcQtPass) <<
"Failed to kill gpg-agent";
423 std::move(batch),
true,
true);
427 QStringList args = {
"--no-tty",
"--with-colons",
"--with-fingerprint"};
428 args.append(secret ?
"--list-secret-keys" :
"--list-keys");
430 for (
const QString &keystring : std::as_const(keystrings)) {
431 if (!keystring.isEmpty()) {
432 args.append(keystring);
443 return listKeys(QStringList(keystring), secret);
448auto containsAny(
const QString &str,
const QStringList &patterns) ->
bool {
449 for (
const QString &p : patterns) {
450 if (str.contains(p)) {
458auto containsAnyCaseInsensitive(
const QString &str,
const QStringList &patterns)
460 const QString lower = str.toLower();
461 for (
const QString &p : patterns) {
462 if (lower.contains(p)) {
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 "
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.");
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 "
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.");
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.");
524 const QString &err) {
525 auto pid =
static_cast<PROCESS>(id);
553 const QString &err) {
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()));
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);
591 const QString humanErr = humanLines.join(
'\n').trimmed();
592 return humanErr.isEmpty() ? friendly : friendly +
"\n\n" + humanErr;
596 const QString &err) {
607 using Signal = void (
Pass::*)(
const QString &,
const QString &);
608 static const QHash<int, Signal> kSignals{
619 if (
const Signal signal = kSignals.value(pid,
nullptr)) {
620 (this->*signal)(out, err);
628 env.insert(name, value);
632 setEnvVar(QStringLiteral(
"PASSWORD_STORE_SIGNING_KEY"),
639 setEnvVar(QStringLiteral(
"GIT_LITERAL_PATHSPECS"), QStringLiteral(
"1"));
642 setEnvVar(QStringLiteral(
"PASSWORD_STORE_GENERATED_LENGTH"),
643 QString::number(passConfig.
length));
645 setEnvVar(QStringLiteral(
"PASSWORD_STORE_CHARACTER_SET"),
646 effectiveCharset(passConfig));
651 QStringList gpgOpts = QProcessEnvironment::systemEnvironment()
652 .value(QStringLiteral(
"PASSWORD_STORE_GPG_OPTS"))
653 .split(QRegularExpression(QStringLiteral(
"\\s+")),
656 setEnvVar(QStringLiteral(
"PASSWORD_STORE_GPG_OPTS"),
657 gpgOpts.join(QLatin1Char(
' ')));
672 setEnvVar(QStringLiteral(
"GIT_DIR"), QString());
673 setEnvVar(QStringLiteral(
"GIT_WORK_TREE"), QString());
675 setEnvVar(QStringLiteral(
"GIT_CEILING_DIRECTORIES"),
676 qEnvironmentVariable(
"GIT_CEILING_DIRECTORIES"));
688 -> QProcessEnvironment {
692 base.remove(QStringLiteral(
"GIT_DIR"));
693 base.remove(QStringLiteral(
"GIT_WORK_TREE"));
696 base.insert(name, value);
709constexpr auto kPathCase = Qt::CaseInsensitive;
711constexpr auto kPathCase = Qt::CaseSensitive;
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);
730auto gpgIdLookup(
const QString &for_file,
const QString &passStore)
732 const QString normalizedFile = QDir::fromNativeSeparators(for_file);
735 const QString storeDir =
736 QDir::cleanPath(QDir::fromNativeSeparators(passStore));
737 const QString storePrefix = storeDir.endsWith(QLatin1Char(
'/'))
739 : storeDir + QLatin1Char(
'/');
740 const QString cleanFile = QDir::cleanPath(normalizedFile);
741 const bool insideStore = isAtOrUnder(cleanFile, storeDir, storePrefix);
744 const QString fullPath = insideStore || QDir::isAbsolutePath(cleanFile)
746 : storePrefix + cleanFile;
749 const bool isFolder =
750 normalizedFile.endsWith(QLatin1Char(
'/')) || QFileInfo(fullPath).isDir();
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();
761 return {QDir(start), storeDir, storePrefix};
766auto isOwnGpgId(
const QFileInfo &candidate) ->
bool {
767 return candidate.exists() && !candidate.isSymLink() &&
768 !candidate.isJunction();
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();
793 const QString full = QDir::isAbsolutePath(path)
802 <<
"Path spelled through the store root's link target, not as the "
803 "store is configured; Pass operations expect the configured "
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)));
821 if (
const std::optional<QString> inStore =
823 if (relative !=
nullptr) {
824 *relative = *inStore;
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))));
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")},
853 {QLatin1String(
"sequencer"), tr(
"cherry-pick or revert")},
855 QStringList args{QStringLiteral(
"rev-parse")};
856 for (
const auto &marker : markers) {
857 args << QStringLiteral(
"--git-path") << marker.first;
865 const QStringList paths = out.split(QLatin1Char(
'\n'), Qt::SkipEmptyParts);
866 for (
int i = 0; i < paths.size() && i <
static_cast<int>(markers.size());
868 const QString path = paths.at(i).trimmed();
869 const QString full = QDir::isAbsolutePath(path)
872 if (!QFileInfo::exists(full)) {
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 "
878 .arg(markers.at(
static_cast<size_t>(i)).second);
888 tr(
"The password store is being updated (a Git pull is in progress). "
889 "Wait until it has finished, then try again.");
893 emit
critical(tr(
"Update in progress"), why);
907 emit
critical(tr(
"Unfinished Git operation"), why);
916 emit
critical(tr(
"Not part of the store"), why);
922 return nearestGpgId(gpgIdLookup(for_file, passStore))
924 QDir(QDir::fromNativeSeparators(passStore)).filePath(
".gpg-id"));
928 const QString &except) ->
bool {
934 const std::optional<QString> skipped =
936 if (!skipped || (!except.isEmpty() &&
937 QDir::cleanPath(*skipped) == QDir::cleanPath(except))) {
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)));
951 QCoreApplication::translate(
"ImitatePass",
"Recipient list missing"),
952 QCoreApplication::translate(
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)));
964 QByteArray *contents, QString *why)
const ->
bool {
965 if (!signer.verifyFile(gpgIdFile, contents)) {
974 if (why !=
nullptr) {
983 QString *recordError) -> std::optional<QString> {
984 const GpgIdLookup lookup = gpgIdLookup(forFile, passStore);
985 QDir dir = lookup.start;
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)) {
998 if (candidate.isSymLink() || candidate.isJunction()) {
999 return candidate.absoluteFilePath();
1005 const std::optional<qint64> seen =
1008 if (recordError !=
nullptr) {
1011 ? tr(
"The generation record of the recipient lists cannot "
1015 return candidate.absoluteFilePath();
1018 return candidate.absoluteFilePath();
1024 return std::nullopt;
1032 const QByteArray &contents,
const QString &gpgIdPath,
1035 RecipientsForEditing result;
1038 result.state = RecipientsForEditing::State::Verified;
1044 result.state = RecipientsForEditing::State::VerifiedRollback;
1046 result.warning = why;
1056 result.state = RecipientsForEditing::State::Rejected;
1058 Pass::tr(
"%1 Nothing is preselected: saving would sign whatever is "
1059 "in it. Select the recipients yourself.")
1071 const QString folder = dir.isEmpty() ? QString() : dir;
1072 const QString gpgIdPath =
getGpgIdPath(folder, passStore);
1083 const QFileInfo info(gpgIdPath);
1084 if (!info.exists() && !info.isSymLink()) {
1088 const std::optional<QByteArray> bytes =
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.")
1104 QString recordError;
1105 if (
const std::optional<QString> skipped =
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.")
1119 : tr(
"%1 Nothing is preselected: whether %2 was removed cannot "
1121 .arg(recordError, *skipped);
1124 if (!QFileInfo::exists(gpgIdPath)) {
1130 QByteArray contents;
1131 if (!signer.
verifyFile(gpgIdPath, &contents)) {
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.")
1143 return judgedRecipients(verdict, contents, gpgIdPath, why);
1148 const QString gpgIdPath =
getGpgIdPath(for_file, passStore);
1153 const std::optional<QByteArray> bytes =
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()) {
1173 qCWarning(lcQtPass) <<
"Skipping unusable recipient in" << sourceName
1174 <<
":" << recipient;
1177 recipients += recipient;
1184 const QString gpgIdFile = QDir(newDir).absoluteFilePath(
".gpg-id");
1185 if (QFileInfo::exists(gpgIdFile)) {
1191 if (recipients.isEmpty()) {
1200 (recipients.join(QLatin1Char(
'\n')) + QLatin1Char(
'\n')).toUtf8(),
false);
1214 const quint32 rejectionThreshold = (1 + ~bound) % bound;
1217 randval = QRandomGenerator::system()->generate();
1218 }
while (randval < rejectionThreshold);
1220 return randval % bound;
1225 if (charset.isEmpty() || length == 0U) {
1229 for (
unsigned int i = 0; i < length; ++i) {
1230 out.append(charset.at(
static_cast<int>(
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.
void execute(int id, const QString &app, const QStringList &args, bool readStdout, bool readStderr=true)
Queue a command without stdin input.
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.
static auto parseWslCommand(const QString &app) -> std::optional< WslCommand >
Parse a configured executable as a WSL command.
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,...
auto enabled() const -> bool
Whether any signing key is configured at all.
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.
void GenerateGPGKeys(QString batch)
Generate GPG keys using batch script.
static QString getDefaultKeyTemplate(const QString &gpgExecutable={})
Get default key template for new GPG keys.
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.
static auto getGpgIdPath(const QString &for_file, const QString &passStore) -> QString
Get .gpg-id file path for a password file.
void handleProcessError(PROCESS pid, int exitCode, const QString &out, const QString &err)
Report a process that exited non-zero; called by finished().
auto refusePendingPull() -> bool
Refuse a store change while a Git pull is still queued or running.
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.
virtual auto generatePassword(unsigned int length, const QString &charset) -> QString
Generate random password.
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...
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...
auto unfinishedGitOperationReason() -> QString
The unfinished git operation in progress in the store, if any.
PathSpelling
How a path handed to refuseLinkedPath() is spelled.
@ Configured
As the store is configured or relative to it, the class's path rule. A path spelled through the targe...
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...
auto boundedRandom(quint32 bound) -> quint32
Generate random number in range.
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.
static bool gpgSupportsEd25519(const QString &gpgExecutable={})
Check if GPG supports Ed25519 encryption.
void refuseAsNotPartOfStore(const QString &why)
Refuse an operation on something that is not part of the store.
static auto maxOutputArgs() -> QStringList
The gpg options that enforce kMaxPlaintextBytes.
static auto seedGpgIdFile(const QString &newDir, const QString &passStore) -> bool
Seed a new folder's .gpg-id from the recipients it inherits.
virtual auto storeGit(const QStringList &args, QString *out) -> bool
Run a git command in the store's repository, blocking.
Pass()
Construct a Pass instance.
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....
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.
static auto resolveGpgconfCommand(const QString &gpgPath) -> ResolvedGpgconfCommand
Resolve the gpgconf command to kill agents.
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...
auto listKeys(QStringList keystrings, bool secret=false) -> QList< UserInfo >
List GPG keys matching patterns.
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.
Executor exec
Internal command executor for queuing and running subprocesses.
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())...
void init(const AppSettings &settings)
Initialize the Pass instance with a settings snapshot.
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-...
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()),...
void setEnvVar(const QString &name, const QString &value)
Set or remove an environment variable.
static auto loggableArgs(const QStringList &args) -> QStringList
The arguments as they may appear in the debug log: the value of --passphrase and friends,...
void updateEnv()
Update environment for subprocesses.
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.
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.
int m_insertsPending
Inserts that have started queueing but not yet reported; see queueInsert.
static auto getRecipientList(const QString &for_file, const QString &passStore) -> QStringList
Get list of recipients for a password file.
LinkScope
What refuseLinkedPath() judges.
@ EntryAndParents
The entry itself and every folder above it in the store.
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.
QQueue< QString > m_pendingShows
Files of the Show() calls still in flight, oldest first.
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...
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.
auto generateRandomPassword(const QString &charset, unsigned int length) -> QString
Generate random password from charset.
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...
static auto writeFileReplacing(const QString &path, const QByteArray &bytes, bool replace, QString *error=nullptr) -> bool
Write bytes as the file path through stageFileReplacing().
static constexpr qint64 kMaxGpgIdBytes
Most bytes of a .gpg-id QtPass reads.
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.
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...
static auto isLinkedFolder(const QString &path) -> bool
Whether path names a symbolic link or NTFS junction, with or without a trailing separator (which woul...
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...
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.
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...
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...
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.
QString gpgExecutable
Path to the gpg/gpg2 executable.
QString passSigningKey
GPG key used to sign .gpg-id files.
QString passStore
Password-store root path.
QString gitExecutable
Path to the git executable.
QString gpgHome
GPG home directory (read-only externally).
bool gitUseParentRepository
PasswordConfiguration passwordConfiguration
Length/charset/custom chars.
What a dialog may do with the list of a folder (recipientsForEditing()).
QStringList recipients
The recipients to preselect; empty when Rejected.
State state
How the list on disk stood up.
QString warning
What the dialog should show, if anything.
@ Verified
Signature and generation and folder all accepted.
@ Unsigned
No signing key: the list as it is.
@ Rejected
Not to be used: signature invalid, written for another folder or for no folder in particular while a ...
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.
QStringList arguments
Full argv list, including any wrapper/prefix args followed by the actual gpgconf operation flags (e....
QString program
Executable to run — may be a bare name resolved via PATH or a wrapper (e.g. wsl.exe) rather than a di...
Util: static helpers for the process environment, binary discovery, store paths, URL safety and the l...