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

IJHack / QtPass / 24632806189

19 Apr 2026 03:40PM UTC coverage: 27.226% (+0.01%) from 27.213%
24632806189

Pull #1065

github

web-flow
Merge d4fae5533 into 6c9e5283f
Pull Request #1065: fix: ai findings in pass.cpp

5 of 8 new or added lines in 1 file covered. (62.5%)

49 existing lines in 1 file now uncovered.

1578 of 5796 relevant lines covered (27.23%)

10.26 hits per line

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

68.01
/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 <QDir>
10
#include <QFileInfo>
11
#include <QProcess>
12
#include <QRandomGenerator>
13
#include <QRegularExpression>
14
#include <utility>
15

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

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

33
/**
34
 * @brief Pass::Pass wrapper for using either pass or the pass imitation
35
 */
36
Pass::Pass() : wrapperRunning(false), env(QProcess::systemEnvironment()) {
29✔
37
  connect(&exec,
29✔
38
          static_cast<void (Executor::*)(int, int, const QString &,
39
                                         const QString &)>(&Executor::finished),
40
          this, &Pass::finished);
29✔
41

42
  // This was previously using direct QProcess signals.
43
  // The code now uses Executor instead of raw QProcess for better control.
44
  // connect(&process, SIGNAL(error(QProcess::ProcessError)), this,
45
  //        SIGNAL(error(QProcess::ProcessError)));
46

47
  connect(&exec, &Executor::starting, this, &Pass::startingExecuteWrapper);
29✔
48
  // Merge our vars into WSLENV rather than blindly appending a duplicate entry
49
  const QStringList wslenvVars = {
50
      QStringLiteral("PASSWORD_STORE_DIR/p"),
58✔
51
      QStringLiteral("PASSWORD_STORE_GENERATED_LENGTH/w"),
29✔
52
      QStringLiteral("PASSWORD_STORE_CHARACTER_SET/w")};
145✔
53
  const QString wslenvPrefix = QStringLiteral("WSLENV=");
29✔
54
  auto it = std::find_if(env.begin(), env.end(), [&wslenvPrefix](const QString &s) {
58✔
55
    return s.startsWith(wslenvPrefix);
3,720✔
56
  });
57
  if (it == env.end()) {
29✔
58
    env.append(wslenvPrefix + wslenvVars.join(':'));
58✔
59
  } else {
60
    QStringList parts =
UNCOV
61
        it->mid(wslenvPrefix.size()).split(':', Qt::SkipEmptyParts);
×
UNCOV
62
    for (const QString &v : wslenvVars) {
×
63
      if (!parts.contains(v))
×
64
        parts.append(v);
65
    }
UNCOV
66
    *it = wslenvPrefix + parts.join(':');
×
67
  }
68
}
29✔
69

70
/**
71
 * @brief Executes a wrapper command.
72
 * @param id Process ID
73
 * @param app Application to execute
74
 * @param args Arguments
75
 * @param readStdout Whether to read stdout
76
 * @param readStderr Whether to read stderr
77
 */
UNCOV
78
void Pass::executeWrapper(PROCESS id, const QString &app,
×
79
                          const QStringList &args, bool readStdout,
80
                          bool readStderr) {
UNCOV
81
  executeWrapper(id, app, args, QString(), readStdout, readStderr);
×
UNCOV
82
}
×
83

84
void Pass::executeWrapper(PROCESS id, const QString &app,
17✔
85
                          const QStringList &args, QString input,
86
                          bool readStdout, bool readStderr) {
87
#ifdef QT_DEBUG
88
  dbg() << app << args;
89
#endif
90
  exec.execute(id, QtPassSettings::getPassStore(), app, args, std::move(input),
34✔
91
               readStdout, readStderr);
92
}
17✔
93

94
/**
95
 * @brief Initializes the pass wrapper environment.
96
 */
97
void Pass::init() {
9✔
98
#ifdef __APPLE__
99
  // If it exists, add the gpgtools to PATH
100
  if (QFile("/usr/local/MacGPG2/bin").exists())
101
    env.replaceInStrings("PATH=", "PATH=/usr/local/MacGPG2/bin:");
102
  // Add missing /usr/local/bin
103
  if (env.filter("/usr/local/bin").isEmpty())
104
    env.replaceInStrings("PATH=", "PATH=/usr/local/bin:");
105
#endif
106

107
  if (!QtPassSettings::getGpgHome().isEmpty()) {
18✔
108
    QDir absHome(QtPassSettings::getGpgHome());
16✔
109
    absHome.makeAbsolute();
8✔
110
    env << "GNUPGHOME=" + absHome.path();
8✔
111
  }
8✔
112
}
9✔
113

