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

IJHack / QtPass / 24585183540

17 Apr 2026 08:25PM UTC coverage: 21.818% (+0.5%) from 21.329%
24585183540

push

github

web-flow
fix: set PASSWORD_STORE_* env vars and grey out pass-managed controls (#1032)

* fix: set PASSWORD_STORE_* env vars and grey out pass-managed controls (#265 #443)

- Pass::updateEnv() now sets PASSWORD_STORE_GENERATED_LENGTH and
  PASSWORD_STORE_CHARACTER_SET from QtPass settings so the pass binary
  honours the configured length and character set
- ConfigDialog::setGroupBoxState() disables password-generation
  controls (length, charset, pwgen options) when the pass binary is
  selected, since those are managed by pass in that mode
- passworddialog.ui spinBox_pwdLength maximum was already 255

Closes #265
Closes #443

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: isolate generateRandomPassword, boundedRandom, configIsValid tests from QSettings

Tests were reading isUsePwgen() and isUsePass() from the user's saved
QSettings, causing failures when those flags were enabled. Add RAII
guards to force the relevant settings to the expected values and restore
them on exit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address code review findings on password generation controls and env vars

configdialog.cpp:
- setGroupBoxState() now disables all password-gen controls in pass mode
  and delegates to on_checkBoxUsePwgen_clicked() when restoring native
  mode, instead of blindly re-enabling everything
- on_checkBoxUsePwgen_clicked() guards against pass mode so usePwgen()
  called after usePass(true) in the constructor cannot re-enable the
  pwgen sub-controls

pass.cpp:
- Clamp passConfig.selected before array access to guard against
  out-of-range values in QSettings
- Remove stale PASSWORD_STORE_CHARACTER_SET env entry when charset
  becomes empty, instead of leaving the old value in place
- Add PASSWORD_STORE_GENERATED_LENGTH and PASSWORD_STORE_CHARACTER_SET
  to WSLENV so they propagate into the WSL environment

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: replace duplicate inline rollback ... (continued)

28 of 46 new or added lines in 3 files covered. (60.87%)

1 existing line in 1 file now uncovered.

1176 of 5390 relevant lines covered (21.82%)

8.68 hits per line

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

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

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

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

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

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

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

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

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

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

105
  if (!QtPassSettings::getGpgHome().isEmpty()) {
2✔
106
    QDir absHome(QtPassSettings::getGpgHome());
×
107
    absHome.makeAbsolute();
×
108
    env << "GNUPGHOME=" + absHome.path();
×
109
  }
×
110
}
1✔
111

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

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

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

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

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

255
  QDir dir(gpgInfo.absolutePath());
1✔
256

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

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

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

326
} // namespace
327

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

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

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

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

372
  if (!first.contains('/') && !first.contains('\\')) {
2✔
373
    return {"gpgconf", {}};
374
  }
375

376
  QString gpgconfPath = findGpgconfInGpgDir(gpgPath);
1✔
377
  if (!gpgconfPath.isEmpty()) {
1✔
378
    return {gpgconfPath, {}};
×
379
  }
380

381
  return {"gpgconf", {}};
382
}
8✔
383

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

401
  executeWrapper(GPG_GENKEYS, gpgPath, {"--gen-key", "--no-tty", "--batch"},
×
402
                 std::move(batch));
403
}
×
404

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

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

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

438
/**
439
 * @brief Pass::processFinished reemits specific signal based on what process
440
 * has finished
441
 * @param id    id of Pass process that was scheduled and finished
442
 * @param exitCode  return code of a process
443
 * @param out   output generated by process(if capturing was requested, empty
444
 *              otherwise)
445
 * @param err   error output generated by process(if capturing was requested,
446
 *              or error occurred)
447
 */
