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

IJHack / QtPass / 35158407238

16 Sep 2026 10:35PM UTC coverage: 72.336% (+0.05%) from 72.284%
35158407238

push

github

web-flow
Initialise new profiles without the active store's backend (#1782)

* Initialise new profiles without the active store's backend (#1774)

initializeNewProfiles() ran UsersDialog::accept() -> Pass::Init() and
GitInit() on the shared backend, whose settings snapshot, PASSWORD_STORE_DIR
and process queue belong to the active store: pass init nested the new
.gpg-id under the active store and git init ran there. Making the store
switch effective on that backend is not safe either (a late pass init
would re-encrypt the active store).

ProfileInit::initialise() now does the work synchronously and only inside
the new directory: writes .gpg-id (enabled keys, owner-only), signs it when
a signing key is configured, and with git runs init/add/commit through a
QProcess of its own. Existing *.gpg files are left alone and the user is
told to re-encrypt from within that profile. UsersDialog gained
setInitOnAccept(false) + selectedUsers() so it can hand the selection back
instead of calling Pass::Init.

Found on the way: a folder without .gpg-id made loadRecipients() call
listKeys() with an empty filter, which lists the whole keyring, so every
key came up pre-selected in the first-run wizard and for new profiles.

Closes #1774.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JuQsrHonihp1nARE7bzstc

* Profile init: per-profile signing key, WSL-safe gpg path, no OK without a recipient

- initializeNewProfiles() signs the new .gpg-id with the profile's own
  signingKey column, the way the profile switch applies it, not with the
  active store's key.
- ProfileInit::signGpgId() hands gpg a wslpath-translated file name when
  the executable is WSL-routed; Executor::translatePathForWsl() is the
  former ImitatePass helper made static so both can use it.
- UsersDialog: OK is disabled until a key is ticked and accept() refuses an
  empty selection. Now that a new store no longer comes up with the whole
  keyring pre-selec... (continued)

67 of 107 new or added lines in 7 files covered. (62.62%)

11 existing lines in 2 files now uncovered.

5478 of 7573 relevant lines covered (72.34%)

60.52 hits per line

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

72.37
/src/profileinit.cpp
1
// SPDX-FileCopyrightText: 2026 Anne Jan Brouwer
2
// SPDX-License-Identifier: GPL-3.0-or-later
3
#include "profileinit.h"
4
#include "appsettings.h"
5
#include "executor.h"
6
#include "userinfo.h"
7
#include <QDir>
8
#include <QFile>
9
#include <QFileInfo>
10
#include <QProcess>
11

12
auto ProfileInit::needsInit(const QString &path) -> bool {
7 ✔
13
  if (path.isEmpty()) {
7 ✔
14
    return false;
15
  }
16
  QDir dir(path);
6 ✔
17
  if (!dir.exists()) {
6 ✔
18
    return false;
19
  }
20
  return !dir.exists(".gpg-id");
5 ✔
21
}
6 ✔
22

23
auto ProfileInit::initialise(const QString &dir, const QList<UserInfo> &users,
5 ✔
24
                             const AppSettings &s, bool useGit, QString *note)
25
    -> bool {
26
  QString scratch;
5 ✔
27
  QString &out = note != nullptr ? *note : scratch;
5 ✔
28
  out.clear();
5 ✔
29

30
  const QDir folder(dir);
5 ✔
31
  if (!folder.exists() && !QDir().mkpath(folder.absolutePath())) {
8 ✔
NEW
32
    out = tr("Could not create %1.").arg(folder.absolutePath());
×
NEW
33
    return false;
×
34
  }
35
  const QString gpgIdFile = folder.filePath(QStringLiteral(".gpg-id"));
10 ✔
36
  if (!writeGpgId(gpgIdFile, users, &out)) {
5 ✔
37
    return false;
38
  }
39
  QString sigFile;
4 ✔
40
  if (!s.passSigningKey.trimmed().isEmpty()) {
8 ✔
NEW
41
    if (!signGpgId(gpgIdFile, s, &out)) {
×
42
      return false;
43
    }
NEW
44
    sigFile = gpgIdFile + QStringLiteral(".sig");
×
45
  }
46
  if (useGit &&
5 ✔
47
      !commitGpgId(folder.absolutePath(), gpgIdFile, sigFile, s, &out)) {
5 ✔
48
    return false;
49
  }
50

51
  const QStringList existing = folder.entryList(
4 ✔
52
      {QStringLiteral("*.gpg")}, QDir::Files | QDir::NoDotAndDotDot);
12 ✔
53
  if (!existing.isEmpty()) {
4 ✔
54
    out = tr("%1 already contains %n encrypted file(s); they were not "
1 ✔
55
             "re-encrypted. Switch to the profile and open Users to do that.",
56
             nullptr, static_cast<int>(existing.size()))
57
              .arg(folder.absolutePath());
2 ✔
58
  }
59
  return true;
60
}
5 ✔
61

62
auto ProfileInit::writeGpgId(const QString &gpgIdFile,
5 ✔
63
                             const QList<UserInfo> &users, QString *note)
64
    -> bool {
65
  QStringList ids;
5 ✔
66
  for (const UserInfo &user : users) {
15 ✔
67
    if (user.enabled) {
10 ✔
68
      ids << user.key_id;
4 ✔
69
    }
70
  }
71
  if (ids.isEmpty()) {
5 ✔
72
    *note = tr("No recipient selected; %1 was not written.").arg(gpgIdFile);
2 ✔
73
    return false;
1 ✔
74
  }
75
  QFile file(gpgIdFile);
4 ✔
76
  if (!file.open(QIODevice::WriteOnly | QIODevice::Text) ||
8 ✔
77
      file.write((ids.join(QLatin1Char('\n')) + QLatin1Char('\n')).toUtf8()) <
24 ✔
78
          0) {
NEW
79
    *note = tr("Could not write %1: %2").arg(gpgIdFile, file.errorString());
×
NEW
80
    return false;
×
81
  }
82
  file.close();
4 ✔
83
  // Same as ImitatePass::writeGpgIdFile: the recipient list leaks which keys
84
  // the store is encrypted to, so keep it owner-only where that means
85
  // anything.
86
  QFile::setPermissions(gpgIdFile, QFile::ReadOwner | QFile::WriteOwner);
4 ✔
87
  return true;
88
}
4 ✔
89

NEW
90
auto ProfileInit::signGpgId(const QString &gpgIdFile, const AppSettings &s,
×
91
                            QString *note) -> bool {
92
  // First key only, like ImitatePass::signGpgIdFile: repeated --default-key
93
  // options override each other.
94
  const QString key =
NEW
95
      s.passSigningKey.split(QLatin1Char(' '), Qt::SkipEmptyParts).first();
×
NEW
96
  QString err;
×
NEW
97
  const int rc = Executor::executeBlocking(
×
98
      s.gpgExecutable,
NEW
99
      {QStringLiteral("--default-key"), key, QStringLiteral("--yes"),
×
NEW
100
       QStringLiteral("--detach-sign"),
×
NEW
101
       Executor::translatePathForWsl(gpgIdFile, s.gpgExecutable)},
×
NEW
102
      QString(), nullptr, &err);
×
NEW
103
  if (rc != 0) {
×
104
    *note =
NEW
105
        tr("Could not sign %1 with %2: %3").arg(gpgIdFile, key, err.trimmed());
×
NEW
106
    return false;
×
107
  }
108
  return true;
NEW
109
}
×
110

111
auto ProfileInit::commitGpgId(const QString &dir, const QString &gpgIdFile,
1 ✔
112
                              const QString &sigFile, const AppSettings &s,
113
                              QString *note) -> bool {
114
  // A process of our own in the profile directory: nothing here may touch
115
  // the shared Executor or PASSWORD_STORE_DIR of the active store.
116
  QProcess git;
1 ✔
117
  git.setWorkingDirectory(dir);
1 ✔
118
  auto run = [&](const QStringList &args) -> bool {
3 ✔
119
    QString err;
3 ✔
120
    const int rc = Executor::executeBlocking(git, s.gitExecutable, args,
3 ✔
121
                                             QString(), nullptr, &err);
3 ✔
122
    if (rc != 0) {
3 ✔
123
      *note =
NEW
124
          tr("git %1 failed in %2: %3").arg(args.first(), dir, err.trimmed());
×
NEW
125
      return false;
×
126
    }
127
    return true;
128
  };
1 ✔
129
  QStringList files{QFileInfo(gpgIdFile).fileName()};
3 ✔
130
  if (!sigFile.isEmpty()) {
1 ✔
NEW
131
    files << QFileInfo(sigFile).fileName();
×
132
  }
133
  const QStringList add =
134
      QStringList{QStringLiteral("add"), QStringLiteral("--")} + files;
4 ✔
135
  const QStringList commit =
136
      QStringList{QStringLiteral("commit"), QStringLiteral("-m"),
7 ✔
137
                  QStringLiteral("Added .gpg-id using QtPass."),
1 ✔
138
                  QStringLiteral("--")} +
5 ✔
139
      files;
140
  return run({QStringLiteral("init")}) && run(add) && run(commit);
4 ✔
141
}
2 ✔
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