114
/**
115
 * @brief Pass::Generate use either pwgen or internal password
116
 * generator
117
 * @param length of the desired password
118
 * @param charset to use for generation
119
 * @return the password
120
 */
121
auto Pass::generatePassword(unsigned int length, const QString &charset)
1,006✔
122
    -> QString {
123
  QString passwd;
1,006✔
124
  if (QtPassSettings::isUsePwgen()) {
1,006✔
125
    // --secure goes first as it overrides --no-* otherwise
UNCOV
126
    QStringList args;
×
UNCOV
127
    args.append("-1");
×
128
    if (!QtPassSettings::isLessRandom()) {
×
129
      args.append("--secure");
×
130
    }
131
    args.append(QtPassSettings::isAvoidCapitals() ? "--no-capitalize"
×
132
                                                  : "--capitalize");
133
    args.append(QtPassSettings::isAvoidNumbers() ? "--no-numerals"
×
134
                                                 : "--numerals");
135
    if (QtPassSettings::isUseSymbols()) {
×
UNCOV
136
      args.append("--symbols");
×
137
    }
138
    args.append(QString::number(length));
×
139
    // executeBlocking returns 0 on success, non-zero on failure
140
    if (Executor::executeBlocking(QtPassSettings::getPwgenExecutable(), args,
×
141
                                  &passwd) == 0) {
142
      static const QRegularExpression literalNewLines{"[\\n\\r]"};
×
UNCOV
143
      passwd.remove(literalNewLines);
×
144
    } else {
145
      passwd.clear();
×
146
#ifdef QT_DEBUG
147
      qDebug() << __FILE__ << ":" << __LINE__ << "\t"
148
               << "pwgen fail";
149
#endif
150
      // Error is already handled by clearing passwd; no need for critical
151
      // signal here
152
    }
153
  } else {
154
    // Validate charset - if CUSTOM is selected but chars are empty,
155
    // fall back to ALLCHARS to prevent weak passwords (issue #780)
156
    QString effectiveCharset = charset;
157
    if (effectiveCharset.isEmpty()) {
1,006✔
158
      effectiveCharset = QtPassSettings::getPasswordConfiguration()
2✔
159
                             .Characters[PasswordConfiguration::ALLCHARS];
160
    }
161
    if (effectiveCharset.length() > 0) {
1,006✔
162
      passwd = generateRandomPassword(effectiveCharset, length);
2,012✔
163
    } else {
UNCOV
164
      emit critical(
×
UNCOV
165
          tr("No characters chosen"),
×
166
          tr("Can't generate password, there are no characters to choose from "
×
167
             "set in the configuration!"));
168
    }
169
  }
170
  return passwd;
1,006✔
171
}
172

173
/**
174
 * @brief Pass::gpgSupportsEd25519 check if GPG supports ed25519 (ECC)
175
 * GPG 2.1+ supports ed25519 which is much faster for key generation
176
 * @return true if ed25519 is supported
177
 */
178
bool Pass::gpgSupportsEd25519() {
2✔
179
  QString out, err;
2✔
180
  if (Executor::executeBlocking(QtPassSettings::getGpgExecutable(),
8✔
181
                                {"--version"}, &out, &err) != 0) {
182
    return false;
183
  }
UNCOV
184
  QRegularExpression versionRegex(R"(gpg \(GnuPG\) (\d+)\.(\d+))");
×
UNCOV
185
  QRegularExpressionMatch match = versionRegex.match(out);
×
186
  if (!match.hasMatch()) {
×
187
    return false;
188
  }
UNCOV
189
  int major = match.captured(1).toInt();
×
UNCOV
190
  int minor = match.captured(2).toInt();
×
191
  return major > 2 || (major == 2 && minor >= 1);
×
192
}
2✔
193

194
/**
195
 * @brief Pass::getDefaultKeyTemplate return default key generation template
196
 * Uses ed25519 if supported, otherwise falls back to RSA
197
 * @return GPG batch template string
198
 */