448
void Pass::finished(int id, int exitCode, const QString &out,
×
449
                    const QString &err) {
450
  auto pid = static_cast<PROCESS>(id);
451
  if (exitCode != 0) {
×
452
    emit processErrorExit(exitCode, err);
×
453
    return;
×
454
  }
455
  switch (pid) {
×
456
  case GIT_INIT:
×
457
    emit finishedGitInit(out, err);
×
458
    break;
×
459
  case GIT_PULL:
×
460
    emit finishedGitPull(out, err);
×
461
    break;
×
462
  case GIT_PUSH:
×
463
    emit finishedGitPush(out, err);
×
464
    break;
×
465
  case PASS_SHOW:
×
466
    emit finishedShow(out);
×
467
    break;
×
468
  case PASS_OTP_GENERATE:
×
469
    emit finishedOtpGenerate(out);
×
470
    break;
×
471
  case PASS_INSERT:
×
472
    emit finishedInsert(out, err);
×
473
    break;
×
474
  case PASS_REMOVE:
×
475
    emit finishedRemove(out, err);
×
476
    break;
×
477
  case PASS_INIT:
×
478
    emit finishedInit(out, err);
×
479
    break;
×
480
  case PASS_MOVE:
×
481
    emit finishedMove(out, err);
×
482
    break;
×
483
  case PASS_COPY:
×
484
    emit finishedCopy(out, err);
×
485
    break;
×
486
  case GPG_GENKEYS:
×
487
    emit finishedGenerateGPGKeys(out, err);
×
488
    break;
×
489
  default:
490
#ifdef QT_DEBUG
491
    dbg() << "Unhandled process type" << pid;
492
#endif
493
    break;
494
  }
495
}
496

497
/**
498
 * @brief Pass::updateEnv update the execution environment (used when
499
 * switching profiles)
500
 */
501
// key must include the trailing '=' (e.g. "FOO="); env.filter() does substring
502
// matching so the '=' anchors the lookup to avoid collisions with longer names.
503
void Pass::setEnvVar(const QString &key, const QString &value) {
38✔
504
  Q_ASSERT(key.endsWith('='));
505
  const QStringList existing = env.filter(key);
38✔
506
  for (const QString &entry : std::as_const(existing))
43✔
507
    env.removeAll(entry);
508
  if (!value.isEmpty())
38✔
509
    env.append(key + value);
56✔
510
}
38✔
511

512
void Pass::updateEnv() {
8✔
513
  setEnvVar(QStringLiteral("PASSWORD_STORE_SIGNING_KEY="),
16✔
514
            QtPassSettings::getPassSigningKey());
16✔
515
  setEnvVar(QStringLiteral("PASSWORD_STORE_DIR="),
16✔
516
            QtPassSettings::getPassStore());
16✔
517

518
  PasswordConfiguration passConfig = QtPassSettings::getPasswordConfiguration();
8✔
519
  setEnvVar(QStringLiteral("PASSWORD_STORE_GENERATED_LENGTH="),
16✔
520
            QString::number(passConfig.length));
8✔
521

522
  int sel = passConfig.selected;
8✔
523
  if (sel < 0 || sel >= PasswordConfiguration::CHARSETS_COUNT)
8✔
524
    sel = PasswordConfiguration::ALLCHARS;
525
  QString charset = passConfig.Characters[sel];
526
  if (charset.isEmpty())
8✔
527
    charset = passConfig.Characters[PasswordConfiguration::ALLCHARS];
1✔
528
  setEnvVar(QStringLiteral("PASSWORD_STORE_CHARACTER_SET="), charset);
16✔
529

530
  exec.setEnvironment(env);
8✔
531
}
8✔
532

533
/**
534
 * @brief Pass::getGpgIdPath return gpgid file path for some file (folder).
535
 * @param for_file which file (folder) would you like the gpgid file path for.
536
 * @return path to the gpgid file.
537
 */
