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

IJHack / QtPass / 24634672987

19 Apr 2026 05:16PM UTC coverage: 27.191% (-0.01%) from 27.201%
24634672987

push

github

web-flow
Merge pull request #1073 from IJHack/ai-findings-autofix/src-pass.cpp

refactor: rename variables for clarity and extract isGrepHeaderLine helper in pass.cpp

8 of 10 new or added lines in 1 file covered. (80.0%)

2 existing lines in 1 file now uncovered.

1582 of 5818 relevant lines covered (27.19%)

10.25 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

259
  QDir dir(gpgInfo.absolutePath());
1✔
260

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

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

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

330
} // namespace
331

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

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

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

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

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

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

386
  return {"gpgconf", {}};
387
}
8✔
388

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

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

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

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

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

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

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

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

483
} // namespace
484

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

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

525
  return {};
526
}
×
527

528
namespace {
529
auto isGrepHeaderLine(const QString &rawLine, const QString &trimmedLine)
45✔
530
    -> bool {
531
  // ANSI-colored header starts with the blue escape; plain-text fallback:
532
  // a non-indented line ending with ':' (pass grep format without color)
533
  return rawLine.startsWith(QStringLiteral("\x1B[94m")) ||
123✔
534
         (!rawLine.startsWith(' ') && !rawLine.startsWith('\t') &&
91✔
535
          trimmedLine.endsWith(':'));
119✔
536
}
537
} // namespace
538

539
/**
540
 * @brief Parses 'pass grep' raw output into (entry, matches) pairs.
541
 *
542
 * pass grep emits ANSI blue color (\x1B[94m) at the start of each entry
543
 * header line. This is checked before stripping ANSI so headers are detected
544
 * reliably regardless of locale.
545
 */
546
auto parseGrepOutput(const QString &rawOut)
12✔
547
    -> QList<QPair<QString, QStringList>> {
548
  static const QRegularExpression ansi(
549
      QStringLiteral(R"(\x1B\[[0-9;]*[a-zA-Z])"));
13✔
550
  QList<QPair<QString, QStringList>> results;
12✔
551
  QString currentEntry;
12✔
552
  QStringList currentMatches;
12✔
553
  for (const QString &rawLine : rawOut.split('\n')) {
69✔
554
    QString line = rawLine;
555
    line.remove('\r');
45✔
556
    line.remove(ansi);
45✔
557
    line = line.trimmed();
45✔
558
    const bool isHeader = isGrepHeaderLine(rawLine, line);
45✔
559
    if (isHeader) {
45✔
560
      if (!currentEntry.isEmpty() && !currentMatches.isEmpty())
14✔
561
        results.append({currentEntry, currentMatches});
3✔
562
      currentEntry = line.endsWith(':') ? line.chopped(1) : line;
14✔
563
      currentMatches.clear();
14✔
564
    } else if (!currentEntry.isEmpty()) {
31✔
565
      if (!line.isEmpty())
29✔
566
        currentMatches << line;
567
    }
568
  }
569
  if (!currentEntry.isEmpty() && !currentMatches.isEmpty())
12✔
570
    results.append({currentEntry, currentMatches});
11✔
571
  return results;
12✔
572
}
573

574
/**
575
 * @brief Pass::processFinished reemits specific signal based on what process
576
 * has finished
577
 * @param id    id of Pass process that was scheduled and finished
578
 * @param exitCode  return code of a process
579
 * @param out   output generated by process(if capturing was requested, empty
580
 *              otherwise)
581
 * @param err   error output generated by process(if capturing was requested,
582
 *              or error occurred)
583
 */
584
void Pass::finished(int id, int exitCode, const QString &out,
18✔
585
                    const QString &err) {
586
  auto pid = static_cast<PROCESS>(id);
18✔
587

588
  if (exitCode != 0) {
18✔
589
    handleProcessError(pid, exitCode, out, err);
2✔
590
    return;
2✔
591
  }
592

593
  emitProcessFinishedSignal(pid, out, err);
16✔
594
}
595

596
void Pass::handleProcessError(PROCESS pid, int exitCode, const QString &out,
2✔
597
                              const QString &err) {
598
  if (pid == PASS_GREP) {
2✔
599
    handleGrepError(exitCode, err);
2✔
600
    return;
2✔
601
  }
602

603
  if (pid == PASS_INSERT) {
×
604
    const QString friendly = gpgErrorMessage(err);
×
605
    if (!friendly.isEmpty()) {
×
606
      emit processErrorExit(exitCode, formatInsertError(friendly, err));
×
607
      return;
608
    }
609
  }
610

611
  emit processErrorExit(exitCode, err);
×
612
}
613

614
void Pass::handleGrepError(int exitCode, const QString &err) {
2✔
615
  if (exitCode == 1) {
2✔
616
    emit finishedGrep({});
2✔
617
  } else {
618
    emit processErrorExit(exitCode, err);
1✔
619
    emit finishedGrep({});
2✔
620
  }
621
}
2✔
622

623
auto Pass::formatInsertError(const QString &friendly, const QString &err)
×
624
    -> QString {
625
  QStringList humanLines;
×
626
  for (const QString &line : err.split('\n')) {
×
627
    QString cleanedLine = line;
628
    cleanedLine.remove('\r');
×
629
    if (!cleanedLine.startsWith(QLatin1String("[GNUPG:]")))
×
630
      humanLines.append(cleanedLine);
631
  }
632
  const QString humanErr = humanLines.join('\n').trimmed();
×
633
  return humanErr.isEmpty() ? friendly : friendly + "\n\n" + humanErr;
×
634
}
635

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

