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

IJHack / QtPass / 23663436736

27 Mar 2026 07:17PM UTC coverage: 18.536% (+0.1%) from 18.391%
23663436736

Pull #790

github

web-flow
Merge a3914e3bb into ec74b2b97
Pull Request #790: feat: use ed25519 for GPG key generation when available

7 of 15 new or added lines in 1 file covered. (46.67%)

2 existing lines in 1 file now uncovered.

927 of 5001 relevant lines covered (18.54%)

7.52 hits per line

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

33.88
/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()) {
10✔
32
  connect(&exec,
10✔
33
          static_cast<void (Executor::*)(int, int, const QString &,
34
                                         const QString &)>(&Executor::finished),
35
          this, &Pass::finished);
10✔
36

37
  // TODO(bezet): stop using process
38
  // connect(&process, SIGNAL(error(QProcess::ProcessError)), this,
39
  //        SIGNAL(error(QProcess::ProcessError)));
40

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

45
void Pass::executeWrapper(PROCESS id, const QString &app,
×
46
                          const QStringList &args, bool readStdout,
47
                          bool readStderr) {
48
  executeWrapper(id, app, args, QString(), readStdout, readStderr);
×
49
}
×
50

51
void Pass::executeWrapper(PROCESS id, const QString &app,
×
52
                          const QStringList &args, QString input,
53
                          bool readStdout, bool readStderr) {
54
#ifdef QT_DEBUG
55
  dbg() << app << args;
56
#endif
57
  exec.execute(id, QtPassSettings::getPassStore(), app, args, std::move(input),
×
58
               readStdout, readStderr);
59
}
×
60

61
void Pass::init() {
1✔
62
#ifdef __APPLE__
63
  // If it exists, add the gpgtools to PATH
64
  if (QFile("/usr/local/MacGPG2/bin").exists())
65
    env.replaceInStrings("PATH=", "PATH=/usr/local/MacGPG2/bin:");
66
  // Add missing /usr/local/bin
67
  if (env.filter("/usr/local/bin").isEmpty())
68
    env.replaceInStrings("PATH=", "PATH=/usr/local/bin:");
69
#endif
70

71
  if (!QtPassSettings::getGpgHome().isEmpty()) {
2✔
72
    QDir absHome(QtPassSettings::getGpgHome());
×
73
    absHome.makeAbsolute();
×
74
    env << "GNUPGHOME=" + absHome.path();
×
75
  }
×
76
}
1✔
77

78
/**
79
 * @brief Pass::Generate use either pwgen or internal password
80
 * generator
81
 * @param length of the desired password
82
 * @param charset to use for generation
83
 * @return the password
84
 */
85
auto Pass::Generate_b(unsigned int length, const QString &charset) -> QString {
1,004✔
86
  QString passwd;
1,004✔
87
  if (QtPassSettings::isUsePwgen()) {
1,004✔
88
    // --secure goes first as it overrides --no-* otherwise
89
    QStringList args;
×
90
    args.append("-1");
×
91
    if (!QtPassSettings::isLessRandom()) {
×
92
      args.append("--secure");
×
93
    }
94
    args.append(QtPassSettings::isAvoidCapitals() ? "--no-capitalize"
×
95
                                                  : "--capitalize");
96
    args.append(QtPassSettings::isAvoidNumbers() ? "--no-numerals"
×
97
                                                 : "--numerals");
98
    if (QtPassSettings::isUseSymbols()) {
×
99
      args.append("--symbols");
×
100
    }
101
    args.append(QString::number(length));
×
102
    // TODO(bezet): try-catch here(2 statuses to merge o_O)
103
    if (Executor::executeBlocking(QtPassSettings::getPwgenExecutable(), args,
×
104
                                  &passwd) == 0) {
105
      static const QRegularExpression literalNewLines{"[\\n\\r]"};
×
106
      passwd.remove(literalNewLines);
×
107
    } else {
108
      passwd.clear();
×
109
#ifdef QT_DEBUG
110
      qDebug() << __FILE__ << ":" << __LINE__ << "\t"
111
               << "pwgen fail";
112
#endif
113
      // TODO(bezet): emit critical ?
114
    }
115
  } else {
116
    // Validate charset - if CUSTOM is selected but chars are empty,
117
    // fall back to ALLCHARS to prevent weak passwords (issue #780)
118
    QString effectiveCharset = charset;
119
    if (effectiveCharset.isEmpty()) {
1,004✔
120
      effectiveCharset = QtPassSettings::getPasswordConfiguration()
2✔
121
                             .Characters[PasswordConfiguration::ALLCHARS];
122
    }
123
    if (effectiveCharset.length() > 0) {
1,004✔
124
      passwd = generateRandomPassword(effectiveCharset, length);
2,008✔
125
    } else {
126
      emit critical(
×
127
          tr("No characters chosen"),
×
128
          tr("Can't generate password, there are no characters to choose from "
×
129
             "set in the configuration!"));
130
    }
131
  }
132
  return passwd;
1,004✔
133
}
134

135
/**
136
 * @brief Pass::gpgSupportsEd25519 check if GPG supports ed25519 (ECC)
137
 * GPG 2.1+ supports ed25519 which is much faster for key generation
138
 * @return true if ed25519 is supported
139
 */
140
bool Pass::gpgSupportsEd25519() {
1✔
141
  QString out, err;
1✔
142
  if (Executor::executeBlocking(QtPassSettings::getGpgExecutable(),
4✔
143
                                {"--version"}, &out, &err) != 0) {
144
    return false;
145
  }
146
  QRegularExpression versionRegex(R"(gpg \(GnuPG\) (\d+)\.(\d+))");
2✔
147
  QRegularExpressionMatch match = versionRegex.match(out);
1✔
148
  if (!match.hasMatch()) {
1✔
149
    return false;
150
  }
NEW
151
  int major = match.captured(1).toInt();
×
NEW
152
  int minor = match.captured(2).toInt();
×
NEW
153
  return major > 2 || (major == 2 && minor >= 1);
×
154
}
2✔
155

156
/**
157
 * @brief Pass::GenerateGPGKeys internal gpg keypair generator . .
158
 * @param batch GnuPG style configuration string
159
 */
160
void Pass::GenerateGPGKeys(QString batch) {
×
NEW
161
  QStringList args = {"--gen-key", "--no-tty", "--batch"};
×
NEW
162
  if (gpgSupportsEd25519()) {
×
NEW
163
    args.prepend("--default-new-key-algo");
×
NEW
164
    args.prepend("ed25519");
×
165
  }
NEW
166
  executeWrapper(GPG_GENKEYS, QtPassSettings::getGpgExecutable(), args,
×
167
                 std::move(batch));
UNCOV
168
}
×
169

170
/**
171
 * @brief Pass::listKeys list users
172
 * @param keystrings
173
 * @param secret list private keys
174
 * @return QList<UserInfo> users
175
 */
176
auto Pass::listKeys(QStringList keystrings, bool secret) -> QList<UserInfo> {
×
177
  QList<UserInfo> users;
×
178
  QStringList args = {"--no-tty", "--with-colons", "--with-fingerprint"};
×
179
  args.append(secret ? "--list-secret-keys" : "--list-keys");
×
180

181
  for (const QString &keystring : AS_CONST(keystrings)) {
×
182
    if (!keystring.isEmpty()) {
×
183
      args.append(keystring);
184
    }
185
  }
186
  QString p_out;
×
187
  if (Executor::executeBlocking(QtPassSettings::getGpgExecutable(), args,
×
188
                                &p_out) != 0) {
189
    return users;
190
  }
191
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
192
  const QStringList keys =
193
      p_out.split(Util::newLinesRegex(), Qt::SkipEmptyParts);
×
194
#else
195
  const QStringList keys =
196
      p_out.split(Util::newLinesRegex(), QString::SkipEmptyParts);
197
#endif
198
  UserInfo current_user;
×
199
  for (const QString &key : keys) {
×
200
    QStringList props = key.split(':');
×
201
    if (props.size() < 10) {
×
202
      continue;
203
    }
204
    if (props[0] == (secret ? "sec" : "pub")) {
×
205
      if (!current_user.key_id.isEmpty()) {
×
206
        users.append(current_user);
207
      }
208
      current_user = UserInfo();
×
209
      current_user.key_id = props[4];
×
210
      current_user.name = props[9].toUtf8();
×
211
      current_user.validity = props[1][0].toLatin1();
×
212
      current_user.created.setSecsSinceEpoch(props[5].toUInt());
×
213
      current_user.expiry.setSecsSinceEpoch(props[6].toUInt());
×
214
    } else if (current_user.name.isEmpty() && props[0] == "uid") {
×
215
      current_user.name = props[9];
×
216
    } else if ((props[0] == "fpr") && props[9].endsWith(current_user.key_id)) {
×
217
      current_user.key_id = props[9];
×
218
    }
219
  }
UNCOV
220
  if (!current_user.key_id.isEmpty()) {
×
221
    users.append(current_user);
222
  }
223
  return users;
224
}
×
225

226
/**
227
 * @brief Pass::listKeys list users
228
 * @param keystring
229
 * @param secret list private keys
230
 * @return QList<UserInfo> users
231
 */
232
auto Pass::listKeys(const QString &keystring, bool secret) -> QList<UserInfo> {
×
233
  return listKeys(QStringList(keystring), secret);
×
234
}
235

236
/**
237
 * @brief Pass::processFinished reemits specific signal based on what process
238
 * has finished
239
 * @param id    id of Pass process that was scheduled and finished
240
 * @param exitCode  return code of a process
241
 * @param out   output generated by process(if capturing was requested, empty
242
 *              otherwise)
243
 * @param err   error output generated by process(if capturing was requested,
244
 *              or error occurred)
245
 */
246
void Pass::finished(int id, int exitCode, const QString &out,
×
247
                    const QString &err) {
248
  auto pid = static_cast<PROCESS>(id);
249
  if (exitCode != 0) {
×
250
    emit processErrorExit(exitCode, err);
×
251
    return;
×
252
  }
253
  switch (pid) {
×
254
  case GIT_INIT:
×
255
    emit finishedGitInit(out, err);
×
256
    break;
×
257
  case GIT_PULL:
×
258
    emit finishedGitPull(out, err);
×
259
    break;
×
260
  case GIT_PUSH:
×
261
    emit finishedGitPush(out, err);
×
262
    break;
×
263
  case PASS_SHOW:
×
264
    emit finishedShow(out);
×
265
    break;
×
266
  case PASS_OTP_GENERATE:
×
267
    emit finishedOtpGenerate(out);
×
268
    break;
×
269
  case PASS_INSERT:
×
270
    emit finishedInsert(out, err);
×
271
    break;
×
272
  case PASS_REMOVE:
×
273
    emit finishedRemove(out, err);
×
274
    break;
×
275
  case PASS_INIT:
×
276
    emit finishedInit(out, err);
×
277
    break;
×
278
  case PASS_MOVE:
×
279
    emit finishedMove(out, err);
×
280
    break;
×
281
  case PASS_COPY:
×
282
    emit finishedCopy(out, err);
×
283
    break;
×
284
  case GPG_GENKEYS:
×
285
    emit finishedGenerateGPGKeys(out, err);
×
286
    break;
×
287
  default:
288
#ifdef QT_DEBUG
289
    dbg() << "Unhandled process type" << pid;
290
#endif
291
    break;
292
  }
293
}
294

295
/**
296
 * @brief Pass::updateEnv update the execution environment (used when
297
 * switching profiles)
298
 */
299
void Pass::updateEnv() {
×
300
  // put PASSWORD_STORE_SIGNING_KEY in env
301
  QStringList envSigningKey = env.filter("PASSWORD_STORE_SIGNING_KEY=");
×
302
  QString currentSigningKey = QtPassSettings::getPassSigningKey();
×
303
  if (envSigningKey.isEmpty()) {
×
304
    if (!currentSigningKey.isEmpty()) {
×
305
      // dbg()<< "Added
306
      // PASSWORD_STORE_SIGNING_KEY with" + currentSigningKey;
307
      env.append("PASSWORD_STORE_SIGNING_KEY=" + currentSigningKey);
×
308
    }
309
  } else {
310
    if (currentSigningKey.isEmpty()) {
×
311
      // dbg() << "Removed
312
      // PASSWORD_STORE_SIGNING_KEY";
313
      env.removeAll(envSigningKey.first());
314
    } else {
315
      // dbg()<< "Update
316
      // PASSWORD_STORE_SIGNING_KEY with " + currentSigningKey;
317
      env.replaceInStrings(envSigningKey.first(),
×
318
                           "PASSWORD_STORE_SIGNING_KEY=" + currentSigningKey);
×
319
    }
320
  }
321
  // put PASSWORD_STORE_DIR in env
322
  QStringList store = env.filter("PASSWORD_STORE_DIR=");
×
323
  if (store.isEmpty()) {
×
324
    // dbg()<< "Added
325
    // PASSWORD_STORE_DIR";
326
    env.append("PASSWORD_STORE_DIR=" + QtPassSettings::getPassStore());
×
327
  } else {
328
    // dbg()<< "Update
329
    // PASSWORD_STORE_DIR with " + passStore;
330
    env.replaceInStrings(store.first(), "PASSWORD_STORE_DIR=" +
×
331
                                            QtPassSettings::getPassStore());
×
332
  }
333
  exec.setEnvironment(env);
×
334
}
×
335

336
/**
337
 * @brief Pass::getGpgIdPath return gpgid file path for some file (folder).
338
 * @param for_file which file (folder) would you like the gpgid file path for.
339
 * @return path to the gpgid file.
340
 */
341
auto Pass::getGpgIdPath(const QString &for_file) -> QString {
8✔
342
  QString passStore =
343
      QDir::fromNativeSeparators(QtPassSettings::getPassStore());
16✔
344
  QString normalizedFile = QDir::fromNativeSeparators(for_file);
8✔
345
  QString fullPath = normalizedFile.startsWith(passStore)
8✔
346
                         ? normalizedFile
8✔
347
                         : passStore + "/" + normalizedFile;
6✔
348
  QDir gpgIdDir(QFileInfo(fullPath).absoluteDir());
8✔
349
  bool found = false;
350
  while (gpgIdDir.exists() && gpgIdDir.absolutePath().startsWith(passStore)) {
10✔
351
    if (QFile(gpgIdDir.absoluteFilePath(".gpg-id")).exists()) {
2✔
352
      found = true;
353
      break;
354
    }
355
    if (!gpgIdDir.cdUp()) {
×
356
      break;
357
    }
358
  }
359
  QString gpgIdPath(found ? gpgIdDir.absoluteFilePath(".gpg-id")
8✔
360
                          : QtPassSettings::getPassStore() + ".gpg-id");
22✔
361

362
  return gpgIdPath;
8✔
363
}
8✔
364

365
/**
366
 * @brief Pass::getRecipientList return list of gpg-id's to encrypt for
367
 * @param for_file which file (folder) would you like recepients for
368
 * @return recepients gpg-id contents
369
 */
370
auto Pass::getRecipientList(const QString &for_file) -> QStringList {
5✔
371
  QFile gpgId(getGpgIdPath(for_file));
5✔
372
  if (!gpgId.open(QIODevice::ReadOnly | QIODevice::Text)) {
5✔
373
    return {};
×
374
  }
375
  QStringList recipients;
5✔
376
  while (!gpgId.atEnd()) {
14✔
377
    QString recipient(gpgId.readLine());
18✔
378
    recipient = recipient.split("#")[0].trimmed();
18✔
379
    if (!recipient.isEmpty()) {
9✔
380
      recipients += recipient;
381
    }
382
  }
383
  return recipients;
384
}
5✔
385

386
/**
387
 * @brief Pass::getRecipientString formated string for use with GPG
388
 * @param for_file which file (folder) would you like recepients for
389
 * @param separator formating separator eg: " -r "
390
 * @param count
391
 * @return recepient string
392
 */
393
auto Pass::getRecipientString(const QString &for_file, const QString &separator,
2✔
394
                              int *count) -> QStringList {
395
  Q_UNUSED(separator)
396
  QStringList recipients = Pass::getRecipientList(for_file);
2✔
397
  if (count) {
2✔
398
    *count = recipients.size();
1✔
399
  }
400
  return recipients;
2✔
401
}
402

403
/* Copyright (C) 2017 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
404
 */
405

406
auto Pass::boundedRandom(quint32 bound) -> quint32 {
1,160✔
407
  if (bound < 2) {
1,160✔
408
    return 0;
409
  }
410

411
  quint32 randval;
412
  const quint32 max_mod_bound = (1 + ~bound) % bound;
1,160✔
413

414
  do {
415
    randval = QRandomGenerator::system()->generate();
416
  } while (randval < max_mod_bound);
1,160✔
417

418
  return randval % bound;
1,160✔
419
}
420

421
auto Pass::generateRandomPassword(const QString &charset, unsigned int length)
1,004✔
422
    -> QString {
423
  QString out;
1,004✔
424
  for (unsigned int i = 0; i < length; ++i) {
2,164✔
425
    out.append(charset.at(static_cast<int>(
1,160✔
426
        boundedRandom(static_cast<quint32>(charset.length())))));
1,160✔
427
  }
428
  return out;
1,004✔
429
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc