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

IJHack / QtPass / 23904903860

02 Apr 2026 02:15PM UTC coverage: 19.857% (-0.02%) from 19.873%
23904903860

Pull #892

github

web-flow
Merge 86cc3024f into 7a9fe4c42
Pull Request #892: fix: kill stale GPG agents before key generation

0 of 5 new or added lines in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

1030 of 5187 relevant lines covered (19.86%)

7.8 hits per line

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

33.16
/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 "helpers.h"
5
#include "qtpasssettings.h"
6
#include "util.h"
7
#include <QDir>
8
#include <QRandomGenerator>
9
#include <QRegularExpression>
10
#include <utility>
11

12
#ifdef QT_DEBUG
13
#include "debughelper.h"
14
#endif
15

16
using Enums::GIT_INIT;
17
using Enums::GIT_PULL;
18
using Enums::GIT_PUSH;
19
using Enums::GPG_GENKEYS;
20
using Enums::PASS_COPY;
21
using Enums::PASS_INIT;
22
using Enums::PASS_INSERT;
23
using Enums::PASS_MOVE;
24
using Enums::PASS_OTP_GENERATE;
25
using Enums::PASS_REMOVE;
26
using Enums::PASS_SHOW;
27

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

37
  // This was previously using direct QProcess signals.
38
  // The code now uses Executor instead of raw QProcess for better control.
39
  // connect(&process, SIGNAL(error(QProcess::ProcessError)), this,
40
  //        SIGNAL(error(QProcess::ProcessError)));
41

42
  connect(&exec, &Executor::starting, this, &Pass::startingExecuteWrapper);
11✔
43
  env.append("WSLENV=PASSWORD_STORE_DIR/p");
11✔
44
}
11✔
45

46
/**
47
 * @brief Executes a wrapper command.
48
 * @param id Process ID
49
 * @param app Application to execute
50
 * @param args Arguments
51
 * @param readStdout Whether to read stdout
52
 * @param readStderr Whether to read stderr
53
 */
54
void Pass::executeWrapper(PROCESS id, const QString &app,
×
55
                          const QStringList &args, bool readStdout,
56
                          bool readStderr) {
57
  executeWrapper(id, app, args, QString(), readStdout, readStderr);
×
58
}
×
59

60
void Pass::executeWrapper(PROCESS id, const QString &app,
×
61
                          const QStringList &args, QString input,
62
                          bool readStdout, bool readStderr) {
63
#ifdef QT_DEBUG
64
  dbg() << app << args;
65
#endif
66
  exec.execute(id, QtPassSettings::getPassStore(), app, args, std::move(input),
×
67
               readStdout, readStderr);
68
}
×
69

70
/**
71
 * @brief Initializes the pass wrapper environment.
72
 */
73
void Pass::init() {
1✔
74
#ifdef __APPLE__
75
  // If it exists, add the gpgtools to PATH
76
  if (QFile("/usr/local/MacGPG2/bin").exists())
77
    env.replaceInStrings("PATH=", "PATH=/usr/local/MacGPG2/bin:");
78
  // Add missing /usr/local/bin
79
  if (env.filter("/usr/local/bin").isEmpty())
80
    env.replaceInStrings("PATH=", "PATH=/usr/local/bin:");
81
#endif
82

83
  if (!QtPassSettings::getGpgHome().isEmpty()) {
2✔
84
    QDir absHome(QtPassSettings::getGpgHome());
×
85
    absHome.makeAbsolute();
×
86
    env << "GNUPGHOME=" + absHome.path();
×
87
  }
×
88
}
1✔
89

90
/**
91
 * @brief Pass::Generate use either pwgen or internal password
92
 * generator
93
 * @param length of the desired password
94
 * @param charset to use for generation
95
 * @return the password
96
 */