199
QString Pass::getDefaultKeyTemplate() {
1✔
200
  if (gpgSupportsEd25519()) {
1✔
UNCOV
201
    return QStringLiteral("%echo Generating a default key\n"
×
202
                          "Key-Type: EdDSA\n"
203
                          "Key-Curve: Ed25519\n"
204
                          "Subkey-Type: ECDH\n"
205
                          "Subkey-Curve: Curve25519\n"
206
                          "Name-Real: \n"
207
                          "Name-Comment: QtPass\n"
208
                          "Name-Email: \n"
209
                          "Expire-Date: 0\n"
210
                          "%no-protection\n"
211
                          "%commit\n"
212
                          "%echo done");
213
  }
214
  return QStringLiteral("%echo Generating a default key\n"
1✔
215
                        "Key-Type: RSA\n"
216
                        "Subkey-Type: RSA\n"
217
                        "Name-Real: \n"
218
                        "Name-Comment: QtPass\n"
219
                        "Name-Email: \n"
220
                        "Expire-Date: 0\n"
221
                        "%no-protection\n"
222
                        "%commit\n"
223
                        "%echo done");
224
}
225

226
namespace {
227
auto resolveWslGpgconfPath(const QString &lastPart) -> QString {
3✔
228
  int lastSep = lastPart.lastIndexOf('/');
3✔
229
  if (lastSep < 0) {
3✔
230
    lastSep = lastPart.lastIndexOf('\\');
2✔
231
  }
232
  if (lastSep >= 0) {
2✔
233
    return lastPart.left(lastSep + 1) + "gpgconf";
2✔
234
  }
235
  return QStringLiteral("gpgconf");
2✔
236
}
237

238
/**
239
 * @brief Finds the path to the gpgconf executable in the same directory as the
240
 * given GPG path.
241
 * @example
242
 * QString result = findGpgconfInGpgDir(gpgPath);
243
 * std::cout << result.toStdString() << std::endl; // Expected output: path to
244
 * gpgconf or empty string
245
 *
246
 * @param gpgPath - Absolute path to a GPG executable or related file used to
247
 * locate gpgconf.
248
 * @return QString - The full path to gpgconf if found and executable; otherwise
249
 * an empty QString.
250
 */
251
QString findGpgconfInGpgDir(const QString &gpgPath) {
1✔
252
  QFileInfo gpgInfo(gpgPath);
1✔
253
  if (!gpgInfo.isAbsolute()) {
1✔
254
    return QString();
255
  }
256

257
  QDir dir(gpgInfo.absolutePath());
1✔
258

259
#ifdef Q_OS_WIN
260
  QFileInfo candidateExe(dir.filePath("gpgconf.exe"));
261
  if (candidateExe.isExecutable()) {
262
    return candidateExe.filePath();
263
  }
264
#endif
265

266
  QFileInfo candidate(dir.filePath("gpgconf"));
1✔
267
  if (candidate.isExecutable()) {
1✔
UNCOV
268
    return candidate.filePath();
×
269
  }
270
  return QString();
271
}
1✔
272

273
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
274
/**
275
 * @brief Splits a command string into arguments while respecting quotes and
276
 * escape characters.
277
 * @example
278
 * QStringList result = splitCommandCompat("cmd \"arg one\" 'arg two'
279
 * escaped\\ space");
280
 * // Expected output: ["cmd", "arg one", "arg two", "escaped space"]
281
 *
282
 * @param command - The input command string to split into individual arguments.
283
 * @return QStringList - A list of parsed command arguments.
284
 */
285
QStringList splitCommandCompat(const QString &command) {
286
  QStringList result;
287
  QString current;
288
  bool inSingleQuote = false;
289
  bool inDoubleQuote = false;
290
  bool escaping = false;
291
  for (QChar ch : command) {
292
    if (escaping) {
293
      current.append(ch);
294
      escaping = false;
295
      continue;
296
    }
297
    if (ch == '\\') {
298
      escaping = true;
299
      continue;
300
    }
301
    if (ch == '\'' && !inDoubleQuote) {
302
      inSingleQuote = !inSingleQuote;
303
      continue;
304
    }
305
    if (ch == '"' && !inSingleQuote) {
306
      inDoubleQuote = !inDoubleQuote;
307
      continue;
308
    }
309
    if (ch.isSpace() && !inSingleQuote && !inDoubleQuote) {
310
      if (!current.isEmpty()) {
311
        result.append(current);
312
        current.clear();
313
      }
314
      continue;
315
    }
316
    current.append(ch);
317
  }
318
  if (escaping) {
319
    current.append('\\');
320
  }
321
  if (!current.isEmpty()) {
322
    result.append(current);
323
  }
324
  return result;
325
}
326
#endif
327

328
} // namespace
329

330
/**
331
 * @brief Resolves the appropriate gpgconf command from a given GPG executable
332
 * path or command string.
333
 * @example
334
 * ResolvedGpgconfCommand result = Pass::resolveGpgconfCommand("wsl.exe
335
 * /usr/bin/gpg"); std::cout << result.first.toStdString() << std::endl; //
336
 * Expected output sample
337
 *
338
 * @param const QString &gpgPath - Path or command string pointing to the GPG
339
 * executable.
340
 * @return ResolvedGpgconfCommand - A pair containing the resolved gpgconf
341
 * command and its arguments.
342
 */
343
auto Pass::resolveGpgconfCommand(const QString &gpgPath)
8✔
344
    -> ResolvedGpgconfCommand {
345
  if (gpgPath.trimmed().isEmpty()) {
8✔
346
    return {"gpgconf", {}};
347
  }
348

349
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
350
  QStringList parts = QProcess::splitCommand(gpgPath);
7✔
351
#else
352
  QStringList parts = splitCommandCompat(gpgPath);
353
#endif
354

355
  if (parts.isEmpty()) {
7✔
356
    return {"gpgconf", {}};
357
  }
358

359
  const QString first = parts.first();
360
  if (first.compare("wsl", Qt::CaseInsensitive) == 0 ||
16✔
361
      first.compare("wsl.exe", Qt::CaseInsensitive) == 0) {
9✔
362
    if (parts.size() >= 2 && parts.at(1).startsWith("sh")) {
9✔
363
      return {"gpgconf", {}};
364
    }
365
    if (parts.size() >= 2 &&
4✔
366
        QFileInfo(parts.last()).fileName().startsWith("gpg")) {
10✔
367
      QString wslGpgconf = resolveWslGpgconfPath(parts.last());
3✔
368
      parts.removeLast();
3✔
369
      parts.append(wslGpgconf);
370
      return {parts.first(), parts.mid(1)};
371
    }
372
    return {"gpgconf", {}};
373
  }
374

375
  if (!first.contains('/') && !first.contains('\\')) {
2✔
376
    return {"gpgconf", {}};
377
  }
378

379
  QString gpgconfPath = findGpgconfInGpgDir(first);
1✔
380
  if (!gpgconfPath.isEmpty()) {
1✔
UNCOV
381
    return {gpgconfPath, {}};
×
382
  }
383

384
  return {"gpgconf", {}};
385
}
8✔
386

387
/**
388
 * @brief Pass::GenerateGPGKeys internal gpg keypair generator . .
389
 * @param batch GnuPG style configuration string
390
 */
UNCOV
391
void Pass::GenerateGPGKeys(QString batch) {
×
392
  // Kill any stale GPG agents that might be holding locks on the key database
393
  // This helps avoid "database locked" timeouts during key generation
UNCOV
394
  QString gpgPath = QtPassSettings::getGpgExecutable();
×
UNCOV
395
  if (!gpgPath.isEmpty()) {
×
396
    ResolvedGpgconfCommand gpgconf = resolveGpgconfCommand(gpgPath);
×
397
    QStringList killArgs = gpgconf.arguments;
398
    killArgs << "--kill";
×
UNCOV
399
    killArgs << "gpg-agent";
×
400
    // Use same environment as key generation to target correct gpg-agent
401
    Executor::executeBlocking(env, gpgconf.program, killArgs);
×
402
  }
403

UNCOV
404
  executeWrapper(GPG_GENKEYS, gpgPath, {"--gen-key", "--no-tty", "--batch"},
×
405
                 std::move(batch));
406
}
×
407

408
/**
409
 * @brief Pass::listKeys list users
410
 * @param keystrings
411
 * @param secret list private keys
412
 * @return QList<UserInfo> users
413
 */
UNCOV
414
auto Pass::listKeys(QStringList keystrings, bool secret) -> QList<UserInfo> {
×
UNCOV
415
  QStringList args = {"--no-tty", "--with-colons", "--with-fingerprint"};
×
416
  args.append(secret ? "--list-secret-keys" : "--list-keys");
×
417

418
  for (const QString &keystring : AS_CONST(keystrings)) {
×
UNCOV
419
    if (!keystring.isEmpty()) {
×
420
      args.append(keystring);
421
    }
422
  }
UNCOV
423
  QString p_out;
×
UNCOV
424
  if (Executor::executeBlocking(QtPassSettings::getGpgExecutable(), args,
×
425
                                &p_out) != 0) {
426
    return QList<UserInfo>();
×
427
  }
428
  return parseGpgColonOutput(p_out, secret);
×
UNCOV
429
}
×
430

