QtPass 2.0.0
Developer documentation
Loading...
Searching...
No Matches
profileinit.cpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2026 Anne Jan Brouwer
2// SPDX-License-Identifier: GPL-3.0-or-later
8#include "profileinit.h"
9#include "appsettings.h"
10#include "executor.h"
11#include "gpgidgeneration.h"
12#include "gpgidsigner.h"
13#include "qtpasslogging.h"
14#include "userinfo.h"
15#include "util.h"
16#include <QDir>
17#include <QFile>
18#include <QFileInfo>
19#include <QProcess>
20#include <algorithm>
21
22namespace {
28auto withGpgHome(QProcessEnvironment env, const AppSettings &s)
29 -> QProcessEnvironment {
30 if (!s.gpgHome.isEmpty()) {
31 QDir home(s.gpgHome);
32 home.makeAbsolute();
33 if (home.exists()) {
34 env.insert(QStringLiteral("GNUPGHOME"), home.path());
35 }
36 }
37 return env;
38}
39
45auto gitEnvironment(const QString &dir, const AppSettings &s)
46 -> QProcessEnvironment {
47 QProcessEnvironment env =
48 withGpgHome(QProcessEnvironment::systemEnvironment(), s);
49 // As for the active store: the paths handed to git are names, not
50 // patterns (see Pass::updateEnv()).
51 env.insert(QStringLiteral("GIT_LITERAL_PATHSPECS"), QStringLiteral("1"));
52 if (!s.gitUseParentRepository) {
53 for (const auto &[name, value] :
54 Util::gitStoreVariables(dir, s.gitExecutable)) {
55 env.insert(name, value);
56 }
57 }
58 return env;
59}
63auto commitArgs(QProcess &git, const AppSettings &s) -> QStringList {
64 QString out;
65 const bool sign =
67 git, s.gitExecutable,
68 {QStringLiteral("config"), QStringLiteral("--bool"),
69 QStringLiteral("--get"), QStringLiteral("pass.signcommits")},
70 QString(), &out, nullptr) == 0 &&
71 out.trimmed() == QLatin1String("true");
72 return sign ? QStringList{QStringLiteral("commit"), QStringLiteral("-S")}
73 : QStringList{QStringLiteral("commit")};
74}
75} // namespace
76
77auto ProfileInit::gpgEnvironment(const AppSettings &s) -> QProcessEnvironment {
78 return withGpgHome(QProcessEnvironment::systemEnvironment(), s);
79}
80
81auto ProfileInit::needsInit(const QString &path) -> bool {
82 if (path.isEmpty()) {
83 return false;
84 }
85 QDir dir(path);
86 if (!dir.exists()) {
87 return false;
88 }
89 return !dir.exists(".gpg-id");
90}
91
92auto ProfileInit::initialise(const QString &dir, const QList<UserInfo> &users,
93 const AppSettings &s, bool useGit, QString *note)
94 -> bool {
95 QString scratch;
96 QString &out = note != nullptr ? *note : scratch;
97 out.clear();
98
99 const QDir folder(dir);
100 if (!folder.exists() && !Util::mkpathForStore(folder.absolutePath())) {
101 out = tr("Could not create %1.").arg(folder.absolutePath());
102 return false;
103 }
104 const QString gpgIdFile = folder.filePath(QStringLiteral(".gpg-id"));
105 QByteArray written;
106 if (!writeGpgId(gpgIdFile, users, !s.passSigningKey.trimmed().isEmpty(), &out,
107 &written)) {
108 return false;
109 }
110 QString sigFile;
111 if (!s.passSigningKey.trimmed().isEmpty()) {
112 if (!signGpgId(gpgIdFile, written, s, &out)) {
113 return false;
114 }
115 sigFile = gpgIdFile + QStringLiteral(".sig");
116 }
117 if (useGit &&
118 !commitGpgId(folder.absolutePath(), gpgIdFile, sigFile, s, &out)) {
119 return false;
120 }
121
122 // Entries in sub-folders too: a store taken over whole has most of them
123 // there. Visible regular files only, not through links, .git not walked.
124 // Not those under a sub-folder with a .gpg-id of its own: its recipients
125 // did not change here, and Users on the profile's root leaves it alone,
126 // as pass does.
127 const QString root = QDir::cleanPath(folder.absolutePath());
128 const bool viaPass = s.usePass;
129 const QString rootList =
130 QFileInfo(QDir(root).filePath(QStringLiteral(".gpg-id")))
131 .canonicalFilePath();
132 const auto underSubStore = [&root, &rootList, viaPass](const QString &entry) {
133 QString dir = QFileInfo(entry).absolutePath();
134 while (dir != root && dir.startsWith(root + QLatin1Char('/'))) {
135 // A list of its own: a regular file. A linked .gpg-id is no list to
136 // the native backend, as everywhere in QtPass, so its folder's
137 // entries still count; pass follows the link (`-f`) and keeps that
138 // folder on the linked list, so with it they do not, unless the link
139 // leads to the root's own list: that folder follows the root, and
140 // Users on it re-encrypts them.
141 const QFileInfo list(QDir(dir).filePath(QStringLiteral(".gpg-id")));
142 if (viaPass && list.isFile() && !rootList.isEmpty() &&
143 list.canonicalFilePath() == rootList) {
144 return false;
145 }
146 if (list.isFile() &&
147 (viaPass || (!list.isSymLink() && !list.isJunction()))) {
148 return true;
149 }
150 dir = QFileInfo(dir).absolutePath();
151 }
152 return false;
153 };
154 QStringList existing =
155 Util::regularFilesUnder(root, {QStringLiteral("*.gpg")});
156 existing.erase(
157 std::remove_if(existing.begin(), existing.end(), underSubStore),
158 existing.end());
159 if (!existing.isEmpty()) {
160 out = tr("%1 already contains %n encrypted file(s); they were not "
161 "re-encrypted. Switch to the profile and open Users to do that.",
162 nullptr, static_cast<int>(existing.size()))
163 .arg(folder.absolutePath());
164 }
165 return true;
166}
167
168auto ProfileInit::writeGpgId(const QString &gpgIdFile,
169 const QList<UserInfo> &users, bool signed_,
170 QString *note, QByteArray *written) -> bool {
171 QStringList ids;
172 for (const UserInfo &user : users) {
173 if (user.enabled) {
174 ids << user.key_id;
175 }
176 }
177 if (ids.isEmpty()) {
178 *note = tr("No recipient selected; %1 was not written.").arg(gpgIdFile);
179 return false;
180 }
181 // As ImitatePass::writeGpgIdFile: renamed into place whole, so an
182 // interrupted write leaves no half .gpg-id to sign or use. Owner-only: the
183 // list leaks which keys the store is encrypted to.
184 QByteArray contents =
185 (ids.join(QLatin1Char('\n')) + QLatin1Char('\n')).toUtf8();
186 if (signed_) {
187 // Same rule as ImitatePass::writeGpgIdFile: reserved and recorded
188 // before the write, bound to the store root (GpgIdGeneration). A new
189 // profile has no list on disk to take a generation from.
190 QString why;
191 const std::optional<QString> store = GpgIdGeneration::storeIdFor(
192 QFileInfo(gpgIdFile).absolutePath(), std::nullopt, &why);
193 if (!store) {
194 *note = why;
195 return false;
196 }
197 const std::optional<qint64> generation =
198 GpgIdGeneration::reserveNext(gpgIdFile, std::nullopt, &why);
199 if (!generation) {
200 *note = why;
201 return false;
202 }
203 contents = GpgIdGeneration::withHeader(*generation, QStringLiteral("."),
204 contents, *store);
205 }
206 QString why;
207 if (!Util::writeFileReplacing(gpgIdFile, contents, true, &why)) {
208 *note = why;
209 return false;
210 }
211 if (written != nullptr) {
212 *written = contents;
213 }
214 if (signed_) {
215 // Same as ImitatePass::writeGpgIdFile: the bytes written are the ones
216 // recognised; not fatal, the first read digests them otherwise.
217 QString why;
218 if (!GpgIdGeneration::recordWritten(gpgIdFile, contents, &why)) {
219 qCWarning(lcQtPass) << "Could not record the written .gpg-id:" << why;
220 }
221 }
222 return true;
223}
224
225auto ProfileInit::signGpgId(const QString &gpgIdFile,
226 const QByteArray &contents, const AppSettings &s,
227 QString *note) -> bool {
228 // In the configured GPG home, as ImitatePass's signer runs in the store's
229 // environment.
230 const GpgIdSigner signer(
231 s.gpgExecutable, GpgIdSigner::keysFromSetting(s.passSigningKey),
232 [&s](const QString &app, const QStringList &args, const QString &input,
233 QString *out, QString *err) {
234 QProcess gpg;
235 gpg.setProcessEnvironment(
236 withGpgHome(QProcessEnvironment::systemEnvironment(), s));
237 return Executor::executeBlocking(gpg, app, args, input, out, err);
238 });
239 QString err;
240 if (!signer.sign(gpgIdFile, contents, &err)) {
241 *note = err.trimmed().isEmpty()
242 ? tr("Could not sign %1 with %2.")
243 .arg(gpgIdFile, signer.keys().first())
244 : tr("Could not sign %1 with %2: %3")
245 .arg(gpgIdFile, signer.keys().first(), err.trimmed());
246 // withGpgHome() fell back to the inherited home: gpg's "no secret key"
247 // would not say that the configured one is missing.
248 if (!s.gpgHome.isEmpty()) {
249 QDir home(s.gpgHome);
250 home.makeAbsolute();
251 if (!home.exists()) {
252 *note +=
253 QLatin1Char(' ') +
254 tr("The configured GPG home %1 does not exist.").arg(home.path());
255 }
256 }
257 return false;
258 }
259 return true;
260}
261
262auto ProfileInit::commitGpgId(const QString &dir, const QString &gpgIdFile,
263 const QString &sigFile, const AppSettings &s,
264 QString *note) -> bool {
265 // A process of our own in the profile directory: nothing here may touch
266 // the shared Executor or PASSWORD_STORE_DIR of the active store.
267 QProcess git;
268 git.setWorkingDirectory(dir);
269 git.setProcessEnvironment(gitEnvironment(dir, s));
270 auto run = [&](const QStringList &args) -> bool {
271 QString err;
272 const int rc = Executor::executeBlocking(git, s.gitExecutable, args,
273 QString(), nullptr, &err);
274 if (rc != 0) {
275 *note =
276 tr("git %1 failed in %2: %3").arg(args.first(), dir, err.trimmed());
277 return false;
278 }
279 return true;
280 };
281 QStringList files{QFileInfo(gpgIdFile).fileName()};
282 if (!sigFile.isEmpty()) {
283 files << QFileInfo(sigFile).fileName();
284 }
285 const QStringList add =
286 QStringList{QStringLiteral("add"), QStringLiteral("--")} + files;
287 if (!run({QStringLiteral("init")}) || !run(add)) {
288 return false;
289 }
290 return run(commitArgs(git, s) +
291 QStringList{QStringLiteral("-m"),
292 QStringLiteral("Added .gpg-id using QtPass."),
293 QStringLiteral("--")} +
294 files);
295}
296
297auto ProfileInit::initGit(const QString &dir, const AppSettings &s,
298 QString *note) -> bool {
299 QProcess git;
300 git.setWorkingDirectory(dir);
301 git.setProcessEnvironment(gitEnvironment(dir, s));
302 auto run = [&](const QStringList &args) -> bool {
303 QString err;
304 const int rc = Executor::executeBlocking(git, s.gitExecutable, args,
305 QString(), nullptr, &err);
306 if (rc != 0) {
307 *note =
308 tr("git %1 failed in %2: %3").arg(args.first(), dir, err.trimmed());
309 return false;
310 }
311 return true;
312 };
313 // Stage only store files (as the re-encryption backup commit does): an
314 // existing folder may hold plaintext exports or swap files. Regular files
315 // only: a link or junction must not have its target staged.
316 QStringList files;
317 const QDir base(dir);
318 // Hidden directories (.git among them) are not walked, hidden files are:
319 // .gpg-id is one.
320 const QStringList found = Util::regularFilesUnder(
321 dir,
322 {QStringLiteral("*.gpg"), QStringLiteral(".gpg-id"),
323 QStringLiteral(".gpg-id.sig")},
324 nullptr, true);
325 for (const QString &path : found) {
326 files << base.relativeFilePath(path);
327 }
328 if (!run({QStringLiteral("init")})) {
329 return false;
330 }
331 if (!files.isEmpty() &&
332 !run(QStringList{QStringLiteral("add"), QStringLiteral("--")} + files)) {
333 return false;
334 }
335 return run(commitArgs(git, s) +
336 QStringList{QStringLiteral("--allow-empty"), QStringLiteral("-m"),
337 QStringLiteral("Added password store using QtPass.")});
338}
339
341 const AppSettings &s) -> bool {
342 QProcess git;
343 git.setWorkingDirectory(dir);
344 git.setProcessEnvironment(gitEnvironment(dir, s));
345 for (const char *key : {"user.name", "user.email"}) {
346 QString out;
347 if (Executor::executeBlocking(git, s.gitExecutable,
348 {QStringLiteral("config"),
349 QStringLiteral("--get"),
350 QString::fromLatin1(key)},
351 QString(), &out, nullptr) != 0 ||
352 out.trimmed().isEmpty()) {
353 return false;
354 }
355 }
356 return true;
357}
AppSettings, the value object holding every flat QtPass setting.
static auto executeBlocking(const QString &app, const QStringList &args, const QString &input=QString(), QString *process_out=nullptr, QString *process_err=nullptr) -> int
Run a command synchronously and return its exit code.
Definition executor.cpp:422
static auto reserveNext(const QString &gpgIdFile, std::optional< qint64 > verifiedOnDisk, QString *error=nullptr) -> std::optional< qint64 >
Reserve the generation to write next for gpgIdFile: one above both what this device remembers and ver...
static auto recordWritten(const QString &gpgIdFile, const QByteArray &contents, QString *error=nullptr) -> bool
Record the bytes just written for gpgIdFile, so that this device recognises its own list afterwards a...
static auto withHeader(qint64 generation, const QString &folder, const QByteArray &recipients, const QString &store={}) -> QByteArray
A complete list: the header in front of the recipients.
static auto storeIdFor(const QString &storeRoot, const std::optional< QString > &onDisk, QString *error=nullptr) -> std::optional< QString >
The store id to write into a list of storeRoot: the one this device knows the store by; else onDisk,...
The detached signature on a store's .gpg-id: whether the configured signing key is usable,...
Definition gpgidsigner.h:28
auto sign(const QString &gpgIdFile, const QByteArray &contents, QString *error=nullptr) const -> bool
Write <file>.sig, a detached signature by the first key over contents, the bytes the caller just wrot...
auto keys() const -> const QStringList &
The keys this signer was given.
Definition gpgidsigner.h:78
static auto keysFromSetting(const QString &passSigningKey) -> QStringList
Split the settings value into keys, upper-cased the way gpg prints fingerprints.
static auto needsInit(const QString &path) -> bool
Check if a profile path needs initialization.
static auto initGit(const QString &dir, const AppSettings &s, QString *note) -> bool
Put an existing store under Git: git init, stage the .gpg entries and the .gpg-id files (nothing else...
static auto commitGpgId(const QString &dir, const QString &gpgIdFile, const QString &sigFile, const AppSettings &s, QString *note) -> bool
git init in dir, stage the .gpg-id and its signature, and commit them (signed when pass....
static auto gpgEnvironment(const AppSettings &s) -> QProcessEnvironment
The environment QtPass runs gpg in outside the active store's backend: the system's,...
static auto gitIdentityConfigured(const QString &dir, const AppSettings &s) -> bool
Whether git has a name and e-mail to commit with, i.e. user.name and user.email resolve (globally or ...
static auto initialise(const QString &dir, const QList< UserInfo > &users, const AppSettings &s, bool useGit, QString *note) -> bool
Write the recipients and, if wanted, put the folder under git.
static auto writeGpgId(const QString &gpgIdFile, const QList< UserInfo > &users, bool signed_, QString *note, QByteArray *written=nullptr) -> bool
Write the enabled recipients to gpgIdFile, whole or not at all; with signed_ the list carries the gen...
static auto signGpgId(const QString &gpgIdFile, const QByteArray &contents, const AppSettings &s, QString *note) -> bool
Sign the list just written as gpgIdFile, over the bytes contents that were written.
static auto regularFilesUnder(const QString &dir, const QStringList &nameFilters, QStringList *skipped=nullptr, bool hiddenFiles=false) -> QStringList
Regular files under dir whose name matches one of nameFilters, found by walking only real directories...
Definition util.cpp:721
static auto writeFileReplacing(const QString &path, const QByteArray &bytes, bool replace, QString *error=nullptr) -> bool
Write bytes as the file path through stageFileReplacing().
Definition util.cpp:1165
static auto gitStoreVariables(const QString &store, const QString &git={}, std::optional< uint > asUser=std::nullopt) -> QList< std::pair< QString, QString > >
The variables that keep git in store's own repository, never one above it: GIT_CEILING_DIRECTORIES (g...
Definition util.cpp:376
static auto mkpathForStore(const QString &path) -> bool
Create path and any missing parents as pass would.
Definition util.cpp:438
Executor, the FIFO queue every gpg, git and pass child process of QtPass goes through,...
Rollback detection for signed .gpg-id files: the generation counter and the record of what was last s...
GpgIdSigner, which signs a store's .gpg-id and verifies an existing .gpg-id.sig against the configure...
Initialising a new profile's store (.gpg-id, signature, Git) with blocking processes,...
The "qtpass" logging category.
Plain value-object holding all flat QtPass configuration.
Definition appsettings.h:36
Stores GPG key info including validity, creation date, and more.
Definition userinfo.h:19
UserInfo, one GPG key as listed by gpg, with the selection flag the recipient dialogs use.
Util: static helpers for the process environment, binary discovery, store paths, URL safety and the l...