97
auto Pass::generatePassword(unsigned int length, const QString &charset)
1,004✔
98
    -> QString {
99
  QString passwd;
1,004✔
100
  if (QtPassSettings::isUsePwgen()) {
1,004✔
101
    // --secure goes first as it overrides --no-* otherwise
102
    QStringList args;
×
103
    args.append("-1");
×
104
    if (!QtPassSettings::isLessRandom()) {
×
105
      args.append("--secure");
×
106
    }
107
    args.append(QtPassSettings::isAvoidCapitals() ? "--no-capitalize"
×
108
                                                  : "--capitalize");
109
    args.append(QtPassSettings::isAvoidNumbers() ? "--no-numerals"
×
110
                                                 : "--numerals");
111
    if (QtPassSettings::isUseSymbols()) {
×
112
      args.append("--symbols");
×
113
    }
114
    args.append(QString::number(length));
×
115
    // executeBlocking returns 0 on success, non-zero on failure
116
    if (Executor::executeBlocking(QtPassSettings::getPwgenExecutable(), args,
×
117
                                  &passwd) == 0) {
118
      static const QRegularExpression literalNewLines{"[\\n\\r]"};
×
119
      passwd.remove(literalNewLines);
×
120
    } else {
121
      passwd.clear();
×
122
#ifdef QT_DEBUG
123
      qDebug() << __FILE__ << ":" << __LINE__ << "\t"
124
               << "pwgen fail";
125
#endif
126
      // Error is already handled by clearing passwd; no need for critical
127
      // signal here
128
    }
129
  } else {
130
    // Validate charset - if CUSTOM is selected but chars are empty,
131
    // fall back to ALLCHARS to prevent weak passwords (issue #780)
132
    QString effectiveCharset = charset;
133
    if (effectiveCharset.isEmpty()) {
1,004✔
134
      effectiveCharset = QtPassSettings::getPasswordConfiguration()
2✔
135
                             .Characters[PasswordConfiguration::ALLCHARS];
136
    }
137
    if (effectiveCharset.length() > 0) {
1,004✔
138
      passwd = generateRandomPassword(effectiveCharset, length);
2,008✔
139
    } else {
140
      emit critical(
×
141
          tr("No characters chosen"),
×
142
          tr("Can't generate password, there are no characters to choose from "
×
143
             "set in the configuration!"));
144
    }
145
  }
146
  return passwd;
1,004✔
147
}
148

149
/**
150
 * @brief Pass::gpgSupportsEd25519 check if GPG supports ed25519 (ECC)
151
 * GPG 2.1+ supports ed25519 which is much faster for key generation
152
 * @return true if ed25519 is supported
153
 */
154
bool Pass::gpgSupportsEd25519() {
3✔
155
  QString out, err;
3✔
156
  if (Executor::executeBlocking(QtPassSettings::getGpgExecutable(),
12✔
157
                                {"--version"}, &out, &err) != 0) {
158
    return false;
159
  }
160
  QRegularExpression versionRegex(R"(gpg \(GnuPG\) (\d+)\.(\d+))");
×
161
  QRegularExpressionMatch match = versionRegex.match(out);
×
162
  if (!match.hasMatch()) {
×
163
    return false;
164
  }
165
  int major = match.captured(1).toInt();
×
166
  int minor = match.captured(2).toInt();
×
167
  return major > 2 || (major == 2 && minor >= 1);
×
168
}
3✔
169

170
/**
171
 * @brief Pass::getDefaultKeyTemplate return default key generation template
172
 * Uses ed25519 if supported, otherwise falls back to RSA
173
 * @return GPG batch template string
174
 */
175
QString Pass::getDefaultKeyTemplate() {
1✔
176
  if (gpgSupportsEd25519()) {
1✔
177
    return QStringLiteral("%echo Generating a default key\n"
×
178
                          "Key-Type: EdDSA\n"
179
                          "Key-Curve: Ed25519\n"
180
                          "Subkey-Type: ECDH\n"
181
                          "Subkey-Curve: Curve25519\n"
182
                          "Name-Real: \n"
183
                          "Name-Comment: QtPass\n"
184
                          "Name-Email: \n"
185
                          "Expire-Date: 0\n"
186
                          "%no-protection\n"
187
                          "%commit\n"
188
                          "%echo done");
189
  }
190
  return QStringLiteral("%echo Generating a default key\n"
1✔
191
                        "Key-Type: RSA\n"
192
                        "Subkey-Type: RSA\n"
193
                        "Name-Real: \n"
194
                        "Name-Comment: QtPass\n"
195
                        "Name-Email: \n"
196
                        "Expire-Date: 0\n"
197
                        "%no-protection\n"
198
                        "%commit\n"
199
                        "%echo done");
200
}
201