431
/**
432
 * @brief Pass::listKeys list users
433
 * @param keystring
434
 * @param secret list private keys
435
 * @return QList<UserInfo> users
436
 */
UNCOV
437
auto Pass::listKeys(const QString &keystring, bool secret) -> QList<UserInfo> {
×
UNCOV
438
  return listKeys(QStringList(keystring), secret);
×
439
}
440

441
/**
442
 * @brief Maps GPG stderr (which may include --status-fd 2 tokens) to a
443
 * user-friendly encryption error string.
444
 *
445
 * Checked in order: machine-readable [GNUPG:] status tokens first (locale-
446
 * independent), then case-insensitive substring fallbacks for GPG builds that
447
 * don't emit status tokens.
448
 *
449
 * @param err Raw stderr from GPG
450
 * @return Translated human-readable error, or empty string if not recognised
451
 */
452
namespace {
453

454
auto containsAny(const QString &str, const QStringList &patterns) -> bool {
31✔
455
  for (const QString &p : patterns) {
82✔
456
    if (str.contains(p)) {
58✔
457
      return true;
458
    }
459
  }
460
  return false;
461
}
462

463
/**
464
 * @brief Checks if str contains any of the patterns (case-insensitive).
465
 * @param str String to search in (will be lowercased once).
466
 * @param patterns List of patterns to search for (must be lowercase; caller
467
 * should convert patterns to lowercase before calling).
468
 * @return true if any pattern is found.
469
 */
470
auto containsAnyCaseInsensitive(const QString &str, const QStringList &patterns)
13✔
471
    -> bool {
472
  const QString lower = str.toLower();
473
  for (const QString &p : patterns) {
28✔
474
    if (lower.contains(p)) {
19✔
475
      return true;
476
    }
477
  }
478
  return false;
479
}
480

481
} // namespace
482

483
auto gpgErrorMessage(const QString &err) -> QString {
13✔
484
  // Machine-readable status tokens added by --status-fd 2
485
  if (containsAny(err, {QStringLiteral("[GNUPG:] KEYEXPIRED"),
65✔
486
                        QStringLiteral("[GNUPG:] INV_RECP 5 ")}))
13✔
487
    return QCoreApplication::translate(
488
        "Pass", "Encryption failed: GPG key has expired. Please renew or "
489
                "replace it.");
3✔
490
  if (containsAny(err, {QStringLiteral("[GNUPG:] KEYREVOKED"),
50✔
491
                        QStringLiteral("[GNUPG:] INV_RECP 4 ")}))
10✔
492
    return QCoreApplication::translate(
493
        "Pass", "Encryption failed: GPG key has been revoked.");
2✔
494
  if (containsAny(err, {QStringLiteral("[GNUPG:] NO_PUBKEY"),
40✔
495
                        QStringLiteral("[GNUPG:] INV_RECP")}))
8✔
496
    return QCoreApplication::translate(
497
        "Pass", "Encryption failed: recipient GPG key not found or invalid. "
498
                "Check that the key ID in .gpg-id is correct and imported.");
2✔
499
  if (err.contains(QStringLiteral("[GNUPG:] FAILURE")))
6✔
500
    return QCoreApplication::translate(
501
        "Pass", "Encryption failed. Check that your GPG key is valid.");
1✔
502

503
  // Locale-dependent fallbacks
504
  if (containsAnyCaseInsensitive(err, {QLatin1String("key has expired")}))
15✔
505
    return QCoreApplication::translate(
506
        "Pass", "Encryption failed: GPG key has expired. Please renew or "
507
                "replace it.");
1✔
508
  if (containsAnyCaseInsensitive(err, {QLatin1String("key has been revoked"),
16✔
509
                                       QLatin1String("revoked")}))
510
    return QCoreApplication::translate(
511
        "Pass", "Encryption failed: GPG key has been revoked.");
1✔
512
  if (containsAnyCaseInsensitive(err, {QLatin1String("no public key"),
15✔
513
                                       QLatin1String("unusable public key"),
514
                                       QLatin1String("no secret key")}))
515
    return QCoreApplication::translate(
516
        "Pass", "Encryption failed: recipient GPG key not found or invalid. "
517
                "Check that the key ID in .gpg-id is correct and imported.");
2✔
518
  if (containsAnyCaseInsensitive(err, {QLatin1String("encryption failed")}))
3✔
519
    return QCoreApplication::translate(
UNCOV
520
        "Pass", "Encryption failed. Check that your GPG key is valid.");
×
521

522
  return {};
523
}
×
524

525
/**
526
 * @brief Parses 'pass grep' raw output into (entry, matches) pairs.
527
 *
528
 * pass grep emits ANSI blue color (\x1B[94m) at the start of each entry
529
 * header line. This is checked before stripping ANSI so headers are detected
530
 * reliably regardless of locale.
531
 */
532
auto parseGrepOutput(const QString &rawOut)
12✔
533
    -> QList<QPair<QString, QStringList>> {
534
  static const QRegularExpression ansi(
535
      QStringLiteral(R"(\x1B\[[0-9;]*[a-zA-Z])"));
13✔
536
  QList<QPair<QString, QStringList>> results;
12✔
537
  QString currentEntry;
12✔
538
  QStringList currentMatches;
12✔
539
  for (const QString &rawLine : rawOut.split('\n')) {
69✔
540
    QString line = rawLine;
541
    line.remove('\r');
45✔
542
    line.remove(ansi);
45✔
543
    line = line.trimmed();
45✔
544
    // ANSI-colored header starts with the blue escape; plain-text fallback:
545
    // a non-indented line ending with ':' (pass grep format without color)
546
    bool isHeader = rawLine.startsWith(QStringLiteral("\x1B[94m")) ||
123✔
547
                    (!rawLine.startsWith(' ') && !rawLine.startsWith('\t') &&
91✔
548
                     line.endsWith(':') && !line.isEmpty());
76✔
549
    if (isHeader) {
45✔
550
      if (!currentEntry.isEmpty() && !currentMatches.isEmpty())
14✔
551
        results.append({currentEntry, currentMatches});
3✔
552
      currentEntry = line.endsWith(':') ? line.chopped(1) : line;
14✔
553
      currentMatches.clear();
14✔
554
    } else if (!currentEntry.isEmpty()) {
31✔
555
      if (!line.isEmpty())
29✔
556
        currentMatches << line;
557
    }
558
  }
559
  if (!currentEntry.isEmpty() && !currentMatches.isEmpty())
12✔
560
    results.append({currentEntry, currentMatches});
11✔
561
  return results;
12✔
562
}
563

564
/**
565
 * @brief Pass::processFinished reemits specific signal based on what process
566
 * has finished
567
 * @param id    id of Pass process that was scheduled and finished
568
 * @param exitCode  return code of a process
569
 * @param out   output generated by process(if capturing was requested, empty
570
 *              otherwise)
571
 * @param err   error output generated by process(if capturing was requested,
572
 *              or error occurred)
573
 */
574
void Pass::finished(int id, int exitCode, const QString &out,
18✔
575
                    const QString &err) {
576
  auto pid = static_cast<PROCESS>(id);
18✔
577

578
  if (exitCode != 0) {
18✔
579
    handleProcessError(pid, exitCode, out, err);
2✔
580
    return;
2✔
581
  }
582

583
  emitProcessFinishedSignal(pid, out, err);
16✔
584
}
585

586
void Pass::handleProcessError(PROCESS pid, int exitCode, const QString &out,
2✔
587
                              const QString &err) {
588
  if (pid == PASS_GREP) {
2✔
589
    handleGrepError(exitCode, err);
2✔
590
    return;
2✔
591
  }
592

UNCOV
593
  if (pid == PASS_INSERT) {
×
UNCOV
594
    const QString friendly = gpgErrorMessage(err);
×
UNCOV
595
    if (!friendly.isEmpty()) {
×
596
      emit processErrorExit(exitCode, formatInsertError(friendly, err));
×
597
      return;
598
    }
599
  }
600

UNCOV
601
  emit processErrorExit(exitCode, err);
×
602
}
603

604
void Pass::handleGrepError(int exitCode, const QString &err) {
2✔
605
  if (exitCode == 1) {
2✔
606
    emit finishedGrep({});
2✔
607
  } else {
608
    emit processErrorExit(exitCode, err);
1✔
609
    emit finishedGrep({});
2✔
610
  }
611
}
2✔
612

UNCOV
613
auto Pass::formatInsertError(const QString &friendly, const QString &err)
×
614
    -> QString {
UNCOV
615
  QStringList humanLines;
×
NEW
616
  for (const QString &line : err.split('\n')) {
×
617
    QString cleanedLine = line;
NEW
618
    cleanedLine.remove('\r');
×
NEW
619
    if (!cleanedLine.startsWith(QLatin1String("[GNUPG:]")))
×
620
      humanLines.append(cleanedLine);
621
  }
622
  const QString humanErr = humanLines.join('\n').trimmed();
×
623
  return humanErr.isEmpty() ? friendly : friendly + "\n\n" + humanErr;
×
624
}
625

