8#include <QCoreApplication>
13#include <QRandomGenerator>
14#include <QRegularExpression>
36auto fallbackCharset(
const QString &input,
const QString &fallback) -> QString {
37 return input.isEmpty() ? fallback : input;
41 int sel = passConfig.selected;
44 return fallbackCharset(
45 passConfig.Characters[sel],
53Pass::Pass() : wrapperRunning(false), env(QProcess::systemEnvironment()) {
55 static_cast<void (
Executor::*)(
int,
int,
const QString &,
56 const QString &)
>(&Executor::finished),
66 const QStringList wslenvVars = {
67 QStringLiteral(
"PASSWORD_STORE_DIR/p"),
68 QStringLiteral(
"PASSWORD_STORE_GENERATED_LENGTH/w"),
69 QStringLiteral(
"PASSWORD_STORE_CHARACTER_SET/w")};
70 const QString wslenvPrefix = QStringLiteral(
"WSLENV=");
72 std::find_if(env.begin(), env.end(), [&wslenvPrefix](
const QString &s) {
73 return s.startsWith(wslenvPrefix);
75 if (it == env.end()) {
76 env.append(wslenvPrefix + wslenvVars.join(
':'));
79 it->mid(wslenvPrefix.size()).split(
':', Qt::SkipEmptyParts);
80 for (
const QString &v : wslenvVars) {
81 if (!parts.contains(v))
84 *it = wslenvPrefix + parts.join(
':');
97 const QStringList &args,
bool readStdout,
103 const QStringList &args, QString input,
104 bool readStdout,
bool readStderr) {
106 dbg() << app << args;
109 readStdout, readStderr);
118 if (QFile(
"/usr/local/MacGPG2/bin").exists())
119 env.replaceInStrings(
"PATH=",
"PATH=/usr/local/MacGPG2/bin:");
121 if (env.filter(
"/usr/local/bin").isEmpty())
122 env.replaceInStrings(
"PATH=",
"PATH=/usr/local/bin:");
127 absHome.makeAbsolute();
128 env <<
"GNUPGHOME=" + absHome.path();
142 emit
critical(tr(
"Invalid password length"),
143 tr(
"Can't generate password with zero length."));
152 args.append(
"--secure");
159 args.append(
"--symbols");
161 args.append(QString::number(length));
165 static const QRegularExpression literalNewLines{
"[\\n\\r]"};
166 passwd.remove(literalNewLines);
170 qDebug() << __FILE__ <<
":" << __LINE__ <<
"\t"
179 const QString cs = fallbackCharset(
182 if (cs.length() > 0) {
186 tr(
"No characters chosen"),
187 tr(
"Can't generate password, there are no characters to choose from "
188 "set in the configuration!"));
202 {
"--version"}, &out, &err) != 0) {
205 QRegularExpression versionRegex(R
"(gpg \(GnuPG\) (\d+)\.(\d+))");
206 QRegularExpressionMatch match = versionRegex.match(out);
207 if (!match.hasMatch()) {
210 int major = match.captured(1).toInt();
211 int minor = match.captured(2).toInt();
212 return major > 2 || (major == 2 && minor >= 1);
222 return QStringLiteral(
"%echo Generating a default key\n"
224 "Key-Curve: Ed25519\n"
225 "Subkey-Type: ECDH\n"
226 "Subkey-Curve: Curve25519\n"
228 "Name-Comment: QtPass\n"
235 return QStringLiteral(
"%echo Generating a default key\n"
239 "Name-Comment: QtPass\n"
248auto resolveWslGpgconfPath(
const QString &lastPart) -> QString {
249 int lastSep = lastPart.lastIndexOf(
'/');
251 lastSep = lastPart.lastIndexOf(
'\\');
254 return lastPart.left(lastSep + 1) +
"gpgconf";
256 return QStringLiteral(
"gpgconf");
272QString findGpgconfInGpgDir(
const QString &gpgPath) {
273 QFileInfo gpgInfo(gpgPath);
274 if (!gpgInfo.isAbsolute()) {
278 QDir dir(gpgInfo.absolutePath());
281 QFileInfo candidateExe(dir.filePath(
"gpgconf.exe"));
282 if (candidateExe.isExecutable()) {
283 return candidateExe.filePath();
287 QFileInfo candidate(dir.filePath(
"gpgconf"));
288 if (candidate.isExecutable()) {
289 return candidate.filePath();
297#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
309QStringList splitCommandCompat(
const QString &command) {
312 bool inSingleQuote =
false;
313 bool inDoubleQuote =
false;
314 bool escaping =
false;
315 for (QChar ch : command) {
325 if (ch ==
'\'' && !inDoubleQuote) {
326 inSingleQuote = !inSingleQuote;
329 if (ch ==
'"' && !inSingleQuote) {
330 inDoubleQuote = !inDoubleQuote;
333 if (ch.isSpace() && !inSingleQuote && !inDoubleQuote) {
334 if (!current.isEmpty()) {
335 result.append(current);
343 current.append(
'\\');
345 if (!current.isEmpty()) {
346 result.append(current);
369 if (gpgPath.trimmed().isEmpty()) {
370 return {
"gpgconf", {}};
373#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
374 QStringList parts = QProcess::splitCommand(gpgPath);
376 QStringList parts = splitCommandCompat(gpgPath);
379 if (parts.isEmpty()) {
380 return {
"gpgconf", {}};
383 const QString first = parts.first();
384 if (first.compare(
"wsl", Qt::CaseInsensitive) == 0 ||
385 first.compare(
"wsl.exe", Qt::CaseInsensitive) == 0) {
386 if (parts.size() >= 2 && parts.at(1).startsWith(
"sh")) {
387 return {
"gpgconf", {}};
389 if (parts.size() >= 2 &&
390 QFileInfo(parts.last()).fileName().startsWith(
"gpg")) {
391 QString wslGpgconf = resolveWslGpgconfPath(parts.last());
393 parts.append(wslGpgconf);
394 return {parts.first(), parts.mid(1)};
396 return {
"gpgconf", {}};
399 if (!first.contains(
'/') && !first.contains(
'\\')) {
400 return {
"gpgconf", {}};
403 QString gpgconfPath = findGpgconfInGpgDir(first);
404 if (!gpgconfPath.isEmpty()) {
405 return {gpgconfPath, {}};
408 return {
"gpgconf", {}};
419 if (!gpgPath.isEmpty()) {
421 QStringList killArgs = resolvedGpgconf.
arguments;
422 killArgs <<
"--kill";
423 killArgs <<
"gpg-agent";
439 QStringList args = {
"--no-tty",
"--with-colons",
"--with-fingerprint"};
440 args.append(secret ?
"--list-secret-keys" :
"--list-keys");
442 for (
const QString &keystring :
AS_CONST(keystrings)) {
443 if (!keystring.isEmpty()) {
444 args.append(keystring);
450 return QList<UserInfo>();
462 return listKeys(QStringList(keystring), secret);
478auto containsAny(
const QString &str,
const QStringList &patterns) ->
bool {
479 for (
const QString &p : patterns) {
480 if (str.contains(p)) {
494auto containsAnyCaseInsensitive(
const QString &str,
const QStringList &patterns)
496 const QString lower = str.toLower();
497 for (
const QString &p : patterns) {
498 if (lower.contains(p)) {
509 if (containsAny(err, {QStringLiteral(
"[GNUPG:] KEYEXPIRED"),
510 QStringLiteral(
"[GNUPG:] INV_RECP 5 ")}))
511 return QCoreApplication::translate(
512 "Pass",
"Encryption failed: GPG key has expired. Please renew or "
514 if (containsAny(err, {QStringLiteral(
"[GNUPG:] KEYREVOKED"),
515 QStringLiteral(
"[GNUPG:] INV_RECP 4 ")}))
516 return QCoreApplication::translate(
517 "Pass",
"Encryption failed: GPG key has been revoked.");
518 if (containsAny(err, {QStringLiteral(
"[GNUPG:] NO_PUBKEY"),
519 QStringLiteral(
"[GNUPG:] INV_RECP")}))
520 return QCoreApplication::translate(
521 "Pass",
"Encryption failed: recipient GPG key not found or invalid. "
522 "Check that the key ID in .gpg-id is correct and imported.");
523 if (err.contains(QStringLiteral(
"[GNUPG:] FAILURE")))
524 return QCoreApplication::translate(
525 "Pass",
"Encryption failed. Check that your GPG key is valid.");
528 if (containsAnyCaseInsensitive(err, {QLatin1String(
"key has expired"),
529 QLatin1String(
"key expired")}))
530 return QCoreApplication::translate(
531 "Pass",
"Encryption failed: GPG key has expired. Please renew or "
533 if (containsAnyCaseInsensitive(err, {QLatin1String(
"key has been revoked"),
534 QLatin1String(
"revoked")}))
535 return QCoreApplication::translate(
536 "Pass",
"Encryption failed: GPG key has been revoked.");
537 if (containsAnyCaseInsensitive(err, {QLatin1String(
"no public key"),
538 QLatin1String(
"unusable public key"),
539 QLatin1String(
"no secret key")}))
540 return QCoreApplication::translate(
541 "Pass",
"Encryption failed: recipient GPG key not found or invalid. "
542 "Check that the key ID in .gpg-id is correct and imported.");
543 if (containsAnyCaseInsensitive(err, {QLatin1String(
"encryption failed")}))
544 return QCoreApplication::translate(
545 "Pass",
"Encryption failed. Check that your GPG key is valid.");
551auto isGrepHeaderLine(
const QString &rawLine,
const QString &trimmedLine)
555 return rawLine.startsWith(QStringLiteral(
"\x1B[94m")) ||
556 (!rawLine.startsWith(
' ') && !rawLine.startsWith(
'\t') &&
557 trimmedLine.endsWith(
':'));
569 -> QList<QPair<QString, QStringList>> {
570 static const QRegularExpression ansi(
571 QStringLiteral(R
"(\x1B\[[0-9;]*[a-zA-Z])"));
572 QList<QPair<QString, QStringList>> results;
573 QString currentEntry;
574 QStringList currentMatches;
575 for (
const QString &rawLine : rawOut.split(
'\n')) {
576 QString line = rawLine;
579 line = line.trimmed();
580 const bool isHeader = isGrepHeaderLine(rawLine, line);
582 if (!currentEntry.isEmpty() && !currentMatches.isEmpty())
583 results.append({currentEntry, currentMatches});
584 currentEntry = line.endsWith(
':') ? line.chopped(1) : line;
585 currentMatches.clear();
586 }
else if (!currentEntry.isEmpty()) {
588 currentMatches << line;
591 if (!currentEntry.isEmpty() && !currentMatches.isEmpty())
592 results.append({currentEntry, currentMatches});
607 const QString &err) {
608 auto pid =
static_cast<PROCESS>(id);
611 handleProcessError(pid, exitCode, out, err);
615 emitProcessFinishedSignal(pid, out, err);
618void Pass::handleProcessError(
PROCESS pid,
int exitCode,
const QString &out,
619 const QString &err) {
621 handleGrepError(exitCode, err);
627 if (!friendly.isEmpty()) {
636void Pass::handleGrepError(
int exitCode,
const QString &err) {
645auto Pass::formatInsertError(
const QString &friendly,
const QString &err)
647 QStringList humanLines;
648 for (
const QString &line : err.split(
'\n')) {
649 QString cleanedLine = line;
650 cleanedLine.remove(
'\r');
651 if (!cleanedLine.startsWith(QLatin1String(
"[GNUPG:]")))
652 humanLines.append(cleanedLine);
654 const QString humanErr = humanLines.join(
'\n').trimmed();
655 return humanErr.isEmpty() ? friendly : friendly +
"\n\n" + humanErr;
658void Pass::emitProcessFinishedSignal(
PROCESS pid,
const QString &out,
659 const QString &err) {
699 dbg() <<
"Unhandled process type" << pid;
712 const bool hasEq = key.endsWith(
'=');
713 Q_ASSERT_X(hasEq,
"Pass::setEnvVar",
714 "called with malformed key (missing '=')");
716 qWarning() <<
"Pass::setEnvVar called with malformed key (missing '='):"
720 const QStringList existing = env.filter(key);
721 for (
const QString &entry : existing)
722 env.removeAll(entry);
723 if (!value.isEmpty())
724 env.append(key + value);
728 setEnvVar(QStringLiteral(
"PASSWORD_STORE_SIGNING_KEY="),
730 setEnvVar(QStringLiteral(
"PASSWORD_STORE_DIR="),
734 setEnvVar(QStringLiteral(
"PASSWORD_STORE_GENERATED_LENGTH="),
735 QString::number(passConfig.
length));
737 setEnvVar(QStringLiteral(
"PASSWORD_STORE_CHARACTER_SET="),
738 effectiveCharset(passConfig));
740 exec.setEnvironment(env);
751 QString normalizedFile = QDir::fromNativeSeparators(for_file);
752 QString fullPath = normalizedFile.startsWith(passStore)
754 : passStore +
"/" + normalizedFile;
755 QDir gpgIdDir(QFileInfo(fullPath).absoluteDir());
757 while (gpgIdDir.exists() && gpgIdDir.absolutePath().startsWith(passStore)) {
758 if (QFile(gpgIdDir.absoluteFilePath(
".gpg-id")).exists()) {
762 if (!gpgIdDir.cdUp()) {
767 found ? gpgIdDir.absoluteFilePath(
".gpg-id")
780 if (!gpgId.open(QIODevice::ReadOnly | QIODevice::Text)) {
783 QStringList recipients;
784 while (!gpgId.atEnd()) {
785 QString recipient(gpgId.readLine());
786 recipient = recipient.split(
"#")[0].trimmed();
788 recipients += recipient;
802 int *count) -> QStringList {
806 *count = recipients.size();
833 const quint32 rejectionThreshold = (1 + ~bound) % bound;
836 randval = QRandomGenerator::system()->generate();
837 }
while (randval < rejectionThreshold);
839 return randval % bound;
850 if (charset.isEmpty() || length == 0U) {
854 for (
unsigned int i = 0; i < length; ++i) {
855 out.append(charset.at(
static_cast<int>(
id Identifier provided by the caller for this queued request.
static auto executeBlocking(const 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.
void starting()
starting signal that is emited when process starts
void init()
Initialize the Pass instance.
void startingExecuteWrapper()
Emitted before executing a command.
void GenerateGPGKeys(QString batch)
Generate GPG keys using batch script.
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.
virtual auto generatePassword(unsigned int length, const QString &charset) -> QString
Generate random password.
void finishedMove(const QString &, const QString &)
Emitted when move finishes.
static bool gpgSupportsEd25519()
Check if GPG supports Ed25519 encryption.
void setEnvVar(const QString &key, const QString &value)
Set or remove an environment variable.
auto boundedRandom(quint32 bound) -> quint32
Generate random number in range.
void finishedGitInit(const QString &, const QString &)
Emitted when Git init finishes.
Pass()
Construct a Pass instance.
void executeWrapper(PROCESS id, const QString &app, const QStringList &args, bool readStdout=true, bool readStderr=true)
Execute external wrapper command.
static auto getRecipientList(const QString &for_file) -> QStringList
Get list of recipients for a password file.
static auto resolveGpgconfCommand(const QString &gpgPath) -> ResolvedGpgconfCommand
Resolve the gpgconf command to kill agents.
auto listKeys(QStringList keystrings, bool secret=false) -> QList< UserInfo >
List GPG keys matching patterns.
void finishedInsert(const QString &, const QString &)
Emitted when insert finishes.
void finishedGrep(const QList< QPair< QString, QStringList > > &results)
Emitted when grep finishes with matching results.
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.
void finishedOtpGenerate(const QString &)
Emitted when OTP generation finishes.
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.
virtual void finished(int id, int exitCode, const QString &out, const QString &err)
Handle process completion.
static auto getRecipientString(const QString &for_file, const QString &separator=" ", int *count=nullptr) -> QStringList
Get recipients as string.
static QString getDefaultKeyTemplate()
Get default key template for new GPG keys.
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.
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. Accepts:
Debug utilities for QtPass.
#define dbg()
Simple debug macro that includes file and line number.
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.
auto gpgErrorMessage(const QString &err) -> QString
Maps GPG stderr (which may include –status-fd 2 tokens) to a translated user-friendly encryption erro...
auto parseGrepOutput(const QString &rawOut) -> QList< QPair< QString, QStringList > >
Parses 'pass grep' raw output into (entry, matches) pairs.
QList< QPair< QString, QStringList > > parseGrepOutput(const QString &rawOut)
Parses 'pass grep' raw output into (entry, matches) pairs.
QString gpgErrorMessage(const QString &err)
Maps GPG stderr (which may include –status-fd 2 tokens) to a translated user-friendly encryption erro...
PROCESS
Identifies different subprocess operations used in QtPass.
Holds the Password configuration settings.
int length
Length of the password.