202
/**
203
 * @brief Pass::GenerateGPGKeys internal gpg keypair generator . .
204
 * @param batch GnuPG style configuration string
205
 */
206
void Pass::GenerateGPGKeys(QString batch) {
×
207
  // Kill any stale GPG agents that might be holding locks on the key database
208
  // This helps avoid "database locked" timeouts during key generation
NEW
209
  QString gpgPath = QtPassSettings::getGpgExecutable();
×
NEW
210
  if (!gpgPath.isEmpty()) {
×
NEW
211
    Executor::executeBlocking(gpgPath, {"--kill", "gpg-agent"});
×
NEW
212
    Executor::executeBlocking(gpgPath, {"gpgconf", "--kill", "gpg-agent"});
×
213
  }
214

NEW
215
  executeWrapper(GPG_GENKEYS, gpgPath, {"--gen-key", "--no-tty", "--batch"},
×
216
                 std::move(batch));
UNCOV
217
}
×
218

219
/**
220
 * @brief Pass::listKeys list users
221
 * @param keystrings
222
 * @param secret list private keys
223
 * @return QList<UserInfo> users
224
 */
225
auto Pass::listKeys(QStringList keystrings, bool secret) -> QList<UserInfo> {
×
226
  QList<UserInfo> users;
×
227
  QStringList args = {"--no-tty", "--with-colons", "--with-fingerprint"};
×
228
  args.append(secret ? "--list-secret-keys" : "--list-keys");
×
229

230
  for (const QString &keystring : AS_CONST(keystrings)) {
×
231
    if (!keystring.isEmpty()) {
×
232
      args.append(keystring);
233
    }
234
  }
235
  QString p_out;
×
236
  if (Executor::executeBlocking(QtPassSettings::getGpgExecutable(), args,
×
237
                                &p_out) != 0) {
238
    return users;
239
  }
240
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
241
  const QStringList keys =
242
      p_out.split(Util::newLinesRegex(), Qt::SkipEmptyParts);
×
243
#else
244
  const QStringList keys =
245
      p_out.split(Util::newLinesRegex(), QString::SkipEmptyParts);
246
#endif
247
  UserInfo current_user;
×
248
  for (const QString &key : keys) {
×
249
    QStringList props = key.split(':');
×
250
    if (props.size() < 10) {
×
251
      continue;
252
    }
253
    if (props[0] == (secret ? "sec" : "pub")) {
×
254
      if (!current_user.key_id.isEmpty()) {
×
255
        users.append(current_user);
256
      }
257
      current_user = UserInfo();
×
258
      current_user.key_id = props[4];
×
259
      current_user.name = props[9].toUtf8();
×
260
      current_user.validity = props[1][0].toLatin1();
×
261
      current_user.created.setSecsSinceEpoch(props[5].toUInt());
×
262
      current_user.expiry.setSecsSinceEpoch(props[6].toUInt());
×
263
    } else if (current_user.name.isEmpty() && props[0] == "uid") {
×
264
      current_user.name = props[9];
×
265
    } else if ((props[0] == "fpr") && props[9].endsWith(current_user.key_id)) {
×
266
      current_user.key_id = props[9];
×
267
    }
268
  }
269
  if (!current_user.key_id.isEmpty()) {
×
270
    users.append(current_user);
271
  }
272
  return users;
273
}
×
274

275
/**
276
 * @brief Pass::listKeys list users
277
 * @param keystring
278
 * @param secret list private keys
279
 * @return QList<UserInfo> users
280
 */
281
auto Pass::listKeys(const QString &keystring, bool secret) -> QList<UserInfo> {
×
282
  return listKeys(QStringList(keystring), secret);
×
283
}
284

285
/**
286
 * @brief Pass::processFinished reemits specific signal based on what process
287
 * has finished
288
 * @param id    id of Pass process that was scheduled and finished
289
 * @param exitCode  return code of a process
290
 * @param out   output generated by process(if capturing was requested, empty
291
 *              otherwise)
292
 * @param err   error output generated by process(if capturing was requested,
293
 *              or error occurred)
294
 */