626
void Pass::emitProcessFinishedSignal(PROCESS pid, const QString &out,
16✔
627
                                     const QString &err) {
628
  switch (pid) {
16✔
UNCOV
629
  case GIT_INIT:
×
UNCOV
630
    emit finishedGitInit(out, err);
×
UNCOV
631
    break;
×
632
  case GIT_PULL:
×
633
    emit finishedGitPull(out, err);
×
634
    break;
×
635
  case GIT_PUSH:
×
636
    emit finishedGitPush(out, err);
×
637
    break;
×
638
  case PASS_SHOW:
5✔
639
    emit finishedShow(out);
5✔
640
    break;
5✔
UNCOV
641
  case PASS_OTP_GENERATE:
×
UNCOV
642
    emit finishedOtpGenerate(out);
×
UNCOV
643
    break;
×
644
  case PASS_INSERT:
10✔
645
    emit finishedInsert(out, err);
10✔
646
    break;
10✔
UNCOV
647
  case PASS_REMOVE:
×
UNCOV
648
    emit finishedRemove(out, err);
×
UNCOV
649
    break;
×
650
  case PASS_INIT:
×
651
    emit finishedInit(out, err);
×
652
    break;
×
653
  case PASS_MOVE:
×
654
    emit finishedMove(out, err);
×
655
    break;
×
656
  case PASS_COPY:
×
657
    emit finishedCopy(out, err);
×
658
    break;
×
659
  case GPG_GENKEYS:
×
660
    emit finishedGenerateGPGKeys(out, err);
×
661
    break;
×
662
  case PASS_GREP:
1✔
663
    emit finishedGrep(parseGrepOutput(out));
1✔
664
    break;
1✔
665
  default:
666
#ifdef QT_DEBUG
667
    dbg() << "Unhandled process type" << pid;
668
#endif
669
    break;
670
  }
671
}
16✔
672

673
/**
674
 * @brief Pass::updateEnv update the execution environment (used when
675
 * switching profiles)
676
 */
677
// key must include the trailing '=' (e.g. "FOO="); env.filter() does substring
678
// matching so the '=' anchors the lookup to avoid collisions with longer names.
679
void Pass::setEnvVar(const QString &key, const QString &value) {
70✔
680
  Q_ASSERT(key.endsWith('='));
681
  const QStringList existing = env.filter(key);
70✔
682
  for (const QString &entry : existing)
75✔
683
    env.removeAll(entry);
684
  if (!value.isEmpty())
70✔
685
    env.append(key + value);
104✔
686
}
70✔
687

688
void Pass::updateEnv() {
16✔
689
  setEnvVar(QStringLiteral("PASSWORD_STORE_SIGNING_KEY="),
32✔
690
            QtPassSettings::getPassSigningKey());
32✔
691
  setEnvVar(QStringLiteral("PASSWORD_STORE_DIR="),
32✔
692
            QtPassSettings::getPassStore());
32✔
693

694
  PasswordConfiguration passConfig = QtPassSettings::getPasswordConfiguration();
16✔
695
  setEnvVar(QStringLiteral("PASSWORD_STORE_GENERATED_LENGTH="),
32✔
696
            QString::number(passConfig.length));
16✔
697

698
  int sel = passConfig.selected;
16✔
699
  if (sel < 0 || sel >= PasswordConfiguration::CHARSETS_COUNT)
16✔
700
    sel = PasswordConfiguration::ALLCHARS;
701
  QString charset = passConfig.Characters[sel];
702
  if (charset.isEmpty())
16✔
703
    charset = passConfig.Characters[PasswordConfiguration::ALLCHARS];
1✔
704
  setEnvVar(QStringLiteral("PASSWORD_STORE_CHARACTER_SET="), charset);
32✔
705

706
  exec.setEnvironment(env);
16✔
707
}
16✔
708

709
/**
710
 * @brief Pass::getGpgIdPath return gpgid file path for some file (folder).
711
 * @param for_file which file (folder) would you like the gpgid file path for.
712
 * @return path to the gpgid file.
713
 */
