• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IJHack / QtPass / 25576477093

08 May 2026 07:56PM UTC coverage: 28.048% (+0.03%) from 28.015%
25576477093

push

github

web-flow
refactor: clang-tidy cleanup (1 real bug + bulk modernize/performance fixes) (#1432)

* fix(trayicon): remove dead misleading setVisible method

TrayIcon::setVisible(bool) was a name collision with its base
class QWidget::setVisible(bool) — same signature, but the
implementation didn't toggle the tray icon's visibility. It
shows/hides the parent window:

  void TrayIcon::setVisible(bool visible) {
    if (visible) parentwin->show();
    else         parentwin->hide();
  }

Two problems:
- Misleading name: doesn't do what the QWidget API contract
  promises.
- Virtual shadowing without override: calling through a QWidget*
  pointer would call the base implementation, not this one,
  silently breaking caller intent.

Searched for callers — there are none. The method is dead code.
Removed the declaration and implementation together. Show/hide
of the parent window already happens elsewhere (via showAction
/ hideAction signals in createActions).

Caught by clang-tidy modernize-use-override.

* chore(clang-tidy): drop noisy stylistic checks

Two clang-tidy checks were producing high-volume findings that
the project doesn't actually want to fix:

- modernize-use-trailing-return-type (~55 in QtPass code, ~11K
  total counting Qt headers): suggests "auto foo() -> int"
  style. Project codebase mixes both; not enforcing the trailing
  form is a deliberate stylistic choice.

- modernize-use-nodiscard (~5 in QtPass code, ~3.7K total):
  suggests [[nodiscard]] on every getter. Project is selective
  about which getters need it; auto-applying everywhere creates
  noise and doesn't reflect intent.

Disabled both. Keeps the rest of modernize-* / performance-*
active so genuine improvements (default-member-init, override,
braced-init-list, enum-size, etc.) keep getting flagged.

Also added HeaderFilterRegex to keep findings from vendored
qprogressindicator out of the diagnostic stream.

* refactor: apply clang-tidy auto-fixes (modernize-* + performance-*)

Bulk pass o... (continued)

5 of 18 new or added lines in 12 files covered. (27.78%)

9 existing lines in 1 file now uncovered.

1854 of 6610 relevant lines covered (28.05%)

27.13 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

67.51
/src/pass.cpp
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 <QCoreApplication>
9
#include <QDebug>
10
#include <QDir>
11
#include <QFileInfo>
12
#include <QProcess>
13
#include <QRandomGenerator>
14
#include <QRegularExpression>
15
#include <algorithm>
16
#include <utility>
17

18
#ifdef QT_DEBUG
19
#include "debughelper.h"
20
#endif
21

22
using Enums::GIT_INIT;
23
using Enums::GIT_PULL;
24
using Enums::GIT_PUSH;
25
using Enums::GPG_GENKEYS;
26
using Enums::PASS_COPY;
27
using Enums::PASS_GREP;
28
using Enums::PASS_INIT;
29
using Enums::PASS_INSERT;
30
using Enums::PASS_MOVE;
31
using Enums::PASS_OTP_GENERATE;
32
using Enums::PASS_REMOVE;
33
using Enums::PASS_SHOW;
34

35
namespace {
36
/**
37
 * @brief Returns a non-empty charset value, using a fallback when needed.
38
 * @param input Preferred charset value.
39
 * @param fallback Charset to use when @p input is empty.
40
 * @return @p input if it is not empty; otherwise @p fallback.
41
 */
42
auto fallbackCharset(const QString &input, const QString &fallback) -> QString {
43
  return input.isEmpty() ? fallback : input;
1,226✔
44
}
45

46
/**
47
 * @brief Resolve the effective password character set from configuration.
48
 *
49
 * Uses the selected charset index from @p passConfig when it is within range;
50
 * otherwise falls back to the ALLCHARS entry. If the resolved charset string
51
 * is empty, falls back again to the ALLCHARS value.
52
 *
53
 * @param passConfig Password generation configuration.
54
 * @return Non-empty charset string to use for password generation.
55
 */
56
auto effectiveCharset(const PasswordConfiguration &passConfig) -> QString {
22✔
57
  int sel = passConfig.selected;
22✔
58
  if (sel < 0 || sel >= PasswordConfiguration::CHARSETS_COUNT)
22✔
59
    sel = PasswordConfiguration::ALLCHARS;
60
  return fallbackCharset(
61
      passConfig.Characters[sel],
22✔
62
      passConfig.Characters[PasswordConfiguration::ALLCHARS]);
22✔
63
}
64
} // namespace
65

66
/**
67
 * @brief Pass::Pass wrapper for using either pass or the pass imitation
68
 */
69
Pass::Pass() : env(QProcess::systemEnvironment()) {
35✔
70
  connect(&exec,
35✔
71
          static_cast<void (Executor::*)(int, int, const QString &,
72
                                         const QString &)>(&Executor::finished),
73
          this, &Pass::finished);
35✔
74

75
  // This was previously using direct QProcess signals.
76
  // The code now uses Executor instead of raw QProcess for better control.
77
  // connect(&process, SIGNAL(error(QProcess::ProcessError)), this,
78
  //        SIGNAL(error(QProcess::ProcessError)));
79

80
  connect(&exec, &Executor::starting, this, &Pass::startingExecuteWrapper);
35✔
81
  // Merge our vars into WSLENV rather than blindly appending a duplicate entry
82
  const QStringList wslenvVars = {
83
      QStringLiteral("PASSWORD_STORE_DIR/p"),
70✔
84
      QStringLiteral("PASSWORD_STORE_GENERATED_LENGTH/w"),
35✔
85
      QStringLiteral("PASSWORD_STORE_CHARACTER_SET/w")};
175✔
86
  const QString wslenvPrefix = QStringLiteral("WSLENV=");
35✔
87
  auto it =
88
      std::find_if(env.begin(), env.end(), [&wslenvPrefix](const QString &s) {
70✔
89
        return s.startsWith(wslenvPrefix);
4,494✔
90
      });
91
  if (it == env.end()) {
35✔
92
    env.append(wslenvPrefix + wslenvVars.join(':'));
70✔
93
  } else {
94
    QStringList parts =
95
        it->mid(wslenvPrefix.size()).split(':', Qt::SkipEmptyParts);
×
96
    for (const QString &v : wslenvVars) {
×
97
      if (!parts.contains(v))
×
98
        parts.append(v);
99
    }
100
    *it = wslenvPrefix + parts.join(':');
×
101
  }
102
}
35✔
103

104
/**
105
 * @brief Executes a wrapper command.
106
 * @param id Process ID
107
 * @param app Application to execute
108
 * @param args Arguments
109
 * @param readStdout Whether to read stdout
110
 * @param readStderr Whether to read stderr
111
 */
112
void Pass::executeWrapper(PROCESS id, const QString &app,
×
113
                          const QStringList &args, bool readStdout,
114
                          bool readStderr) {
115
  executeWrapper(id, app, args, QString(), readStdout, readStderr);
×
116
}
×
117

118
void Pass::executeWrapper(PROCESS id, const QString &app,
29✔
119
                          const QStringList &args, QString input,
120
                          bool readStdout, bool readStderr) {
121
#ifdef QT_DEBUG
122
  dbg() << app << args;
123
#endif
124
  exec.execute(id, QtPassSettings::getPassStore(), app, args, std::move(input),
58✔
125
               readStdout, readStderr);
126
}
29✔
127

128
/**
129
 * @brief Initializes the pass wrapper environment.
130
 */
131
void Pass::init() {
15✔
132
#ifdef __APPLE__
133
  // If it exists, add the gpgtools to PATH
134
  if (QFile("/usr/local/MacGPG2/bin").exists())
135
    env.replaceInStrings("PATH=", "PATH=/usr/local/MacGPG2/bin:");
136
  // Add missing /usr/local/bin
137
  if (env.filter("/usr/local/bin").isEmpty())
138
    env.replaceInStrings("PATH=", "PATH=/usr/local/bin:");
139
#endif
140

141
  if (!QtPassSettings::getGpgHome().isEmpty()) {
30✔
142
    QDir absHome(QtPassSettings::getGpgHome());
28✔
143
    absHome.makeAbsolute();
14✔
144
    env << "GNUPGHOME=" + absHome.path();
14✔
145
  }
14✔
146
}
15✔
147

148
/**
149
 * @brief Pass::Generate use either pwgen or internal password
150
 * generator
151
 * @param length of the desired password
152
 * @param charset to use for generation
153
 * @return the password
154
 */
155
auto Pass::generatePassword(unsigned int length, const QString &charset)
1,205✔
156
    -> QString {
157
  if (length == 0) {
1,205✔
158
    emit critical(tr("Invalid password length"),
2✔
159
                  tr("Can't generate password with zero length."));
1✔
160
    return {};
161
  }
162
  QString passwd;
1,204✔
163
  if (QtPassSettings::isUsePwgen()) {
1,204✔
164
    // --secure goes first as it overrides --no-* otherwise
165
    QStringList args;
×
166
    args.append("-1");
×
167
    if (!QtPassSettings::isLessRandom()) {
×
168
      args.append("--secure");
×
169
    }
170
    args.append(QtPassSettings::isAvoidCapitals() ? "--no-capitalize"
×
171
                                                  : "--capitalize");
172
    args.append(QtPassSettings::isAvoidNumbers() ? "--no-numerals"
×
173
                                                 : "--numerals");
174
    if (QtPassSettings::isUseSymbols()) {
×
175
      args.append("--symbols");
×
176
    }
177
    args.append(QString::number(length));
×
178
    // executeBlocking returns 0 on success, non-zero on failure
179
    if (Executor::executeBlocking(QtPassSettings::getPwgenExecutable(), args,
×
180
                                  &passwd) == 0) {
181
      static const QRegularExpression literalNewLines{"[\\n\\r]"};
×
182
      passwd.remove(literalNewLines);
×
183
    } else {
184
      passwd.clear();
×
185
#ifdef QT_DEBUG
186
      qDebug() << __FILE__ << ":" << __LINE__ << "\t"
187
               << "pwgen fail";
188
#endif
189
      // Error is already handled by clearing passwd; no need for critical
190
      // signal here
191
    }
192
  } else {
193
    // Validate charset - if CUSTOM is selected but chars are empty,
194
    // fall back to ALLCHARS to prevent weak passwords (issue #780)
195
    const QString cs = fallbackCharset(
196
        charset, QtPassSettings::getPasswordConfiguration()
2,408✔
197
                     .Characters[PasswordConfiguration::ALLCHARS]);
198
    if (cs.length() > 0) {
1,204✔
199
      passwd = generateRandomPassword(cs, length);
2,408✔
200
    } else {
201
      emit critical(
×
202
          tr("No characters chosen"),
×
203
          tr("Can't generate password, there are no characters to choose from "
×
204
             "set in the configuration!"));
205
    }
206
  }
207
  return passwd;
208
}
209

210
/**
211
 * @brief Pass::gpgSupportsEd25519 check if GPG supports ed25519 (ECC)
212
 * GPG 2.1+ supports ed25519 which is much faster for key generation
213
 * @return true if ed25519 is supported
214
 */
215
bool Pass::gpgSupportsEd25519() {
2✔
216
  QString out, err;
2✔
217
  if (Executor::executeBlocking(QtPassSettings::getGpgExecutable(),
8✔
218
                                {"--version"}, &out, &err) != 0) {
219
    return false;
220
  }
221
  QRegularExpression versionRegex(R"(gpg \(GnuPG\) (\d+)\.(\d+))");
×
222
  QRegularExpressionMatch match = versionRegex.match(out);
×
223
  if (!match.hasMatch()) {
×
224
    return false;
225
  }
226
  int major = match.captured(1).toInt();
×
227
  int minor = match.captured(2).toInt();
×
228
  return major > 2 || (major == 2 && minor >= 1);
×
229
}
2✔
230

231
/**
232
 * @brief Pass::getDefaultKeyTemplate return default key generation template
233
 * Uses ed25519 if supported, otherwise falls back to RSA
234
 * @return GPG batch template string
235
 */
236
QString Pass::getDefaultKeyTemplate() {
1✔
237
  if (gpgSupportsEd25519()) {
1✔
238
    return QStringLiteral("%echo Generating a default key\n"
×
239
                          "Key-Type: EdDSA\n"
240
                          "Key-Curve: Ed25519\n"
241
                          "Subkey-Type: ECDH\n"
242
                          "Subkey-Curve: Curve25519\n"
243
                          "Name-Real: \n"
244
                          "Name-Comment: QtPass\n"
245
                          "Name-Email: \n"
246
                          "Expire-Date: 0\n"
247
                          "%no-protection\n"
248
                          "%commit\n"
249
                          "%echo done");
250
  }
251
  return QStringLiteral("%echo Generating a default key\n"
1✔
252
                        "Key-Type: RSA\n"
253
                        "Subkey-Type: RSA\n"
254
                        "Name-Real: \n"
255
                        "Name-Comment: QtPass\n"
256
                        "Name-Email: \n"
257
                        "Expire-Date: 0\n"
258
                        "%no-protection\n"
259
                        "%commit\n"
260
                        "%echo done");
261
}
262

263
namespace {
264
/**
265
 * @brief Resolve a candidate gpgconf path from the trailing WSL path segment.
266
 *
267
 * Takes the directory portion of @p lastPart (separated by '/' or '\\') and
268
 * appends "gpgconf"; if no separator is present, returns the bare executable
269
 * name "gpgconf".
270
 *
271
 * @param lastPart Path fragment that may contain a directory and executable.
272
 * @return Full path ending in "gpgconf", or "gpgconf" as a fallback.
273
 */
274
auto resolveWslGpgconfPath(const QString &lastPart) -> QString {
3✔
275
  int lastSep = lastPart.lastIndexOf('/');
3✔
276
  if (lastSep < 0) {
3✔
277
    lastSep = lastPart.lastIndexOf('\\');
2✔
278
  }
279
  if (lastSep >= 0) {
2✔
280
    return lastPart.left(lastSep + 1) + "gpgconf";
2✔
281
  }
282
  return QStringLiteral("gpgconf");
2✔
283
}
284

285
/**
286
 * @brief Finds the path to the gpgconf executable in the same directory as the
287
 * given GPG path.
288
 * @example
289
 * QString result = findGpgconfInGpgDir(gpgPath);
290
 * std::cout << result.toStdString() << std::endl; // Expected output: path to
291
 * gpgconf or empty string
292
 *
293
 * @param gpgPath - Absolute path to a GPG executable or related file used to
294
 * locate gpgconf.
295
 * @return QString - The full path to gpgconf if found and executable; otherwise
296
 * an empty QString.
297
 */
298
QString findGpgconfInGpgDir(const QString &gpgPath) {
1✔
299
  QFileInfo gpgInfo(gpgPath);
1✔
300
  if (!gpgInfo.isAbsolute()) {
1✔
301
    return {};
302
  }
303

304
  QDir dir(gpgInfo.absolutePath());
1✔
305

306
#ifdef Q_OS_WIN
307
  QFileInfo candidateExe(dir.filePath("gpgconf.exe"));
308
  if (candidateExe.isExecutable()) {
309
    return candidateExe.filePath();
310
  }
311
#endif
312

313
  QFileInfo candidate(dir.filePath("gpgconf"));
1✔
314
  if (candidate.isExecutable()) {
1✔
315
    return candidate.filePath();
×
316
  }
317
  return {};
318
}
1✔
319

320
// Compatibility shim for Qt < 5.15 where QProcess::splitCommand is not
321
// available. Keep this fallback while supporting pre-5.15 builds; remove once
322
// the project's minimum supported Qt version is raised to 5.15 or newer.
323
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
324
/**
325
 * @brief Splits a command string into arguments while respecting quotes and
326
 * escape characters.
327
 * @example
328
 * QStringList result = splitCommandCompat("cmd \"arg one\" 'arg two'
329
 * escaped\\ space");
330
 * // Expected output: ["cmd", "arg one", "arg two", "escaped space"]
331
 *
332
 * @param command - The input command string to split into individual arguments.
333
 * @return QStringList - A list of parsed command arguments.
334
 */
335
QStringList splitCommandCompat(const QString &command) {
336
  QStringList result;
337
  QString current;
338
  bool inSingleQuote = false;
339
  bool inDoubleQuote = false;
340
  bool escaping = false;
341
  for (QChar ch : command) {
342
    if (escaping) {
343
      current.append(ch);
344
      escaping = false;
345
      continue;
346
    }
347
    if (ch == '\\') {
348
      escaping = true;
349
      continue;
350
    }
351
    if (ch == '\'' && !inDoubleQuote) {
352
      inSingleQuote = !inSingleQuote;
353
      continue;
354
    }
355
    if (ch == '"' && !inSingleQuote) {
356
      inDoubleQuote = !inDoubleQuote;
357
      continue;
358
    }
359
    if (ch.isSpace() && !inSingleQuote && !inDoubleQuote) {
360
      if (!current.isEmpty()) {
361
        result.append(current);
362
        current.clear();
363
      }
364
      continue;
365
    }
366
    current.append(ch);
367
  }
368
  if (escaping) {
369
    current.append('\\');
370
  }
371
  if (!current.isEmpty()) {
372
    result.append(current);
373
  }
374
  return result;
375
}
376
#endif
377

378
} // namespace
379

380
/**
381
 * @brief Resolves the appropriate gpgconf command from a given GPG executable
382
 * path or command string.
383
 * @example
384
 * ResolvedGpgconfCommand result = Pass::resolveGpgconfCommand("wsl.exe
385
 * /usr/bin/gpg"); std::cout << result.first.toStdString() << std::endl; //
386
 * Expected output sample
387
 *
388
 * @param const QString &gpgPath - Path or command string pointing to the GPG
389
 * executable.
390
 * @return ResolvedGpgconfCommand - A pair containing the resolved gpgconf
391
 * command and its arguments.
392
 */
393
auto Pass::resolveGpgconfCommand(const QString &gpgPath)
8✔
394
    -> ResolvedGpgconfCommand {
395
  if (gpgPath.trimmed().isEmpty()) {
8✔
396
    return {"gpgconf", {}};
397
  }
398

399
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
400
  QStringList parts = QProcess::splitCommand(gpgPath);
7✔
401
#else
402
  QStringList parts = splitCommandCompat(gpgPath);
403
#endif
404

405
  if (parts.isEmpty()) {
7✔
406
    return {"gpgconf", {}};
407
  }
408

409
  const QString first = parts.first();
410
  if (first.compare("wsl", Qt::CaseInsensitive) == 0 ||
16✔
411
      first.compare("wsl.exe", Qt::CaseInsensitive) == 0) {
9✔
412
    if (parts.size() >= 2 && parts.at(1).startsWith("sh")) {
9✔
413
      return {"gpgconf", {}};
414
    }
415
    if (parts.size() >= 2 &&
4✔
416
        QFileInfo(parts.last()).fileName().startsWith("gpg")) {
10✔
417
      QString wslGpgconf = resolveWslGpgconfPath(parts.last());
3✔
418
      parts.removeLast();
3✔
419
      parts.append(wslGpgconf);
420
      return {parts.first(), parts.mid(1)};
421
    }
422
    return {"gpgconf", {}};
423
  }
424

425
  if (!first.contains('/') && !first.contains('\\')) {
2✔
426
    return {"gpgconf", {}};
427
  }
428

429
  QString gpgconfPath = findGpgconfInGpgDir(first);
1✔
430
  if (!gpgconfPath.isEmpty()) {
1✔
431
    return {gpgconfPath, {}};
×
432
  }
433

434
  return {"gpgconf", {}};
435
}
8✔
436

437
/**
438
 * @brief Pass::GenerateGPGKeys internal gpg keypair generator . .
439
 * @param batch GnuPG style configuration string
440
 */
441
void Pass::GenerateGPGKeys(QString batch) {
×
442
  // Kill any stale GPG agents that might be holding locks on the key database
443
  // This helps avoid "database locked" timeouts during key generation
444
  QString gpgPath = QtPassSettings::getGpgExecutable();
×
445
  if (!gpgPath.isEmpty()) {
×
446
    ResolvedGpgconfCommand resolvedGpgconf = resolveGpgconfCommand(gpgPath);
×
447
    QStringList killArgs = resolvedGpgconf.arguments;
448
    killArgs << "--kill";
×
449
    killArgs << "gpg-agent";
×
450
    // Use same environment as key generation to target correct gpg-agent
451
    if (Executor::executeBlocking(env, resolvedGpgconf.program, killArgs) !=
×
452
        0) {
453
      qWarning() << "Failed to kill gpg-agent";
×
454
    }
455
  }
456

457
  executeWrapper(GPG_GENKEYS, gpgPath, {"--gen-key", "--no-tty", "--batch"},
×
458
                 std::move(batch));
459
}
×
460

461
/**
462
 * @brief Pass::listKeys list users
463
 * @param keystrings
464
 * @param secret list private keys
465
 * @return QList<UserInfo> users
466
 */
467
auto Pass::listKeys(QStringList keystrings, bool secret) -> QList<UserInfo> {
×
468
  QStringList args = {"--no-tty", "--with-colons", "--with-fingerprint"};
×
469
  args.append(secret ? "--list-secret-keys" : "--list-keys");
×
470

471
  for (const QString &keystring : AS_CONST(keystrings)) {
×
472
    if (!keystring.isEmpty()) {
×
473
      args.append(keystring);
474
    }
475
  }
476
  QString p_out;
×
477
  if (Executor::executeBlocking(QtPassSettings::getGpgExecutable(), args,
×
478
                                &p_out) != 0) {
NEW
479
    return {};
×
480
  }
481
  return parseGpgColonOutput(p_out, secret);
×
482
}
×
483

484
/**
485
 * @brief Pass::listKeys list users
486
 * @param keystring
487
 * @param secret list private keys
488
 * @return QList<UserInfo> users
489
 */
490
auto Pass::listKeys(const QString &keystring, bool secret) -> QList<UserInfo> {
×
491
  return listKeys(QStringList(keystring), secret);
×
492
}
493

494
/**
495
 * @brief Maps GPG stderr (which may include --status-fd 2 tokens) to a
496
 * user-friendly encryption error string.
497
 *
498
 * Checked in order: machine-readable [GNUPG:] status tokens first (locale-
499
 * independent), then case-insensitive substring fallbacks for GPG builds that
500
 * don't emit status tokens.
501
 *
502
 * @param err Raw stderr from GPG
503
 * @return Translated human-readable error, or empty string if not recognised
504
 */
505
namespace {
506

507
/**
508
 * @brief Checks if @p str contains any of the @p patterns (case-sensitive).
509
 * @param str String to search in.
510
 * @param patterns Patterns to search for.
511
 * @return true if any pattern is found, false otherwise.
512
 */
513
auto containsAny(const QString &str, const QStringList &patterns) -> bool {
31✔
514
  for (const QString &p : patterns) {
82✔
515
    if (str.contains(p)) {
58✔
516
      return true;
517
    }
518
  }
519
  return false;
520
}
521

522
/**
523
 * @brief Checks if str contains any of the patterns (case-insensitive).
524
 * @param str String to search in (will be lowercased once).
525
 * @param patterns List of patterns to search for (must be lowercase; caller
526
 * should convert patterns to lowercase before calling).
527
 * @return true if any pattern is found.
528
 */
529
auto containsAnyCaseInsensitive(const QString &str, const QStringList &patterns)
13✔
530
    -> bool {
531
  const QString lower = str.toLower();
532
  for (const QString &p : patterns) {
32✔
533
    if (lower.contains(p)) {
23✔
534
      return true;
535
    }
536
  }
537
  return false;
538
}
539

540
} // namespace
541

542
auto gpgErrorMessage(const QString &err) -> QString {
13✔
543
  // Machine-readable status tokens added by --status-fd 2
544
  if (containsAny(err, {QStringLiteral("[GNUPG:] KEYEXPIRED"),
65✔
545
                        QStringLiteral("[GNUPG:] INV_RECP 5 ")}))
13✔
546
    return QCoreApplication::translate(
547
        "Pass", "Encryption failed: GPG key has expired. Please renew or "
548
                "replace it.");
3✔
549
  if (containsAny(err, {QStringLiteral("[GNUPG:] KEYREVOKED"),
50✔
550
                        QStringLiteral("[GNUPG:] INV_RECP 4 ")}))
10✔
551
    return QCoreApplication::translate(
552
        "Pass", "Encryption failed: GPG key has been revoked.");
2✔
553
  if (containsAny(err, {QStringLiteral("[GNUPG:] NO_PUBKEY"),
40✔
554
                        QStringLiteral("[GNUPG:] INV_RECP")}))
8✔
555
    return QCoreApplication::translate(
556
        "Pass", "Encryption failed: recipient GPG key not found or invalid. "
557
                "Check that the key ID in .gpg-id is correct and imported.");
2✔
558
  if (err.contains(QStringLiteral("[GNUPG:] FAILURE")))
6✔
559
    return QCoreApplication::translate(
560
        "Pass", "Encryption failed. Check that your GPG key is valid.");
1✔
561

562
  // Locale-dependent fallbacks
563
  if (containsAnyCaseInsensitive(err, {QLatin1String("key has expired"),
20✔
564
                                       QLatin1String("key expired")}))
565
    return QCoreApplication::translate(
566
        "Pass", "Encryption failed: GPG key has expired. Please renew or "
567
                "replace it.");
1✔
568
  if (containsAnyCaseInsensitive(err, {QLatin1String("key has been revoked"),
16✔
569
                                       QLatin1String("revoked")}))
570
    return QCoreApplication::translate(
571
        "Pass", "Encryption failed: GPG key has been revoked.");
1✔
572
  if (containsAnyCaseInsensitive(err, {QLatin1String("no public key"),
15✔
573
                                       QLatin1String("unusable public key"),
574
                                       QLatin1String("no secret key")}))
575
    return QCoreApplication::translate(
576
        "Pass", "Encryption failed: recipient GPG key not found or invalid. "
577
                "Check that the key ID in .gpg-id is correct and imported.");
2✔
578
  if (containsAnyCaseInsensitive(err, {QLatin1String("encryption failed")}))
3✔
579
    return QCoreApplication::translate(
580
        "Pass", "Encryption failed. Check that your GPG key is valid.");
×
581

582
  return {};
583
}
×
584

585
namespace {
586
/**
587
 * @brief Determine whether a line from `pass grep` output is an entry header.
588
 *
589
 * Detects the ANSI blue escape (\x1B[94m) emitted by `pass grep`; as a
590
 * plain-text fallback, treats a non-indented line ending in ':' as a header.
591
 *
592
 * @param rawLine Original unmodified output line (with any ANSI codes).
593
 * @param trimmedLine The line after surrounding whitespace has been stripped.
594
 * @return true if the line is an entry header; otherwise false.
595
 */
596
auto isGrepHeaderLine(const QString &rawLine, const QString &trimmedLine)
45✔
597
    -> bool {
598
  return rawLine.startsWith(QStringLiteral("\x1B[94m")) ||
123✔
599
         (!rawLine.startsWith(' ') && !rawLine.startsWith('\t') &&
91✔
600
          trimmedLine.endsWith(':'));
119✔
601
}
602
} // namespace
603

604
/**
605
 * @brief Parses 'pass grep' raw output into (entry, matches) pairs.
606
 *
607
 * pass grep emits ANSI blue color (\x1B[94m) at the start of each entry
608
 * header line. This is checked before stripping ANSI so headers are detected
609
 * reliably regardless of locale.
610
 */
611
auto parseGrepOutput(const QString &rawOut)
12✔
612
    -> QList<QPair<QString, QStringList>> {
613
  static const QRegularExpression ansi(
614
      QStringLiteral(R"(\x1B\[[0-9;]*[a-zA-Z])"));
13✔
615
  QList<QPair<QString, QStringList>> results;
12✔
616
  QString currentEntry;
12✔
617
  QStringList currentMatches;
12✔
618
  for (const QString &rawLine : rawOut.split('\n')) {
69✔
619
    QString line = rawLine;
620
    line.remove('\r');
45✔
621
    line.remove(ansi);
45✔
622
    line = line.trimmed();
45✔
623
    const bool isHeader = isGrepHeaderLine(rawLine, line);
45✔
624
    if (isHeader) {
45✔
625
      if (!currentEntry.isEmpty() && !currentMatches.isEmpty())
14✔
626
        results.append({currentEntry, currentMatches});
3✔
627
      currentEntry = line.endsWith(':') ? line.chopped(1) : line;
14✔
628
      currentMatches.clear();
14✔
629
    } else if (!currentEntry.isEmpty()) {
31✔
630
      if (!line.isEmpty())
29✔
631
        currentMatches << line;
632
    }
633
  }
634
  if (!currentEntry.isEmpty() && !currentMatches.isEmpty())
12✔
635
    results.append({currentEntry, currentMatches});
11✔
636
  return results;
12✔
637
}
638

639
/**
640
 * @brief Pass::processFinished reemits specific signal based on what process
641
 * has finished
642
 * @param id    id of Pass process that was scheduled and finished
643
 * @param exitCode  return code of a process
644
 * @param out   output generated by process(if capturing was requested, empty
645
 *              otherwise)
646
 * @param err   error output generated by process(if capturing was requested,
647
 *              or error occurred)
648
 */
649
void Pass::finished(int id, int exitCode, const QString &out,
30✔
650
                    const QString &err) {
651
  auto pid = static_cast<PROCESS>(id);
30✔
652

653
  if (exitCode != 0) {
30✔
654
    handleProcessError(pid, exitCode, out, err);
2✔
655
    return;
2✔
656
  }
657

658
  emitProcessFinishedSignal(pid, out, err);
28✔
659
}
660

661
void Pass::handleProcessError(PROCESS pid, int exitCode, const QString &out,
2✔
662
                              const QString &err) {
663
  Q_UNUSED(out);
664

665
  if (pid == PASS_GREP) {
2✔
666
    handleGrepError(exitCode, err);
2✔
667
    return;
2✔
668
  }
669

670
  if (pid == PASS_INSERT) {
×
671
    const QString friendly = gpgErrorMessage(err);
×
672
    if (!friendly.isEmpty()) {
×
673
      emit processErrorExit(exitCode, formatInsertError(friendly, err));
×
674
      return;
675
    }
676
  }
677

678
  emit processErrorExit(exitCode, err);
×
679
}
680

681
void Pass::handleGrepError(int exitCode, const QString &err) {
2✔
682
  if (exitCode == 1) {
2✔
683
    emit finishedGrep({});
2✔
684
  } else {
685
    emit processErrorExit(exitCode, err);
1✔
686
    emit finishedGrep({});
2✔
687
  }
688
}
2✔
689

690
auto Pass::formatInsertError(const QString &friendly, const QString &err)
×
691
    -> QString {
692
  QStringList humanLines;
×
693
  for (const QString &line : err.split('\n')) {
×
694
    QString cleanedLine = line;
695
    cleanedLine.remove('\r');
×
696
    if (!cleanedLine.startsWith(QLatin1String("[GNUPG:]")))
×
697
      humanLines.append(cleanedLine);
698
  }
699
  const QString humanErr = humanLines.join('\n').trimmed();
×
700
  return humanErr.isEmpty() ? friendly : friendly + "\n\n" + humanErr;
×
701
}
702

703
/**
704
 * @brief Emit the appropriate finished signal for a completed subprocess.
705
 *
706
 * Emits a specific Qt signal corresponding to the given process identifier; for
707
 * grep results the stdout is parsed into a list of matches before emitting.
708
 *
709
 * @param pid The process identifier indicating which finished signal to emit.
710
 * @param out Standard output produced by the process.
711
 * @param err Standard error produced by the process.
712
 */
713
void Pass::emitProcessFinishedSignal(PROCESS pid, const QString &out,
28✔
714
                                     const QString &err) {
715
  /**
716
   * @brief Filter sensitive commands to prevent password leakage.
717
   *
718
   * Sensitive commands (PASS_SHOW, etc.) output plaintext passwords or
719
   * searchable content that should not be exposed to any UI listener.
720
   *
721
   * Using a default branch: if new PASS_* values are added, they
722
   * default to NOT leaking (safe by default). Making this
723
   * exhaustive would require updating here for every new
724
   * command and risk silent password leakage if forgotten.
725
   */
726
  switch (pid) {
28✔
727
  case PASS_SHOW:
728
  case PASS_OTP_GENERATE:
729
  case PASS_GREP:
730
  case PASS_INSERT:
731
    break;
732
  default:
×
733
    emit finishedAny(out, err);
×
734
    emit finishedAnyWithPid(out, err, pid);
×
735
    break;
×
736
  }
737

738
  switch (pid) {
28✔
739
  case GIT_INIT:
×
740
    emit finishedGitInit(out, err);
×
741
    break;
×
742
  case GIT_PULL:
×
743
    emit finishedGitPull(out, err);
×
744
    break;
×
745
  case GIT_PUSH:
×
746
    emit finishedGitPush(out, err);
×
747
    break;
×
748
  case PASS_SHOW:
10✔
749
    emit finishedShow(out);
10✔
750
    break;
10✔
751
  case PASS_OTP_GENERATE:
×
752
    emit finishedOtpGenerate(out);
×
753
    break;
×
754
  case PASS_INSERT:
17✔
755
    emit finishedInsert(out, err);
17✔
756
    break;
17✔
757
  case PASS_REMOVE:
×
758
    emit finishedRemove(out, err);
×
759
    break;
×
760
  case PASS_INIT:
×
761
    emit finishedInit(out, err);
×
762
    break;
×
763
  case PASS_MOVE:
×
764
    emit finishedMove(out, err);
×
765
    break;
×
766
  case PASS_COPY:
×
767
    emit finishedCopy(out, err);
×
768
    break;
×
769
  case GPG_GENKEYS:
×
770
    emit finishedGenerateGPGKeys(out, err);
×
771
    break;
×
772
  case PASS_GREP:
1✔
773
    emit finishedGrep(parseGrepOutput(out));
1✔
774
    break;
1✔
775
  default:
776
#ifdef QT_DEBUG
777
    dbg() << "Unhandled process type" << pid;
778
#endif
779
    break;
780
  }
781
}
28✔
782

783
/**
784
 * @brief Set or remove a single environment variable in the local env list.
785
 *
786
 * The provided key must include a trailing '=' (e.g. "FOO="). Existing entries
787
 * whose text begins with the given key are removed before the new value is
788
 * applied. If value is non-empty the pair "key+value" is appended; if value is
789
 * empty the variable is removed.
790
 *
791
 * The function asserts if key does not end with '='; if the assertion is not
792
 * active it will emit a warning and return without modifying env.
793
 *
794
 * @param key Environment variable name with trailing '=' (anchors the lookup).
795
 * @param value Value to set for the variable; an empty string unsets the
796
 * variable.
797
 */
798
void Pass::setEnvVar(const QString &key, const QString &value) {
94✔
799
  const bool hasEq = key.endsWith('=');
94✔
800
  Q_ASSERT_X(hasEq, "Pass::setEnvVar",
801
             "called with malformed key (missing '=')");
802
  if (!hasEq) {
94✔
803
    qWarning() << "Pass::setEnvVar called with malformed key (missing '='):"
×
804
               << key;
×
805
    return;
×
806
  }
807
  env.erase(std::remove_if(
188✔
808
                env.begin(), env.end(),
809
                [&key](const QString &entry) { return entry.startsWith(key); }),
12,326✔
810
            env.end());
811
  if (!value.isEmpty())
94✔
812
    env.append(key + value);
140✔
813
}
814

815
/**
816
 * @brief Update the process environment used for executing external commands.
817
 *
818
 * Updates environment entries for PASSWORD_STORE_SIGNING_KEY,
819
 * PASSWORD_STORE_DIR, PASSWORD_STORE_GENERATED_LENGTH, and
820
 * PASSWORD_STORE_CHARACTER_SET based on current settings, then applies the
821
 * environment to the internal executor.
822
 */
823
void Pass::updateEnv() {
22✔
824
  setEnvVar(QStringLiteral("PASSWORD_STORE_SIGNING_KEY="),
44✔
825
            QtPassSettings::getPassSigningKey());
44✔
826
  setEnvVar(QStringLiteral("PASSWORD_STORE_DIR="),
44✔
827
            QtPassSettings::getPassStore());
44✔
828

829
  PasswordConfiguration passConfig = QtPassSettings::getPasswordConfiguration();
22✔
830
  setEnvVar(QStringLiteral("PASSWORD_STORE_GENERATED_LENGTH="),
44✔
831
            QString::number(passConfig.length));
22✔
832

833
  setEnvVar(QStringLiteral("PASSWORD_STORE_CHARACTER_SET="),
44✔
834
            effectiveCharset(passConfig));
22✔
835

836
  exec.setEnvironment(env);
22✔
837
}
22✔
838

839
/**
840
 * @brief Pass::getGpgIdPath return gpgid file path for some file (folder).
841
 * @param for_file which file (folder) would you like the gpgid file path for.
842
 * @return path to the gpgid file.
843
 */
844
auto Pass::getGpgIdPath(const QString &for_file) -> QString {
46✔
845
  QString passStore =
846
      QDir::fromNativeSeparators(QtPassSettings::getPassStore());
92✔
847
  QString normalizedFile = QDir::fromNativeSeparators(for_file);
46✔
848
  QString fullPath = normalizedFile.startsWith(passStore)
46✔
849
                         ? normalizedFile
46✔
850
                         : passStore + "/" + normalizedFile;
41✔
851
  QDir gpgIdDir(QFileInfo(fullPath).absoluteDir());
46✔
852
  // QDir::cleanPath() always normalises to forward slashes, so use '/'
853
  // here rather than QDir::separator() (which returns '\\' on Windows).
854
  QString cleanPassStore = QDir::cleanPath(passStore);
46✔
855
  bool found = false;
856
  while (gpgIdDir.exists()) {
63✔
857
    QString currentPath = QDir::cleanPath(gpgIdDir.absolutePath());
112✔
858
    if (currentPath != cleanPassStore &&
74✔
859
        !currentPath.startsWith(cleanPassStore + "/")) {
74✔
860
      break;
861
    }
862
    if (QFile(gpgIdDir.absoluteFilePath(".gpg-id")).exists()) {
110✔
863
      found = true;
864
      break;
865
    }
866
    if (!gpgIdDir.cdUp()) {
17✔
867
      break;
868
    }
869
  }
870
  QString gpgIdPath(
871
      found ? gpgIdDir.absoluteFilePath(".gpg-id")
46✔
872
            : QDir(QtPassSettings::getPassStore()).filePath(".gpg-id"));
108✔
873

874
  return gpgIdPath;
46✔
875
}
46✔
876

877
/**
878
 * @brief Pass::getRecipientList return list of gpg-id's to encrypt for
879
 * @param for_file which file (folder) would you like recipients for
880
 * @return recipients gpg-id contents
881
 */
882
auto Pass::getRecipientList(const QString &for_file) -> QStringList {
24✔
883
  QFile gpgId(getGpgIdPath(for_file));
24✔
884
  if (!gpgId.open(QIODevice::ReadOnly | QIODevice::Text)) {
24✔
885
    return {};
×
886
  }
887
  QStringList recipients;
24✔
888
  while (!gpgId.atEnd()) {
56✔
889
    QString recipient(gpgId.readLine());
64✔
890
    recipient = recipient.split("#")[0].trimmed();
64✔
891
    if (!recipient.isEmpty() && Util::isValidKeyId(recipient)) {
32✔
892
      recipients += recipient;
893
    }
894
  }
895
  return recipients;
896
}
24✔
897

898
/**
899
 * @brief Pass::getRecipientString formatted string for use with GPG
900
 * @param for_file which file (folder) would you like recipients for
901
 * @param separator formatting separator eg: " -r "
902
 * @param count
903
 * @return recipient string
904
 */
905
auto Pass::getRecipientString(const QString &for_file, const QString &separator,
2✔
906
                              int *count) -> QStringList {
907
  Q_UNUSED(separator)
908
  QStringList recipients = Pass::getRecipientList(for_file);
2✔
909
  if (count) {
2✔
910
    *count = recipients.size();
1✔
911
  }
912
  return recipients;
2✔
913
}
914

915
/* Copyright (C) 2017 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
916
 */
917

918
/**
919
 * @brief Generates a random number bounded by the given value.
920
 * @param bound Upper bound (exclusive)
921
 * @return Random number in range [0, bound)
922
 */
923
auto Pass::boundedRandom(quint32 bound) -> quint32 {
7,592✔
924
  if (bound < 2) {
7,592✔
925
    return 0;
926
  }
927

928
  quint32 randval;
929
  // Rejection-sampling threshold to avoid modulo bias.
930
  // This follows the well-known "arc4random_uniform"-style approach:
931
  // reject values in the low range [0, min), where
932
  //   min = 2^32 % bound
933
  // so that the remaining range size is an exact multiple of `bound`.
934
  //
935
  // In quint32 arithmetic, (1 + ~bound) wraps to (2^32 - bound), therefore
936
  //   (1 + ~bound) % bound == 2^32 % bound.
937
  const quint32 rejectionThreshold = (1 + ~bound) % bound;
7,592✔
938

939
  do {
940
    randval = QRandomGenerator::system()->generate();
941
  } while (randval < rejectionThreshold);
7,592✔
942

943
  return randval % bound;
7,592✔
944
}
945

946
/**
947
 * @brief Generates a random password from the given charset.
948
 * @param charset Characters to use in the password
949
 * @param length Desired password length
950
 * @return Generated password string
951
 */
952
auto Pass::generateRandomPassword(const QString &charset, unsigned int length)
1,204✔
953
    -> QString {
954
  if (charset.isEmpty() || length == 0U) {
1,204✔
955
    return {};
956
  }
957
  QString out;
1,204✔
958
  for (unsigned int i = 0; i < length; ++i) {
8,796✔
959
    out.append(charset.at(static_cast<int>(
7,592✔
960
        boundedRandom(static_cast<quint32>(charset.length())))));
7,592✔
961
  }
962
  return out;
963
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc