QtPass 1.6.0
Multi-platform GUI for pass, the standard unix password manager.
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
3#include "pass.h"
4#include "gpgkeystate.h"
5#include "helpers.h"
6#include "qtpasssettings.h"
7#include "util.h"
8#include <QDir>
9#include <QFileInfo>
10#include <QProcess>
11#include <QRandomGenerator>
12#include <QRegularExpression>
13#include <utility>
14
15#ifdef QT_DEBUG
16#include "debughelper.h"
17#endif
18
19using Enums::GIT_INIT;
20using Enums::GIT_PULL;
21using Enums::GIT_PUSH;
30
34Pass::Pass() : wrapperRunning(false), env(QProcess::systemEnvironment()) {
35 connect(&exec,
36 static_cast<void (Executor::*)(int, int, const QString &,
37 const QString &)>(&Executor::finished),
38 this, &Pass::finished);
39
40 // This was previously using direct QProcess signals.
41 // The code now uses Executor instead of raw QProcess for better control.
42 // connect(&process, SIGNAL(error(QProcess::ProcessError)), this,
43 // SIGNAL(error(QProcess::ProcessError)));
44
46 env.append("WSLENV=PASSWORD_STORE_DIR/p");
47}
48
57void Pass::executeWrapper(PROCESS id, const QString &app,
58 const QStringList &args, bool readStdout,
59 bool readStderr) {
60 executeWrapper(id, app, args, QString(), readStdout, readStderr);
61}
62
63void Pass::executeWrapper(PROCESS id, const QString &app,
64 const QStringList &args, QString input,
65 bool readStdout, bool readStderr) {
66#ifdef QT_DEBUG
67 dbg() << app << args;
68#endif
69 exec.execute(id, QtPassSettings::getPassStore(), app, args, std::move(input),
70 readStdout, readStderr);
71}
72
76void Pass::init() {
77#ifdef __APPLE__
78 // If it exists, add the gpgtools to PATH
79 if (QFile("/usr/local/MacGPG2/bin").exists())
80 env.replaceInStrings("PATH=", "PATH=/usr/local/MacGPG2/bin:");
81 // Add missing /usr/local/bin
82 if (env.filter("/usr/local/bin").isEmpty())
83 env.replaceInStrings("PATH=", "PATH=/usr/local/bin:");
84#endif
85
86 if (!QtPassSettings::getGpgHome().isEmpty()) {
87 QDir absHome(QtPassSettings::getGpgHome());
88 absHome.makeAbsolute();
89 env << "GNUPGHOME=" + absHome.path();
90 }
91}
92
100auto Pass::generatePassword(unsigned int length, const QString &charset)
101 -> QString {
102 QString passwd;
104 // --secure goes first as it overrides --no-* otherwise
105 QStringList args;
106 args.append("-1");
108 args.append("--secure");
109 }
110 args.append(QtPassSettings::isAvoidCapitals() ? "--no-capitalize"
111 : "--capitalize");
112 args.append(QtPassSettings::isAvoidNumbers() ? "--no-numerals"
113 : "--numerals");
115 args.append("--symbols");
116 }
117 args.append(QString::number(length));
118 // executeBlocking returns 0 on success, non-zero on failure
120 &passwd) == 0) {
121 static const QRegularExpression literalNewLines{"[\\n\\r]"};
122 passwd.remove(literalNewLines);
123 } else {
124 passwd.clear();
125#ifdef QT_DEBUG
126 qDebug() << __FILE__ << ":" << __LINE__ << "\t"
127 << "pwgen fail";
128#endif
129 // Error is already handled by clearing passwd; no need for critical
130 // signal here
131 }
132 } else {
133 // Validate charset - if CUSTOM is selected but chars are empty,
134 // fall back to ALLCHARS to prevent weak passwords (issue #780)
135 QString effectiveCharset = charset;
136 if (effectiveCharset.isEmpty()) {
139 }
140 if (effectiveCharset.length() > 0) {
141 passwd = generateRandomPassword(effectiveCharset, length);
142 } else {
143 emit critical(
144 tr("No characters chosen"),
145 tr("Can't generate password, there are no characters to choose from "
146 "set in the configuration!"));
147 }
148 }
149 return passwd;
150}
151
158 QString out, err;
160 {"--version"}, &out, &err) != 0) {
161 return false;
162 }
163 QRegularExpression versionRegex(R"(gpg \‍(GnuPG\) (\d+)\.(\d+))");
164 QRegularExpressionMatch match = versionRegex.match(out);
165 if (!match.hasMatch()) {
166 return false;
167 }
168 int major = match.captured(1).toInt();
169 int minor = match.captured(2).toInt();
170 return major > 2 || (major == 2 && minor >= 1);
171}
172
179 if (gpgSupportsEd25519()) {
180 return QStringLiteral("%echo Generating a default key\n"
181 "Key-Type: EdDSA\n"
182 "Key-Curve: Ed25519\n"
183 "Subkey-Type: ECDH\n"
184 "Subkey-Curve: Curve25519\n"
185 "Name-Real: \n"
186 "Name-Comment: QtPass\n"
187 "Name-Email: \n"
188 "Expire-Date: 0\n"
189 "%no-protection\n"
190 "%commit\n"
191 "%echo done");
192 }
193 return QStringLiteral("%echo Generating a default key\n"
194 "Key-Type: RSA\n"
195 "Subkey-Type: RSA\n"
196 "Name-Real: \n"
197 "Name-Comment: QtPass\n"
198 "Name-Email: \n"
199 "Expire-Date: 0\n"
200 "%no-protection\n"
201 "%commit\n"
202 "%echo done");
203}
204
205namespace {
206auto resolveWslGpgconfPath(const QString &lastPart) -> QString {
207 int lastSep = lastPart.lastIndexOf('/');
208 if (lastSep < 0) {
209 lastSep = lastPart.lastIndexOf('\\');
210 }
211 if (lastSep >= 0) {
212 return lastPart.left(lastSep + 1) + "gpgconf";
213 }
214 return QStringLiteral("gpgconf");
215}
216
230QString findGpgconfInGpgDir(const QString &gpgPath) {
231 QFileInfo gpgInfo(gpgPath);
232 if (!gpgInfo.isAbsolute()) {
233 return QString();
234 }
235
236 QDir dir(gpgInfo.absolutePath());
237
238#ifdef Q_OS_WIN
239 QFileInfo candidateExe(dir.filePath("gpgconf.exe"));
240 if (candidateExe.isExecutable()) {
241 return candidateExe.filePath();
242 }
243#endif
244
245 QFileInfo candidate(dir.filePath("gpgconf"));
246 if (candidate.isExecutable()) {
247 return candidate.filePath();
248 }
249 return QString();
250}
251
252#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
264QStringList splitCommandCompat(const QString &command) {
265 QStringList result;
266 QString current;
267 bool inSingleQuote = false;
268 bool inDoubleQuote = false;
269 bool escaping = false;
270 for (QChar ch : command) {
271 if (escaping) {
272 current.append(ch);
273 escaping = false;
274 continue;
275 }
276 if (ch == '\\') {
277 escaping = true;
278 continue;
279 }
280 if (ch == '\'' && !inDoubleQuote) {
281 inSingleQuote = !inSingleQuote;
282 continue;
283 }
284 if (ch == '"' && !inSingleQuote) {
285 inDoubleQuote = !inDoubleQuote;
286 continue;
287 }
288 if (ch.isSpace() && !inSingleQuote && !inDoubleQuote) {
289 if (!current.isEmpty()) {
290 result.append(current);
291 current.clear();
292 }
293 continue;
294 }
295 current.append(ch);
296 }
297 if (escaping) {
298 current.append('\\');
299 }
300 if (!current.isEmpty()) {
301 result.append(current);
302 }
303 return result;
304}
305#endif
306
307} // namespace
308
322auto Pass::resolveGpgconfCommand(const QString &gpgPath)
324 if (gpgPath.trimmed().isEmpty()) {
325 return {"gpgconf", {}};
326 }
327
328#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
329 QStringList parts = QProcess::splitCommand(gpgPath);
330#else
331 QStringList parts = splitCommandCompat(gpgPath);
332#endif
333
334 if (parts.isEmpty()) {
335 return {"gpgconf", {}};
336 }
337
338 const QString first = parts.first();
339 if (first == "wsl" || first == "wsl.exe") {
340 if (parts.size() >= 2 && parts.at(1).startsWith("sh")) {
341 return {"gpgconf", {}};
342 }
343 if (parts.size() >= 2 &&
344 QFileInfo(parts.last()).fileName().startsWith("gpg")) {
345 QString wslGpgconf = resolveWslGpgconfPath(parts.last());
346 parts.removeLast();
347 parts.append(wslGpgconf);
348 return {parts.first(), parts.mid(1)};
349 }
350 return {"gpgconf", {}};
351 }
352
353 if (!first.contains('/') && !first.contains('\\')) {
354 return {"gpgconf", {}};
355 }
356
357 QString gpgconfPath = findGpgconfInGpgDir(gpgPath);
358 if (!gpgconfPath.isEmpty()) {
359 return {gpgconfPath, {}};
360 }
361
362 return {"gpgconf", {}};
363}
364
369void Pass::GenerateGPGKeys(QString batch) {
370 // Kill any stale GPG agents that might be holding locks on the key database
371 // This helps avoid "database locked" timeouts during key generation
372 QString gpgPath = QtPassSettings::getGpgExecutable();
373 if (!gpgPath.isEmpty()) {
375 QStringList killArgs = gpgconf.arguments;
376 killArgs << "--kill";
377 killArgs << "gpg-agent";
378 // Use same environment as key generation to target correct gpg-agent
379 Executor::executeBlocking(env, gpgconf.program, killArgs);
380 }
381
382 executeWrapper(GPG_GENKEYS, gpgPath, {"--gen-key", "--no-tty", "--batch"},
383 std::move(batch));
384}
385
392auto Pass::listKeys(QStringList keystrings, bool secret) -> QList<UserInfo> {
393 QStringList args = {"--no-tty", "--with-colons", "--with-fingerprint"};
394 args.append(secret ? "--list-secret-keys" : "--list-keys");
395
396 for (const QString &keystring : AS_CONST(keystrings)) {
397 if (!keystring.isEmpty()) {
398 args.append(keystring);
399 }
400 }
401 QString p_out;
403 &p_out) != 0) {
404 return QList<UserInfo>();
405 }
406 return parseGpgColonOutput(p_out, secret);
407}
408
415auto Pass::listKeys(const QString &keystring, bool secret) -> QList<UserInfo> {
416 return listKeys(QStringList(keystring), secret);
417}
418
429void Pass::finished(int id, int exitCode, const QString &out,
430 const QString &err) {
431 auto pid = static_cast<PROCESS>(id);
432 if (exitCode != 0) {
433 emit processErrorExit(exitCode, err);
434 return;
435 }
436 switch (pid) {
437 case GIT_INIT:
438 emit finishedGitInit(out, err);
439 break;
440 case GIT_PULL:
441 emit finishedGitPull(out, err);
442 break;
443 case GIT_PUSH:
444 emit finishedGitPush(out, err);
445 break;
446 case PASS_SHOW:
447 emit finishedShow(out);
448 break;
450 emit finishedOtpGenerate(out);
451 break;
452 case PASS_INSERT:
453 emit finishedInsert(out, err);
454 break;
455 case PASS_REMOVE:
456 emit finishedRemove(out, err);
457 break;
458 case PASS_INIT:
459 emit finishedInit(out, err);
460 break;
461 case PASS_MOVE:
462 emit finishedMove(out, err);
463 break;
464 case PASS_COPY:
465 emit finishedCopy(out, err);
466 break;
467 case GPG_GENKEYS:
468 emit finishedGenerateGPGKeys(out, err);
469 break;
470 default:
471#ifdef QT_DEBUG
472 dbg() << "Unhandled process type" << pid;
473#endif
474 break;
475 }
476}
477
483 // put PASSWORD_STORE_SIGNING_KEY in env
484 QStringList envSigningKey = env.filter("PASSWORD_STORE_SIGNING_KEY=");
485 QString currentSigningKey = QtPassSettings::getPassSigningKey();
486 if (envSigningKey.isEmpty()) {
487 if (!currentSigningKey.isEmpty()) {
488 // dbg()<< "Added
489 // PASSWORD_STORE_SIGNING_KEY with" + currentSigningKey;
490 env.append("PASSWORD_STORE_SIGNING_KEY=" + currentSigningKey);
491 }
492 } else {
493 if (currentSigningKey.isEmpty()) {
494 env.removeAll(envSigningKey.first());
495 } else {
496 // dbg()<< "Update
497 // PASSWORD_STORE_SIGNING_KEY with " + currentSigningKey;
498 env.replaceInStrings(envSigningKey.first(),
499 "PASSWORD_STORE_SIGNING_KEY=" + currentSigningKey);
500 }
501 }
502 // put PASSWORD_STORE_DIR in env
503 QStringList store = env.filter("PASSWORD_STORE_DIR=");
504 if (store.isEmpty()) {
505 env.append("PASSWORD_STORE_DIR=" + QtPassSettings::getPassStore());
506 } else {
507 // dbg()<< "Update
508 // PASSWORD_STORE_DIR with " + passStore;
509 env.replaceInStrings(store.first(), "PASSWORD_STORE_DIR=" +
511 }
512 exec.setEnvironment(env);
513}
514
520auto Pass::getGpgIdPath(const QString &for_file) -> QString {
521 QString passStore =
522 QDir::fromNativeSeparators(QtPassSettings::getPassStore());
523 QString normalizedFile = QDir::fromNativeSeparators(for_file);
524 QString fullPath = normalizedFile.startsWith(passStore)
525 ? normalizedFile
526 : passStore + "/" + normalizedFile;
527 QDir gpgIdDir(QFileInfo(fullPath).absoluteDir());
528 bool found = false;
529 while (gpgIdDir.exists() && gpgIdDir.absolutePath().startsWith(passStore)) {
530 if (QFile(gpgIdDir.absoluteFilePath(".gpg-id")).exists()) {
531 found = true;
532 break;
533 }
534 if (!gpgIdDir.cdUp()) {
535 break;
536 }
537 }
538 QString gpgIdPath(
539 found ? gpgIdDir.absoluteFilePath(".gpg-id")
540 : QDir(QtPassSettings::getPassStore()).filePath(".gpg-id"));
541
542 return gpgIdPath;
543}
544
550auto Pass::getRecipientList(const QString &for_file) -> QStringList {
551 QFile gpgId(getGpgIdPath(for_file));
552 if (!gpgId.open(QIODevice::ReadOnly | QIODevice::Text)) {
553 return {};
554 }
555 QStringList recipients;
556 while (!gpgId.atEnd()) {
557 QString recipient(gpgId.readLine());
558 recipient = recipient.split("#")[0].trimmed();
559 if (!recipient.isEmpty() && Util::isValidKeyId(recipient)) {
560 recipients += recipient;
561 }
562 }
563 return recipients;
564}
565
573auto Pass::getRecipientString(const QString &for_file, const QString &separator,
574 int *count) -> QStringList {
575 Q_UNUSED(separator)
576 QStringList recipients = Pass::getRecipientList(for_file);
577 if (count) {
578 *count = recipients.size();
579 }
580 return recipients;
581}
582
583/* Copyright (C) 2017 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
584 */
585
591auto Pass::boundedRandom(quint32 bound) -> quint32 {
592 if (bound < 2) {
593 return 0;
594 }
595
596 quint32 randval;
597 // Rejection-sampling threshold to avoid modulo bias:
598 // In quint32 arithmetic, (1 + ~bound) wraps to (2^32 - bound), so
599 // (1 + ~bound) % bound == 2^32 % bound.
600 // Values randval < max_mod_bound are rejected; accepted values produce a
601 // uniform distribution when reduced with (randval % bound).
602 const quint32 max_mod_bound = (1 + ~bound) % bound;
603
604 do {
605 randval = QRandomGenerator::system()->generate();
606 } while (randval < max_mod_bound);
607
608 return randval % bound;
609}
610
617auto Pass::generateRandomPassword(const QString &charset, unsigned int length)
618 -> QString {
619 if (charset.isEmpty() || length == 0U) {
620 return {};
621 }
622 QString out;
623 for (unsigned int i = 0; i < length; ++i) {
624 out.append(charset.at(static_cast<int>(
625 boundedRandom(static_cast<quint32>(charset.length())))));
626 }
627 return out;
628}
id Identifier provided by the caller for this queued request.
Definition executor.h:76
static auto executeBlocking(QString app, const QStringList &args, const QString &input=QString(), QString *process_out=nullptr, QString *process_err=nullptr) -> int
Executor::executeBlocking blocking version of the executor, takes input and presents it as stdin.
Definition executor.cpp:223
void starting()
starting signal that is emited when process starts
void init()
Initialize the Pass instance.
Definition pass.cpp:76
void startingExecuteWrapper()
Emitted before executing a command.
void GenerateGPGKeys(QString batch)
Generate GPG keys using batch script.
Definition pass.cpp:369
void critical(const QString &, const QString &)
Emit critical error.
void finishedCopy(const QString &, const QString &)
Emitted when copy finishes.
void finishedShow(const QString &)
Emitted when show finishes.
void finishedRemove(const QString &, const QString &)
Emitted when remove finishes.
Enums::PROCESS PROCESS
Definition pass.h:44
virtual auto generatePassword(unsigned int length, const QString &charset) -> QString
Generate random password.
Definition pass.cpp:100
void finishedMove(const QString &, const QString &)
Emitted when move finishes.
static bool gpgSupportsEd25519()
Check if GPG supports Ed25519 encryption.
Definition pass.cpp:157
auto boundedRandom(quint32 bound) -> quint32
Generate random number in range.
Definition pass.cpp:591
void finishedGitInit(const QString &, const QString &)
Emitted when Git init finishes.
Pass()
Construct a Pass instance.
Definition pass.cpp:34
void executeWrapper(PROCESS id, const QString &app, const QStringList &args, bool readStdout=true, bool readStderr=true)
Execute external wrapper command.
Definition pass.cpp:57
static auto getRecipientList(const QString &for_file) -> QStringList
Get list of recipients for a password file.
Definition pass.cpp:550
static auto resolveGpgconfCommand(const QString &gpgPath) -> ResolvedGpgconfCommand
Resolve the gpgconf command to kill agents.
Definition pass.cpp:322
auto listKeys(QStringList keystrings, bool secret=false) -> QList< UserInfo >
List GPG keys matching patterns.
Definition pass.cpp:392
void finishedInsert(const QString &, const QString &)
Emitted when insert finishes.
void finishedInit(const QString &, const QString &)
Emitted when init finishes.
static auto getGpgIdPath(const QString &for_file) -> QString
Get .gpg-id file path for a password file.
Definition pass.cpp:520
void finishedOtpGenerate(const QString &)
Emitted when OTP generation finishes.
Executor exec
Definition pass.h:42
void processErrorExit(int exitCode, const QString &err)
Emitted on process error exit.
void finishedGitPull(const QString &, const QString &)
Emitted when Git pull finishes.
void finishedGitPush(const QString &, const QString &)
Emitted when Git push finishes.
void updateEnv()
Update environment for subprocesses.
Definition pass.cpp:482
virtual void finished(int id, int exitCode, const QString &out, const QString &err)
Handle process completion.
Definition pass.cpp:429
static auto getRecipientString(const QString &for_file, const QString &separator=" ", int *count=nullptr) -> QStringList
Get recipients as string.
Definition pass.cpp:573
static QString getDefaultKeyTemplate()
Get default key template for new GPG keys.
Definition pass.cpp:178
void finishedGenerateGPGKeys(const QString &, const QString &)
Emitted when GPG key generation finishes.
auto generateRandomPassword(const QString &charset, unsigned int length) -> QString
Generate random password from charset.
Definition pass.cpp:617
static auto getPwgenExecutable(const QString &defaultValue=QVariant().toString()) -> QString
Get pwgen executable path.
static auto getPasswordConfiguration() -> PasswordConfiguration
Get complete password generation configuration.
static auto getGpgHome(const QString &defaultValue=QVariant().toString()) -> QString
Get GPG home directory.
static auto getPassStore(const QString &defaultValue=QVariant().toString()) -> QString
Get password store directory path.
static auto isAvoidCapitals(const bool &defaultValue=QVariant().toBool()) -> bool
Check whether uppercase characters should be avoided.
static auto getGpgExecutable(const QString &defaultValue=QVariant().toString()) -> QString
Get GPG executable path.
static auto getPassSigningKey(const QString &defaultValue=QVariant().toString()) -> QString
Get GPG signing key for pass.
static auto isUseSymbols(const bool &defaultValue=QVariant().toBool()) -> bool
Check whether symbol characters are enabled.
static auto isLessRandom(const bool &defaultValue=QVariant().toBool()) -> bool
Check whether less random password generation is enabled.
static auto isUsePwgen(const bool &defaultValue=QVariant().toBool()) -> bool
Check whether pwgen support is enabled.
static auto isAvoidNumbers(const bool &defaultValue=QVariant().toBool()) -> bool
Check whether numeric characters should be avoided.
static auto isValidKeyId(const QString &keyId) -> bool
Check if a string looks like a valid GPG key ID. Validates a GPG key ID after normalization:
Definition util.cpp:277
Debug utilities for QtPass.
#define dbg()
Simple debug macro that includes file and line number.
Definition debughelper.h:21
auto parseGpgColonOutput(const QString &output, bool secret) -> QList< UserInfo >
Parse GPG –with-colons output into a list of UserInfo.
Utility macros for QtPass.
#define AS_CONST(x)
Cross-platform const_cast for range-based for loops.
Definition helpers.h:20
@ PASS_INIT
Definition enums.h:36
@ PASS_OTP_GENERATE
Definition enums.h:44
@ PASS_INSERT
Definition enums.h:34
@ GIT_INIT
Definition enums.h:27
@ PASS_COPY
Definition enums.h:39
@ PASS_MOVE
Definition enums.h:38
@ GPG_GENKEYS
Definition enums.h:37
@ PASS_REMOVE
Definition enums.h:35
@ PASS_SHOW
Definition enums.h:33
@ GIT_PULL
Definition enums.h:31
@ GIT_PUSH
Definition enums.h:32
@ PASS_INIT
Definition enums.h:36
@ PASS_OTP_GENERATE
Definition enums.h:44
@ PASS_INSERT
Definition enums.h:34
@ GIT_INIT
Definition enums.h:27
@ PASS_COPY
Definition enums.h:39
@ PASS_MOVE
Definition enums.h:38
@ GPG_GENKEYS
Definition enums.h:37
@ PASS_REMOVE
Definition enums.h:35
@ PASS_SHOW
Definition enums.h:33
@ GIT_PULL
Definition enums.h:31
@ GIT_PUSH
Definition enums.h:32
QStringList arguments
Definition pass.h:19