714
auto Pass::getGpgIdPath(const QString &for_file) -> QString {
32✔
715
  QString passStore =
716
      QDir::fromNativeSeparators(QtPassSettings::getPassStore());
64✔
717
  QString normalizedFile = QDir::fromNativeSeparators(for_file);
32✔
718
  QString fullPath = normalizedFile.startsWith(passStore)
32✔
719
                         ? normalizedFile
32✔
720
                         : passStore + "/" + normalizedFile;
27✔
721
  QDir gpgIdDir(QFileInfo(fullPath).absoluteDir());
32✔
722
  bool found = false;
723
  while (gpgIdDir.exists() && gpgIdDir.absolutePath().startsWith(passStore)) {
81✔
724
    if (QFile(gpgIdDir.absoluteFilePath(".gpg-id")).exists()) {
26✔
725
      found = true;
726
      break;
727
    }
728
    if (!gpgIdDir.cdUp()) {
12✔
729
      break;
730
    }
731
  }
732
  QString gpgIdPath(
733
      found ? gpgIdDir.absoluteFilePath(".gpg-id")
32✔
734
            : QDir(QtPassSettings::getPassStore()).filePath(".gpg-id"));
125✔
735

736
  return gpgIdPath;
32✔
737
}
32✔
738

739
/**
740
 * @brief Pass::getRecipientList return list of gpg-id's to encrypt for
741
 * @param for_file which file (folder) would you like recipients for
742
 * @return recipients gpg-id contents
743
 */
744
auto Pass::getRecipientList(const QString &for_file) -> QStringList {
17✔
745
  QFile gpgId(getGpgIdPath(for_file));
17✔
746
  if (!gpgId.open(QIODevice::ReadOnly | QIODevice::Text)) {
17✔
UNCOV
747
    return {};
×
748
  }
749
  QStringList recipients;
17✔
750
  while (!gpgId.atEnd()) {
42✔
751
    QString recipient(gpgId.readLine());
50✔
752
    recipient = recipient.split("#")[0].trimmed();
50✔
753
    if (!recipient.isEmpty() && Util::isValidKeyId(recipient)) {
25✔
754
      recipients += recipient;
755
    }
756
  }
757
  return recipients;
758
}
17✔
759

760
/**
761
 * @brief Pass::getRecipientString formatted string for use with GPG
762
 * @param for_file which file (folder) would you like recipients for
763
 * @param separator formating separator eg: " -r "
764
 * @param count
765
 * @return recipient string
766
 */
767
auto Pass::getRecipientString(const QString &for_file, const QString &separator,
2✔
768
                              int *count) -> QStringList {
769
  Q_UNUSED(separator)
770
  QStringList recipients = Pass::getRecipientList(for_file);
2✔
771
  if (count) {
2✔
772
    *count = recipients.size();
1✔
773
  }
774
  return recipients;
2✔
775
}
776

777
/* Copyright (C) 2017 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
778
 */
779

780
/**
781
 * @brief Generates a random number bounded by the given value.
782
 * @param bound Upper bound (exclusive)
783
 * @return Random number in range [0, bound)
784
 */
785
auto Pass::boundedRandom(quint32 bound) -> quint32 {
1,224✔
786
  if (bound < 2) {
1,224✔
787
    return 0;
788
  }
789

790
  quint32 randval;
791
  // Rejection-sampling threshold to avoid modulo bias:
792
  // In quint32 arithmetic, (1 + ~bound) wraps to (2^32 - bound), so
793
  // (1 + ~bound) % bound == 2^32 % bound.
794
  // Values randval < max_mod_bound are rejected; accepted values produce a
795
  // uniform distribution when reduced with (randval % bound).
796
  const quint32 max_mod_bound = (1 + ~bound) % bound;
1,224✔
797

798
  do {
799
    randval = QRandomGenerator::system()->generate();
800
  } while (randval < max_mod_bound);
1,224✔
801

802
  return randval % bound;
1,224✔
803
}
804

805
/**
806
 * @brief Generates a random password from the given charset.
807
 * @param charset Characters to use in the password
808
 * @param length Desired password length
809
 * @return Generated password string
810
 */
811
auto Pass::generateRandomPassword(const QString &charset, unsigned int length)
1,006✔
812
    -> QString {
813
  if (charset.isEmpty() || length == 0U) {
1,006✔
814
    return {};
815
  }
816
  QString out;
1,005✔
817
  for (unsigned int i = 0; i < length; ++i) {
2,229✔
818
    out.append(charset.at(static_cast<int>(
1,224✔
819
        boundedRandom(static_cast<quint32>(charset.length())))));
1,224✔
820
  }
821
  return out;
822
}
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