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

IJHack / QtPass / 25827655541

13 May 2026 09:32PM UTC coverage: 29.068% (-0.02%) from 29.083%
25827655541

push

github

web-flow
cleanup: drop AS_CONST macro + qApp deprecation pragmas (#1472)

Two small mechanical refactors that simplify the source without
changing behaviour.

(a) AS_CONST macro removal
--------------------------

src/helpers.h defined a single macro:

    #if __cplusplus >= 201703L
    #  define AS_CONST(x) std::as_const(x)
    #else
    #  define AS_CONST(x) qAsConst(x)
    #endif

The whole repo now requires C++17 (src/src.pro and tests/tests.pri both
say c++17; CLAUDE.md states "this repository enforces C++17 for all
builds"), so the macro is just an alias for std::as_const. It was used
in two places — switch them to std::as_const directly and delete
helpers.h. Also drop the now-unused #include "helpers.h" from
passworddialog.cpp (it had never used the macro).

(b) qApp deprecation pragmas
----------------------------

Two call sites used the qApp macro inside `#pragma GCC diagnostic
ignored "-Wdeprecated-declarations"` blocks (qApp is deprecated in Qt
6.0+). Replace qApp with QApplication::instance() so the deprecation
warning has nothing to fire on, and remove the surrounding pragma
push/pop blocks (~12 lines) — including the MSVC `#pragma warning`
variants.

No functional change. Tests:
- tst_util: 124/124
- tst_storemodel: 33/33
- tst_filecontent: 21/21
- doxygen: zero warnings from our code

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

1 of 5 new or added lines in 4 files covered. (20.0%)

3 existing lines in 2 files now uncovered.

1956 of 6729 relevant lines covered (29.07%)

26.91 hits per line

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

67.41
/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 "qtpasssettings.h"
6
#include "util.h"
7
#include <QCoreApplication>
8
#include <QDebug>
9
#include <QDir>
10
#include <QFileInfo>
11
#include <QProcess>
12
#include <QRandomGenerator>
13
#include <QRegularExpression>
14
#include <algorithm>
15
#include <utility>
16

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

377
} // namespace
378

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

539
} // namespace
540

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

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

581
  return {};
582
}
×
583

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

873
  return gpgIdPath;
46✔
874
}
46✔
875

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

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

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

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

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

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

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

945
/**
946
 * @brief Generates a random password from the given charset.
947
 * @param charset Characters to use in the password
948
 * @param length Desired password length
949
 * @return Generated password string
950
 */
951
auto Pass::generateRandomPassword(const QString &charset, unsigned int length)
1,204✔
952
    -> QString {
953
  if (charset.isEmpty() || length == 0U) {
1,204✔
954
    return {};
955
  }
956
  QString out;
1,204✔
957
  for (unsigned int i = 0; i < length; ++i) {
8,796✔
958
    out.append(charset.at(static_cast<int>(
7,592✔
959
        boundedRandom(static_cast<quint32>(charset.length())))));
7,592✔
960
  }
961
  return out;
962
}
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