683
/**
684
 * @brief Pass::updateEnv update the execution environment (used when
685
 * switching profiles)
686
 */
687
// key must include the trailing '=' (e.g. "FOO="); env.filter() does substring
688
// matching so the '=' anchors the lookup to avoid collisions with longer names.
689
void Pass::setEnvVar(const QString &key, const QString &value) {
70✔
690
  Q_ASSERT(key.endsWith('='));
691
  const QStringList existing = env.filter(key);
70✔
692
  for (const QString &entry : existing)
75✔
693
    env.removeAll(entry);
694
  if (!value.isEmpty())
70✔
695
    env.append(key + value);
104✔
696
}
70✔
697

698
void Pass::updateEnv() {
16✔
699
  setEnvVar(QStringLiteral("PASSWORD_STORE_SIGNING_KEY="),
32✔
700
            QtPassSettings::getPassSigningKey());
32✔
701
  setEnvVar(QStringLiteral("PASSWORD_STORE_DIR="),
32✔
702
            QtPassSettings::getPassStore());
32✔
703

704
  PasswordConfiguration passConfig = QtPassSettings::getPasswordConfiguration();
16✔
705
  setEnvVar(QStringLiteral("PASSWORD_STORE_GENERATED_LENGTH="),
32✔
706
            QString::number(passConfig.length));
16✔
707

708
  int sel = passConfig.selected;
16✔
709
  if (sel < 0 || sel >= PasswordConfiguration::CHARSETS_COUNT)
16✔
710
    sel = PasswordConfiguration::ALLCHARS;
711
  QString charset = passConfig.Characters[sel];
712
  if (charset.isEmpty())
16✔
713
    charset = passConfig.Characters[PasswordConfiguration::ALLCHARS];
1✔
714
  setEnvVar(QStringLiteral("PASSWORD_STORE_CHARACTER_SET="), charset);
32✔
715

716
  exec.setEnvironment(env);
16✔
717
}
16✔
718

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

746
  return gpgIdPath;
32✔
747
}
32✔
748

749
/**
750
 * @brief Pass::getRecipientList return list of gpg-id's to encrypt for
751
 * @param for_file which file (folder) would you like recipients for
752
 * @return recipients gpg-id contents
753
 */
754
auto Pass::getRecipientList(const QString &for_file) -> QStringList {
17✔
755
  QFile gpgId(getGpgIdPath(for_file));
17✔
756
  if (!gpgId.open(QIODevice::ReadOnly | QIODevice::Text)) {
17✔
757
    return {};
×
758
  }
759
  QStringList recipients;
17✔
760
  while (!gpgId.atEnd()) {
42✔
761
    QString recipient(gpgId.readLine());
50✔
762
    recipient = recipient.split("#")[0].trimmed();
50✔
763
    if (!recipient.isEmpty() && Util::isValidKeyId(recipient)) {
25✔
764
      recipients += recipient;
765
    }
766
  }
767
  return recipients;
768
}
17✔
769

770
/**
771
 * @brief Pass::getRecipientString formatted string for use with GPG
772
 * @param for_file which file (folder) would you like recipients for
773
 * @param separator formating separator eg: " -r "
774
 * @param count
775
 * @return recipient string
776
 */
777
auto Pass::getRecipientString(const QString &for_file, const QString &separator,
2✔
778
                              int *count) -> QStringList {
779
  Q_UNUSED(separator)
780
  QStringList recipients = Pass::getRecipientList(for_file);
2✔
781
  if (count) {
2✔
782
    *count = recipients.size();
1✔
783
  }
784
  return recipients;
2✔
785
}
786

787
/* Copyright (C) 2017 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
788
 */
789

790
/**
791
 * @brief Generates a random number bounded by the given value.
792
 * @param bound Upper bound (exclusive)
793
 * @return Random number in range [0, bound)
794
 */
795
auto Pass::boundedRandom(quint32 bound) -> quint32 {
1,224✔
796
  if (bound < 2) {
1,224✔
797
    return 0;
798
  }
799

800
  quint32 randval;
801
  // Rejection-sampling threshold to avoid modulo bias:
802
  // In quint32 arithmetic, (1 + ~bound) wraps to (2^32 - bound), so
803
  // (1 + ~bound) % bound == 2^32 % bound.
804
  // Values randval < rejectionThreshold are rejected; accepted values
805
  // produce a uniform distribution when reduced with (randval % bound).
806
  const quint32 rejectionThreshold = (1 + ~bound) % bound;
1,224✔
807

808
  do {
809
    randval = QRandomGenerator::system()->generate();
810
  } while (randval < rejectionThreshold);
1,224✔
811

812
  return randval % bound;
1,224✔
813
}
814

815
/**
816
 * @brief Generates a random password from the given charset.
817
 * @param charset Characters to use in the password
818
 * @param length Desired password length
819
 * @return Generated password string
820
 */
821
auto Pass::generateRandomPassword(const QString &charset, unsigned int length)
1,006✔
822
    -> QString {
823
  if (charset.isEmpty() || length == 0U) {
1,006✔
824
    return {};
825
  }
826
  QString out;
1,005✔
827
  for (unsigned int i = 0; i < length; ++i) {
2,229✔
828
    out.append(charset.at(static_cast<int>(
1,224✔
829
        boundedRandom(static_cast<quint32>(charset.length())))));
1,224✔
830
  }
831
  return out;
832
}
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