538
auto Pass::getGpgIdPath(const QString &for_file) -> QString {
9✔
539
  QString passStore =
540
      QDir::fromNativeSeparators(QtPassSettings::getPassStore());
18✔
541
  QString normalizedFile = QDir::fromNativeSeparators(for_file);
9✔
542
  QString fullPath = normalizedFile.startsWith(passStore)
9✔
543
                         ? normalizedFile
9✔
544
                         : passStore + "/" + normalizedFile;
7✔
545
  QDir gpgIdDir(QFileInfo(fullPath).absoluteDir());
9✔
546
  bool found = false;
547
  while (gpgIdDir.exists() && gpgIdDir.absolutePath().startsWith(passStore)) {
11✔
548
    if (QFile(gpgIdDir.absoluteFilePath(".gpg-id")).exists()) {
2✔
549
      found = true;
550
      break;
551
    }
552
    if (!gpgIdDir.cdUp()) {
×
553
      break;
554
    }
555
  }
556
  QString gpgIdPath(
557
      found ? gpgIdDir.absoluteFilePath(".gpg-id")
9✔
558
            : QDir(QtPassSettings::getPassStore()).filePath(".gpg-id"));
33✔
559

560
  return gpgIdPath;
9✔
561
}
9✔
562

563
/**
564
 * @brief Pass::getRecipientList return list of gpg-id's to encrypt for
565
 * @param for_file which file (folder) would you like recipients for
566
 * @return recipients gpg-id contents
567
 */
568
auto Pass::getRecipientList(const QString &for_file) -> QStringList {
6✔
569
  QFile gpgId(getGpgIdPath(for_file));
6✔
570
  if (!gpgId.open(QIODevice::ReadOnly | QIODevice::Text)) {
6✔
571
    return {};
×
572
  }
573
  QStringList recipients;
6✔
574
  while (!gpgId.atEnd()) {
20✔
575
    QString recipient(gpgId.readLine());
28✔
576
    recipient = recipient.split("#")[0].trimmed();
28✔
577
    if (!recipient.isEmpty() && Util::isValidKeyId(recipient)) {
14✔
578
      recipients += recipient;
579
    }
580
  }
581
  return recipients;
582
}
6✔
583

584
/**
585
 * @brief Pass::getRecipientString formatted string for use with GPG
586
 * @param for_file which file (folder) would you like recipients for
587
 * @param separator formating separator eg: " -r "
588
 * @param count
589
 * @return recipient string
590
 */
591
auto Pass::getRecipientString(const QString &for_file, const QString &separator,
2✔
592
                              int *count) -> QStringList {
593
  Q_UNUSED(separator)
594
  QStringList recipients = Pass::getRecipientList(for_file);
2✔
595
  if (count) {
2✔
596
    *count = recipients.size();
1✔
597
  }
598
  return recipients;
2✔
599
}
600

601
/* Copyright (C) 2017 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
602
 */
603

604
/**
605
 * @brief Generates a random number bounded by the given value.
606
 * @param bound Upper bound (exclusive)
607
 * @return Random number in range [0, bound)
608
 */
609
auto Pass::boundedRandom(quint32 bound) -> quint32 {
1,224✔
610
  if (bound < 2) {
1,224✔
611
    return 0;
612
  }
613

614
  quint32 randval;
615
  // Rejection-sampling threshold to avoid modulo bias:
616
  // In quint32 arithmetic, (1 + ~bound) wraps to (2^32 - bound), so
617
  // (1 + ~bound) % bound == 2^32 % bound.
618
  // Values randval < max_mod_bound are rejected; accepted values produce a
619
  // uniform distribution when reduced with (randval % bound).
620
  const quint32 max_mod_bound = (1 + ~bound) % bound;
1,224✔
621

622
  do {
623
    randval = QRandomGenerator::system()->generate();
624
  } while (randval < max_mod_bound);
1,224✔
625

626
  return randval % bound;
1,224✔
627
}
628

629
/**
630
 * @brief Generates a random password from the given charset.
631
 * @param charset Characters to use in the password
632
 * @param length Desired password length
633
 * @return Generated password string
634
 */
635
auto Pass::generateRandomPassword(const QString &charset, unsigned int length)
1,006✔
636
    -> QString {
637
  if (charset.isEmpty() || length == 0U) {
1,006✔
638
    return {};
639
  }
640
  QString out;
1,005✔
641
  for (unsigned int i = 0; i < length; ++i) {
2,229✔
642
    out.append(charset.at(static_cast<int>(
1,224✔
643
        boundedRandom(static_cast<quint32>(charset.length())))));
1,224✔
644
  }
645
  return out;
646
}
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