295
void Pass::finished(int id, int exitCode, const QString &out,
×
296
                    const QString &err) {
297
  auto pid = static_cast<PROCESS>(id);
298
  if (exitCode != 0) {
×
299
    emit processErrorExit(exitCode, err);
×
300
    return;
×
301
  }
302
  switch (pid) {
×
303
  case GIT_INIT:
×
304
    emit finishedGitInit(out, err);
×
305
    break;
×
306
  case GIT_PULL:
×
307
    emit finishedGitPull(out, err);
×
308
    break;
×
309
  case GIT_PUSH:
×
310
    emit finishedGitPush(out, err);
×
311
    break;
×
312
  case PASS_SHOW:
×
313
    emit finishedShow(out);
×
314
    break;
×
315
  case PASS_OTP_GENERATE:
×
316
    emit finishedOtpGenerate(out);
×
317
    break;
×
318
  case PASS_INSERT:
×
319
    emit finishedInsert(out, err);
×
320
    break;
×
321
  case PASS_REMOVE:
×
322
    emit finishedRemove(out, err);
×
323
    break;
×
324
  case PASS_INIT:
×
325
    emit finishedInit(out, err);
×
326
    break;
×
327
  case PASS_MOVE:
×
328
    emit finishedMove(out, err);
×
329
    break;
×
330
  case PASS_COPY:
×
331
    emit finishedCopy(out, err);
×
332
    break;
×
333
  case GPG_GENKEYS:
×
334
    emit finishedGenerateGPGKeys(out, err);
×
335
    break;
×
336
  default:
337
#ifdef QT_DEBUG
338
    dbg() << "Unhandled process type" << pid;
339
#endif
340
    break;
341
  }
342
}
343

344
/**
345
 * @brief Pass::updateEnv update the execution environment (used when
346
 * switching profiles)
347
 */
348
void Pass::updateEnv() {
×
349
  // put PASSWORD_STORE_SIGNING_KEY in env
350
  QStringList envSigningKey = env.filter("PASSWORD_STORE_SIGNING_KEY=");
×
351
  QString currentSigningKey = QtPassSettings::getPassSigningKey();
×
352
  if (envSigningKey.isEmpty()) {
×
353
    if (!currentSigningKey.isEmpty()) {
×
354
      // dbg()<< "Added
355
      // PASSWORD_STORE_SIGNING_KEY with" + currentSigningKey;
356
      env.append("PASSWORD_STORE_SIGNING_KEY=" + currentSigningKey);
×
357
    }
358
  } else {
359
    if (currentSigningKey.isEmpty()) {
×
360
      // dbg() << "Removed
361
      // PASSWORD_STORE_SIGNING_KEY";
362
      env.removeAll(envSigningKey.first());
363
    } else {
364
      // dbg()<< "Update
365
      // PASSWORD_STORE_SIGNING_KEY with " + currentSigningKey;
366
      env.replaceInStrings(envSigningKey.first(),
×
367
                           "PASSWORD_STORE_SIGNING_KEY=" + currentSigningKey);
×
368
    }
369
  }
370
  // put PASSWORD_STORE_DIR in env
371
  QStringList store = env.filter("PASSWORD_STORE_DIR=");
×
372
  if (store.isEmpty()) {
×
373
    // dbg()<< "Added
374
    // PASSWORD_STORE_DIR";
375
    env.append("PASSWORD_STORE_DIR=" + QtPassSettings::getPassStore());
×
376
  } else {
377
    // dbg()<< "Update
378
    // PASSWORD_STORE_DIR with " + passStore;
379
    env.replaceInStrings(store.first(), "PASSWORD_STORE_DIR=" +
×
380
                                            QtPassSettings::getPassStore());
×
381
  }
382
  exec.setEnvironment(env);
×
383
}
×
384

385
/**
386
 * @brief Pass::getGpgIdPath return gpgid file path for some file (folder).
387
 * @param for_file which file (folder) would you like the gpgid file path for.
388
 * @return path to the gpgid file.
389
 */
390
auto Pass::getGpgIdPath(const QString &for_file) -> QString {
8✔
391
  QString passStore =
392
      QDir::fromNativeSeparators(QtPassSettings::getPassStore());
16✔
393
  QString normalizedFile = QDir::fromNativeSeparators(for_file);
8✔
394
  QString fullPath = normalizedFile.startsWith(passStore)
8✔
395
                         ? normalizedFile
8✔
396
                         : passStore + "/" + normalizedFile;
6✔
397
  QDir gpgIdDir(QFileInfo(fullPath).absoluteDir());
8✔
398
  bool found = false;
399
  while (gpgIdDir.exists() && gpgIdDir.absolutePath().startsWith(passStore)) {
10✔
400
    if (QFile(gpgIdDir.absoluteFilePath(".gpg-id")).exists()) {
2✔
401
      found = true;
402
      break;
403
    }
404
    if (!gpgIdDir.cdUp()) {
×
405
      break;
406
    }
407
  }
408
  QString gpgIdPath(found ? gpgIdDir.absoluteFilePath(".gpg-id")
8✔
409
                          : QtPassSettings::getPassStore() + ".gpg-id");
22✔
410

411
  return gpgIdPath;
8✔
412
}
8✔
413

414
/**
415
 * @brief Pass::getRecipientList return list of gpg-id's to encrypt for
416
 * @param for_file which file (folder) would you like recipients for
417
 * @return recipients gpg-id contents
418
 */
419
auto Pass::getRecipientList(const QString &for_file) -> QStringList {
5✔
420
  QFile gpgId(getGpgIdPath(for_file));
5✔
421
  if (!gpgId.open(QIODevice::ReadOnly | QIODevice::Text)) {
5✔
422
    return {};
×
423
  }
424
  QStringList recipients;
5✔
425
  while (!gpgId.atEnd()) {
14✔
426
    QString recipient(gpgId.readLine());
18✔
427
    recipient = recipient.split("#")[0].trimmed();
18✔
428
    if (!recipient.isEmpty()) {
9✔
429
      recipients += recipient;
430
    }
431
  }
432
  return recipients;
433
}
5✔
434

435
/**
436
 * @brief Pass::getRecipientString formatted string for use with GPG
437
 * @param for_file which file (folder) would you like recipients for
438
 * @param separator formating separator eg: " -r "
439
 * @param count
440
 * @return recipient string
441
 */
442
auto Pass::getRecipientString(const QString &for_file, const QString &separator,
2✔
443
                              int *count) -> QStringList {
444
  Q_UNUSED(separator)
445
  QStringList recipients = Pass::getRecipientList(for_file);
2✔
446
  if (count) {
2✔
447
    *count = recipients.size();
1✔
448
  }
449
  return recipients;
2✔
450
}
451

452
/* Copyright (C) 2017 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
453
 */
454

455
/**
456
 * @brief Generates a random number bounded by the given value.
457
 * @param bound Upper bound (exclusive)
458
 * @return Random number in range [0, bound)
459
 */
460
auto Pass::boundedRandom(quint32 bound) -> quint32 {
1,160✔
461
  if (bound < 2) {
1,160✔
462
    return 0;
463
  }
464

465
  quint32 randval;
466
  const quint32 max_mod_bound = (1 + ~bound) % bound;
1,160✔
467

468
  do {
469
    randval = QRandomGenerator::system()->generate();
470
  } while (randval < max_mod_bound);
1,160✔
471

472
  return randval % bound;
1,160✔
473
}
474

475
/**
476
 * @brief Generates a random password from the given charset.
477
 * @param charset Characters to use in the password
478
 * @param length Desired password length
479
 * @return Generated password string
480
 */
481
auto Pass::generateRandomPassword(const QString &charset, unsigned int length)
1,004✔
482
    -> QString {
483
  QString out;
1,004✔
484
  for (unsigned int i = 0; i < length; ++i) {
2,164✔
485
    out.append(charset.at(static_cast<int>(
1,160✔
486
        boundedRandom(static_cast<quint32>(charset.length())))));
1,160✔
487
  }
488
  return out;
1,004✔
489
}
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