Line data Source code
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 78 : Pass::Pass() : wrapperRunning(false), env(QProcess::systemEnvironment()) {
32 78 : connect(&exec,
33 : static_cast<void (Executor::*)(int, int, const QString &,
34 : const QString &)>(&Executor::finished),
35 78 : this, &Pass::finished);
36 :
37 : // TODO(bezet): stop using process
38 : // connect(&process, SIGNAL(error(QProcess::ProcessError)), this,
39 : // SIGNAL(error(QProcess::ProcessError)));
40 :
41 78 : connect(&exec, &Executor::starting, this, &Pass::startingExecuteWrapper);
42 78 : env.append("WSLENV=PASSWORD_STORE_DIR/p");
43 78 : }
44 :
45 0 : void Pass::executeWrapper(PROCESS id, const QString &app,
46 : const QStringList &args, bool readStdout,
47 : bool readStderr) {
48 0 : executeWrapper(id, app, args, QString(), readStdout, readStderr);
49 0 : }
50 :
51 0 : 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 0 : exec.execute(id, QtPassSettings::getPassStore(), app, args, std::move(input),
58 : readStdout, readStderr);
59 0 : }
60 :
61 32 : void Pass::init() {
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 64 : if (!QtPassSettings::getGpgHome().isEmpty()) {
72 0 : QDir absHome(QtPassSettings::getGpgHome());
73 0 : absHome.makeAbsolute();
74 0 : env << "GNUPGHOME=" + absHome.path();
75 0 : }
76 32 : }
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 0 : auto Pass::Generate_b(unsigned int length, const QString &charset) -> QString {
86 0 : QString passwd;
87 0 : if (QtPassSettings::isUsePwgen()) {
88 : // --secure goes first as it overrides --no-* otherwise
89 0 : QStringList args;
90 0 : args.append("-1");
91 0 : if (!QtPassSettings::isLessRandom()) {
92 0 : args.append("--secure");
93 : }
94 0 : args.append(QtPassSettings::isAvoidCapitals() ? "--no-capitalize"
95 : : "--capitalize");
96 0 : args.append(QtPassSettings::isAvoidNumbers() ? "--no-numerals"
97 : : "--numerals");
98 0 : if (QtPassSettings::isUseSymbols()) {
99 0 : args.append("--symbols");
100 : }
101 0 : args.append(QString::number(length));
102 : // TODO(bezet): try-catch here(2 statuses to merge o_O)
103 0 : if (Executor::executeBlocking(QtPassSettings::getPwgenExecutable(), args,
104 : &passwd) == 0) {
105 0 : static const QRegularExpression literalNewLines{"[\\n\\r]"};
106 0 : passwd.remove(literalNewLines);
107 : } else {
108 0 : 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 0 : if (charset.length() > 0) {
117 0 : passwd = generateRandomPassword(charset, length);
118 : } else {
119 0 : emit critical(
120 0 : tr("No characters chosen"),
121 0 : tr("Can't generate password, there are no characters to choose from "
122 : "set in the configuration!"));
123 : }
124 : }
125 0 : return passwd;
126 : }
127 :
128 : /**
129 : * @brief Pass::GenerateGPGKeys internal gpg keypair generator . .
130 : * @param batch GnuPG style configuration string
131 : */
132 0 : void Pass::GenerateGPGKeys(QString batch) {
133 0 : executeWrapper(GPG_GENKEYS, QtPassSettings::getGpgExecutable(),
134 : {"--gen-key", "--no-tty", "--batch"}, std::move(batch));
135 : // TODO(annejan): check status / error messages - probably not here, it's just
136 : // started
137 : // here, see finished for details
138 : // https://github.com/IJHack/QtPass/issues/202#issuecomment-251081688
139 0 : }
140 :
141 : /**
142 : * @brief Pass::listKeys list users
143 : * @param keystrings
144 : * @param secret list private keys
145 : * @return QList<UserInfo> users
146 : */
147 0 : auto Pass::listKeys(QStringList keystrings, bool secret) -> QList<UserInfo> {
148 0 : QList<UserInfo> users;
149 0 : QStringList args = {"--no-tty", "--with-colons", "--with-fingerprint"};
150 0 : args.append(secret ? "--list-secret-keys" : "--list-keys");
151 :
152 0 : for (const QString &keystring : AS_CONST(keystrings)) {
153 0 : if (!keystring.isEmpty()) {
154 : args.append(keystring);
155 : }
156 : }
157 0 : QString p_out;
158 0 : if (Executor::executeBlocking(QtPassSettings::getGpgExecutable(), args,
159 : &p_out) != 0) {
160 : return users;
161 : }
162 : #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
163 : const QStringList keys =
164 0 : p_out.split(Util::newLinesRegex(), Qt::SkipEmptyParts);
165 : #else
166 : const QStringList keys =
167 : p_out.split(Util::newLinesRegex(), QString::SkipEmptyParts);
168 : #endif
169 0 : UserInfo current_user;
170 0 : for (const QString &key : keys) {
171 0 : QStringList props = key.split(':');
172 0 : if (props.size() < 10) {
173 : continue;
174 : }
175 0 : if (props[0] == (secret ? "sec" : "pub")) {
176 0 : if (!current_user.key_id.isEmpty()) {
177 : users.append(current_user);
178 : }
179 0 : current_user = UserInfo();
180 0 : current_user.key_id = props[4];
181 0 : current_user.name = props[9].toUtf8();
182 0 : current_user.validity = props[1][0].toLatin1();
183 0 : current_user.created.setSecsSinceEpoch(props[5].toUInt());
184 0 : current_user.expiry.setSecsSinceEpoch(props[6].toUInt());
185 0 : } else if (current_user.name.isEmpty() && props[0] == "uid") {
186 0 : current_user.name = props[9];
187 0 : } else if ((props[0] == "fpr") && props[9].endsWith(current_user.key_id)) {
188 0 : current_user.key_id = props[9];
189 : }
190 : }
191 0 : if (!current_user.key_id.isEmpty()) {
192 : users.append(current_user);
193 : }
194 : return users;
195 0 : }
196 :
197 : /**
198 : * @brief Pass::listKeys list users
199 : * @param keystring
200 : * @param secret list private keys
201 : * @return QList<UserInfo> users
202 : */
203 0 : auto Pass::listKeys(const QString &keystring, bool secret) -> QList<UserInfo> {
204 0 : return listKeys(QStringList(keystring), secret);
205 : }
206 :
207 : /**
208 : * @brief Pass::processFinished reemits specific signal based on what process
209 : * has finished
210 : * @param id id of Pass process that was scheduled and finished
211 : * @param exitCode return code of a process
212 : * @param out output generated by process(if capturing was requested, empty
213 : * otherwise)
214 : * @param err error output generated by process(if capturing was requested,
215 : * or error occurred)
216 : */
217 0 : void Pass::finished(int id, int exitCode, const QString &out,
218 : const QString &err) {
219 : auto pid = static_cast<PROCESS>(id);
220 0 : if (exitCode != 0) {
221 0 : emit processErrorExit(exitCode, err);
222 0 : return;
223 : }
224 0 : switch (pid) {
225 0 : case GIT_INIT:
226 0 : emit finishedGitInit(out, err);
227 0 : break;
228 0 : case GIT_PULL:
229 0 : emit finishedGitPull(out, err);
230 0 : break;
231 0 : case GIT_PUSH:
232 0 : emit finishedGitPush(out, err);
233 0 : break;
234 0 : case PASS_SHOW:
235 0 : emit finishedShow(out);
236 0 : break;
237 0 : case PASS_OTP_GENERATE:
238 0 : emit finishedOtpGenerate(out);
239 0 : break;
240 0 : case PASS_INSERT:
241 0 : emit finishedInsert(out, err);
242 0 : break;
243 0 : case PASS_REMOVE:
244 0 : emit finishedRemove(out, err);
245 0 : break;
246 0 : case PASS_INIT:
247 0 : emit finishedInit(out, err);
248 0 : break;
249 0 : case PASS_MOVE:
250 0 : emit finishedMove(out, err);
251 0 : break;
252 0 : case PASS_COPY:
253 0 : emit finishedCopy(out, err);
254 0 : break;
255 0 : case GPG_GENKEYS:
256 0 : emit finishedGenerateGPGKeys(out, err);
257 0 : break;
258 : default:
259 : #ifdef QT_DEBUG
260 : dbg() << "Unhandled process type" << pid;
261 : #endif
262 : break;
263 : }
264 : }
265 :
266 : /**
267 : * @brief Pass::updateEnv update the execution environment (used when
268 : * switching profiles)
269 : */
270 0 : void Pass::updateEnv() {
271 : // put PASSWORD_STORE_SIGNING_KEY in env
272 0 : QStringList envSigningKey = env.filter("PASSWORD_STORE_SIGNING_KEY=");
273 0 : QString currentSigningKey = QtPassSettings::getPassSigningKey();
274 0 : if (envSigningKey.isEmpty()) {
275 0 : if (!currentSigningKey.isEmpty()) {
276 : // dbg()<< "Added
277 : // PASSWORD_STORE_SIGNING_KEY with" + currentSigningKey;
278 0 : env.append("PASSWORD_STORE_SIGNING_KEY=" + currentSigningKey);
279 : }
280 : } else {
281 0 : if (currentSigningKey.isEmpty()) {
282 : // dbg() << "Removed
283 : // PASSWORD_STORE_SIGNING_KEY";
284 : env.removeAll(envSigningKey.first());
285 : } else {
286 : // dbg()<< "Update
287 : // PASSWORD_STORE_SIGNING_KEY with " + currentSigningKey;
288 0 : env.replaceInStrings(envSigningKey.first(),
289 0 : "PASSWORD_STORE_SIGNING_KEY=" + currentSigningKey);
290 : }
291 : }
292 : // put PASSWORD_STORE_DIR in env
293 0 : QStringList store = env.filter("PASSWORD_STORE_DIR=");
294 0 : if (store.isEmpty()) {
295 : // dbg()<< "Added
296 : // PASSWORD_STORE_DIR";
297 0 : env.append("PASSWORD_STORE_DIR=" + QtPassSettings::getPassStore());
298 : } else {
299 : // dbg()<< "Update
300 : // PASSWORD_STORE_DIR with " + passStore;
301 0 : env.replaceInStrings(store.first(), "PASSWORD_STORE_DIR=" +
302 0 : QtPassSettings::getPassStore());
303 : }
304 0 : exec.setEnvironment(env);
305 0 : }
306 :
307 : /**
308 : * @brief Pass::getGpgIdPath return gpgid file path for some file (folder).
309 : * @param for_file which file (folder) would you like the gpgid file path for.
310 : * @return path to the gpgid file.
311 : */
312 0 : auto Pass::getGpgIdPath(const QString &for_file) -> QString {
313 : QString passStore =
314 0 : QDir::fromNativeSeparators(QtPassSettings::getPassStore());
315 0 : QString normalizedFile = QDir::fromNativeSeparators(for_file);
316 0 : QString fullPath = normalizedFile.startsWith(passStore)
317 0 : ? normalizedFile
318 0 : : passStore + "/" + normalizedFile;
319 0 : QDir gpgIdDir(QFileInfo(fullPath).absoluteDir());
320 : bool found = false;
321 0 : while (gpgIdDir.exists() && gpgIdDir.absolutePath().startsWith(passStore)) {
322 0 : if (QFile(gpgIdDir.absoluteFilePath(".gpg-id")).exists()) {
323 : found = true;
324 : break;
325 : }
326 0 : if (!gpgIdDir.cdUp()) {
327 : break;
328 : }
329 : }
330 0 : QString gpgIdPath(found ? gpgIdDir.absoluteFilePath(".gpg-id")
331 0 : : QtPassSettings::getPassStore() + ".gpg-id");
332 :
333 0 : return gpgIdPath;
334 0 : }
335 :
336 : /**
337 : * @brief Pass::getRecipientList return list of gpg-id's to encrypt for
338 : * @param for_file which file (folder) would you like recepients for
339 : * @return recepients gpg-id contents
340 : */
341 0 : auto Pass::getRecipientList(const QString &for_file) -> QStringList {
342 0 : QFile gpgId(getGpgIdPath(for_file));
343 0 : if (!gpgId.open(QIODevice::ReadOnly | QIODevice::Text)) {
344 0 : return {};
345 : }
346 0 : QStringList recipients;
347 0 : while (!gpgId.atEnd()) {
348 0 : QString recipient(gpgId.readLine());
349 0 : recipient = recipient.split("#")[0].trimmed();
350 0 : if (!recipient.isEmpty()) {
351 : recipients += recipient;
352 : }
353 : }
354 : return recipients;
355 0 : }
356 :
357 : /**
358 : * @brief Pass::getRecipientString formated string for use with GPG
359 : * @param for_file which file (folder) would you like recepients for
360 : * @param separator formating separator eg: " -r "
361 : * @param count
362 : * @return recepient string
363 : */
364 0 : auto Pass::getRecipientString(const QString &for_file, const QString &separator,
365 : int *count) -> QStringList {
366 : Q_UNUSED(separator)
367 : Q_UNUSED(count)
368 0 : return Pass::getRecipientList(for_file);
369 : }
370 :
371 : /* Copyright (C) 2017 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
372 : */
373 :
374 0 : auto Pass::boundedRandom(quint32 bound) -> quint32 {
375 0 : if (bound < 2) {
376 : return 0;
377 : }
378 :
379 : quint32 randval;
380 0 : const quint32 max_mod_bound = (1 + ~bound) % bound;
381 :
382 : do {
383 : randval = QRandomGenerator::system()->generate();
384 0 : } while (randval < max_mod_bound);
385 :
386 0 : return randval % bound;
387 : }
388 :
389 0 : auto Pass::generateRandomPassword(const QString &charset, unsigned int length)
390 : -> QString {
391 0 : QString out;
392 0 : for (unsigned int i = 0; i < length; ++i) {
393 0 : out.append(charset.at(static_cast<int>(
394 0 : boundedRandom(static_cast<quint32>(charset.length())))));
395 : }
396 0 : return out;
397 : }
|