QtPass 1.4.0
Multi-platform GUI for pass, the standard unix password manager.
Loading...
Searching...
No Matches
imitatepass.cpp
Go to the documentation of this file.
1#include "imitatepass.h"
2#include "qtpasssettings.h"
3#include "util.h"
4#include <QDirIterator>
5#include <QRegularExpression>
6#include <utility>
7
8#ifdef QT_DEBUG
9#include "debughelper.h"
10#endif
11
12using namespace Enums;
13
18ImitatePass::ImitatePass() = default;
19
20static QString pgit(const QString &path) {
21 if (!QtPassSettings::getGitExecutable().startsWith("wsl "))
22 return path;
23 QString res = "$(wslpath " + path + ")";
24 return res.replace('\\', '/');
25}
26
27static QString pgpg(const QString &path) {
28 if (!QtPassSettings::getGpgExecutable().startsWith("wsl "))
29 return path;
30 QString res = "$(wslpath " + path + ")";
31 return res.replace('\\', '/');
32}
33
38 executeGit(GIT_INIT, {"init", pgit(QtPassSettings::getPassStore())});
39}
40
44void ImitatePass::GitPull() { executeGit(GIT_PULL, {"pull"}); }
45
51}
52
58 executeGit(GIT_PUSH, {"push"});
59 }
60}
61
65void ImitatePass::Show(QString file) {
66 file = QtPassSettings::getPassStore() + file + ".gpg";
67 QStringList args = {"-d", "--quiet", "--yes", "--no-encrypt-to",
68 "--batch", "--use-agent", pgpg(file)};
69 executeGpg(PASS_SHOW, args);
70}
71
75void ImitatePass::OtpGenerate(QString file) {
76#ifdef QT_DEBUG
77 dbg() << "No OTP generation code for fake pass yet, attempting for file: " +
78 file;
79#else
80 Q_UNUSED(file)
81#endif
82}
83
91void ImitatePass::Insert(QString file, QString newValue, bool overwrite) {
92 file = file + ".gpg";
93 QString gpgIdPath = Pass::getGpgIdPath(file);
94 if (!verifyGpgIdFile(gpgIdPath)) {
95 emit critical(tr("Check .gpgid file signature!"),
96 tr("Signature for %1 is invalid.").arg(gpgIdPath));
97 return;
98 }
99 transactionHelper trans(this, PASS_INSERT);
100 QStringList recipients = Pass::getRecipientList(file);
101 if (recipients.isEmpty()) {
102 // TODO(bezet): probably throw here
103 emit critical(tr("Can not edit"),
104 tr("Could not read encryption key to use, .gpg-id "
105 "file missing or invalid."));
106 return;
107 }
108 QStringList args = {"--batch", "-eq", "--output", pgpg(file)};
109 for (auto &r : recipients) {
110 args.append("-r");
111 args.append(r);
112 };
113 if (overwrite)
114 args.append("--yes");
115 args.append("-");
116 executeGpg(PASS_INSERT, args, newValue);
118 // TODO(bezet) why not?
119 if (!overwrite)
120 executeGit(GIT_ADD, {"add", pgit(file)});
121 QString path = QDir(QtPassSettings::getPassStore()).relativeFilePath(file);
122 path.replace(Util::endsWithGpg(), "");
123 QString msg =
124 QString(overwrite ? "Edit" : "Add") + " for " + path + " using QtPass.";
125 GitCommit(file, msg);
126 }
127}
128
135void ImitatePass::GitCommit(const QString &file, const QString &msg) {
136 if (file.isEmpty())
137 executeGit(GIT_COMMIT, {"commit", "-m", msg});
138 else
139 executeGit(GIT_COMMIT, {"commit", "-m", msg, "--", pgit(file)});
140}
141
145void ImitatePass::Remove(QString file, bool isDir) {
146 file = QtPassSettings::getPassStore() + file;
147 transactionHelper trans(this, PASS_REMOVE);
148 if (!isDir)
149 file += ".gpg";
151 executeGit(GIT_RM, {"rm", (isDir ? "-rf" : "-f"), pgit(file)});
152 // TODO(bezet): commit message used to have pass-like file name inside(ie.
153 // getFile(file, true)
154 GitCommit(file, "Remove for " + file + " using QtPass.");
155 } else {
156 if (isDir) {
157 QDir dir(file);
158 dir.removeRecursively();
159 } else
160 QFile(file).remove();
161 }
162}
163
171void ImitatePass::Init(QString path, const QList<UserInfo> &users) {
172#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
173 QStringList signingKeys =
174 QtPassSettings::getPassSigningKey().split(" ", Qt::SkipEmptyParts);
175#else
176 QStringList signingKeys =
177 QtPassSettings::getPassSigningKey().split(" ", QString::SkipEmptyParts);
178#endif
179 QString gpgIdSigFile = path + ".gpg-id.sig";
180 bool addSigFile = false;
181 if (!signingKeys.isEmpty()) {
182 QString out;
183 QStringList args =
184 QStringList{"--status-fd=1", "--list-secret-keys"} + signingKeys;
186 bool found = false;
187 for (auto &key : signingKeys) {
188 if (out.contains("[GNUPG:] KEY_CONSIDERED " + key)) {
189 found = true;
190 break;
191 }
192 }
193 if (!found) {
194 emit critical(tr("No signing key!"),
195 tr("None of the secret signing keys is available.\n"
196 "You will not be able to change the user list!"));
197 return;
198 }
199 QFileInfo checkFile(gpgIdSigFile);
200 if (!checkFile.exists() || !checkFile.isFile())
201 addSigFile = true;
202 }
203
204 QString gpgIdFile = path + ".gpg-id";
205 QFile gpgId(gpgIdFile);
206 bool addFile = false;
207 transactionHelper trans(this, PASS_INIT);
208 if (QtPassSettings::isAddGPGId(true)) {
209 QFileInfo checkFile(gpgIdFile);
210 if (!checkFile.exists() || !checkFile.isFile())
211 addFile = true;
212 }
213 if (!gpgId.open(QIODevice::WriteOnly | QIODevice::Text)) {
214 emit critical(tr("Cannot update"),
215 tr("Failed to open .gpg-id for writing."));
216 return;
217 }
218 bool secret_selected = false;
219 foreach (const UserInfo &user, users) {
220 if (user.enabled) {
221 gpgId.write((user.key_id + "\n").toUtf8());
222 secret_selected |= user.have_secret;
223 }
224 }
225 gpgId.close();
226 if (!secret_selected) {
227 emit critical(
228 tr("Check selected users!"),
229 tr("None of the selected keys have a secret key available.\n"
230 "You will not be able to decrypt any newly added passwords!"));
231 return;
232 }
233
234 if (!signingKeys.isEmpty()) {
235 QStringList args;
236 for (auto &key : signingKeys) {
237 args.append(QStringList{"--default-key", key});
238 }
239 args.append(QStringList{"--yes", "--detach-sign", gpgIdFile});
241 if (!verifyGpgIdFile(gpgIdFile)) {
242 emit critical(tr("Check .gpgid file signature!"),
243 tr("Signature for %1 is invalid.").arg(gpgIdFile));
244 return;
245 }
246 }
247
250 if (addFile)
251 executeGit(GIT_ADD, {"add", pgit(gpgIdFile)});
252 QString commitPath = gpgIdFile;
253 commitPath.replace(Util::endsWithGpg(), "");
254 GitCommit(gpgIdFile, "Added " + commitPath + " using QtPass.");
255 if (!signingKeys.isEmpty()) {
256 if (addSigFile)
257 executeGit(GIT_ADD, {"add", pgit(gpgIdSigFile)});
258 commitPath = gpgIdSigFile;
259 commitPath.replace(QRegularExpression("\\.gpg$"), "");
260 GitCommit(gpgIdSigFile, "Added " + commitPath + " using QtPass.");
261 }
262 }
263 reencryptPath(path);
264}
265
271bool ImitatePass::verifyGpgIdFile(const QString &file) {
272#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
273 QStringList signingKeys =
274 QtPassSettings::getPassSigningKey().split(" ", Qt::SkipEmptyParts);
275#else
276 QStringList signingKeys =
277 QtPassSettings::getPassSigningKey().split(" ", QString::SkipEmptyParts);
278#endif
279 if (signingKeys.isEmpty())
280 return true;
281 QString out;
282 QStringList args =
283 QStringList{"--verify", "--status-fd=1", pgpg(file) + ".sig", pgpg(file)};
285 QRegularExpression re(
286 "^\\[GNUPG:\\] VALIDSIG ([A-F0-9]{40}) .* ([A-F0-9]{40})\\r?$",
287 QRegularExpression::MultilineOption);
288 QRegularExpressionMatch m = re.match(out);
289 if (!m.hasMatch())
290 return false;
291 QStringList fingerprints = m.capturedTexts();
292 fingerprints.removeFirst();
293 for (auto &key : signingKeys) {
294 if (fingerprints.contains(key))
295 return true;
296 }
297 return false;
298}
299
305bool ImitatePass::removeDir(const QString &dirName) {
306 bool result = true;
307 QDir dir(dirName);
308
309 if (dir.exists(dirName)) {
310 Q_FOREACH (QFileInfo info,
311 dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System |
312 QDir::Hidden | QDir::AllDirs | QDir::Files,
313 QDir::DirsFirst)) {
314 if (info.isDir())
315 result = removeDir(info.absoluteFilePath());
316 else
317 result = QFile::remove(info.absoluteFilePath());
318
319 if (!result)
320 return result;
321 }
322 result = dir.rmdir(dirName);
323 }
324 return result;
325}
326
334void ImitatePass::reencryptPath(const QString &dir) {
335 emit statusMsg(tr("Re-encrypting from folder %1").arg(dir), 3000);
336 emit startReencryptPath();
338 // TODO(bezet): move statuses inside actions?
339 emit statusMsg(tr("Updating password-store"), 2000);
340 GitPull_b();
341 }
342 QDir currentDir;
343 QDirIterator gpgFiles(dir, QStringList() << "*.gpg", QDir::Files,
344 QDirIterator::Subdirectories);
345 QStringList gpgIdFilesVerified;
346 QStringList gpgId;
347 while (gpgFiles.hasNext()) {
348 QString fileName = gpgFiles.next();
349 if (gpgFiles.fileInfo().path() != currentDir.path()) {
350 QString gpgIdPath = Pass::getGpgIdPath(fileName);
351 if (!gpgIdFilesVerified.contains(gpgIdPath)) {
352 if (!verifyGpgIdFile(gpgIdPath)) {
353 emit critical(tr("Check .gpgid file signature!"),
354 tr("Signature for %1 is invalid.").arg(gpgIdPath));
355 emit endReencryptPath();
356 return;
357 }
358 gpgIdFilesVerified.append(gpgIdPath);
359 }
360 gpgId = getRecipientList(fileName);
361 gpgId.sort();
362 }
363 // TODO(bezet): enable --with-colons for better future-proofness?
364 QStringList args = {
365 "-v", "--no-secmem-warning", "--no-permission-warning",
366 "--list-only", "--keyid-format=long", pgpg(fileName)};
367 QString keys, err;
369 QStringList actualKeys;
370 keys += err;
371 static const QRegularExpression newLines{"[\r\n]"};
372#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
373 QStringList key = keys.split(newLines, Qt::SkipEmptyParts);
374#else
375 QStringList key = keys.split(newLines, QString::SkipEmptyParts);
376#endif
377 QListIterator<QString> itr(key);
378 while (itr.hasNext()) {
379 QString current = itr.next();
380 QStringList cur = current.split(" ");
381 if (cur.length() > 4) {
382 QString actualKey = cur.takeAt(4);
383 if (actualKey.length() == 16) {
384 actualKeys << actualKey;
385 }
386 }
387 }
388 actualKeys.sort();
389 if (actualKeys != gpgId) {
390 // dbg()<< actualKeys << gpgId << getRecipientList(fileName);
391#ifdef QT_DEBUG
392 dbg() << "reencrypt " << fileName << " for " << gpgId;
393#endif
394 QString local_lastDecrypt = "Could not decrypt";
395 args = QStringList{
396 "-d", "--quiet", "--yes", "--no-encrypt-to",
397 "--batch", "--use-agent", pgpg(fileName)};
399 &local_lastDecrypt);
400
401 if (!local_lastDecrypt.isEmpty() &&
402 local_lastDecrypt != "Could not decrypt") {
403 if (local_lastDecrypt.right(1) != "\n")
404 local_lastDecrypt += "\n";
405
406 QStringList recipients = Pass::getRecipientList(fileName);
407 if (recipients.isEmpty()) {
408 emit critical(tr("Can not edit"),
409 tr("Could not read encryption key to use, .gpg-id "
410 "file missing or invalid."));
411 return;
412 }
413 args =
414 QStringList{"--yes", "--batch", "-eq", "--output", pgpg(fileName)};
415 for (auto &i : recipients) {
416 args.append("-r");
417 args.append(i);
418 }
419 args.append("-");
421 local_lastDecrypt);
422
425 {"add", pgit(fileName)});
426 QString path =
427 QDir(QtPassSettings::getPassStore()).relativeFilePath(fileName);
428 path.replace(Util::endsWithGpg(), "");
430 {"commit", pgit(fileName), "-m",
431 "Edit for " + path + " using QtPass."});
432 }
433
434 } else {
435#ifdef QT_DEBUG
436 dbg() << "Decrypt error on re-encrypt";
437#endif
438 }
439 }
440 }
442 emit statusMsg(tr("Updating password-store"), 2000);
443 // TODO(bezet): this is non-blocking and shall be done outside
444 GitPush();
445 }
446 emit endReencryptPath();
447}
448
449void ImitatePass::Move(const QString src, const QString dest,
450 const bool force) {
451 transactionHelper trans(this, PASS_MOVE);
452 QFileInfo srcFileInfo(src);
453 QFileInfo destFileInfo(dest);
454 QString destFile;
455 QString srcFileBaseName = srcFileInfo.fileName();
456
457 if (srcFileInfo.isFile()) {
458 if (destFileInfo.isFile()) {
459 if (!force) {
460#ifdef QT_DEBUG
461 dbg() << "Destination file already exists";
462#endif
463 return;
464 }
465 } else if (destFileInfo.isDir()) {
466 destFile = QDir(dest).filePath(srcFileBaseName);
467 } else {
468 destFile = dest;
469 }
470
471 if (destFile.endsWith(".gpg", Qt::CaseInsensitive))
472 destFile.chop(4); // make sure suffix is lowercase
473 destFile.append(".gpg");
474 } else if (srcFileInfo.isDir()) {
475 if (destFileInfo.isDir()) {
476 destFile = QDir(dest).filePath(srcFileBaseName);
477 } else if (destFileInfo.isFile()) {
478#ifdef QT_DEBUG
479 dbg() << "Destination is a file";
480#endif
481 return;
482 } else {
483 destFile = dest;
484 }
485 } else {
486#ifdef QT_DEBUG
487 dbg() << "Source file does not exist";
488#endif
489 return;
490 }
491
492#ifdef QT_DEBUG
493 dbg() << "Move Source: " << src;
494 dbg() << "Move Destination: " << destFile;
495#endif
496
498 QStringList args;
499 args << "mv";
500 if (force) {
501 args << "-f";
502 }
503 args << pgit(src);
504 args << pgit(destFile);
505 executeGit(GIT_MOVE, args);
506
507 QString relSrc = QDir(QtPassSettings::getPassStore()).relativeFilePath(src);
508 relSrc.replace(Util::endsWithGpg(), "");
509 QString relDest =
510 QDir(QtPassSettings::getPassStore()).relativeFilePath(destFile);
511 relDest.replace(Util::endsWithGpg(), "");
512 QString message = QString("Moved for %1 to %2 using QtPass.");
513 message = message.arg(relSrc, relDest);
514 GitCommit("", message);
515 } else {
516 QDir qDir;
517 if (force) {
518 qDir.remove(destFile);
519 }
520 qDir.rename(src, destFile);
521 }
522}
523
524void ImitatePass::Copy(const QString src, const QString dest,
525 const bool force) {
526 QFileInfo destFileInfo(dest);
527 transactionHelper trans(this, PASS_COPY);
529 QStringList args;
530 args << "cp";
531 if (force) {
532 args << "-f";
533 }
534 args << pgit(src);
535 args << pgit(dest);
536 executeGit(GIT_COPY, args);
537
538 QString message = QString("copied from %1 to %2 using QTPass.");
539 message = message.arg(src, dest);
540 GitCommit("", message);
541 } else {
542 QDir qDir;
543 if (force) {
544 qDir.remove(dest);
545 }
546 QFile::copy(src, dest);
547 }
548 // reecrypt all files under the new folder
549 if (destFileInfo.isDir()) {
550 reencryptPath(destFileInfo.absoluteFilePath());
551 } else if (destFileInfo.isFile()) {
552 reencryptPath(destFileInfo.dir().path());
553 }
554}
555
560void ImitatePass::executeGpg(PROCESS id, const QStringList &args, QString input,
561 bool readStdout, bool readStderr) {
562 executeWrapper(id, QtPassSettings::getGpgExecutable(), args, std::move(input),
563 readStdout, readStderr);
564}
569void ImitatePass::executeGit(PROCESS id, const QStringList &args, QString input,
570 bool readStdout, bool readStderr) {
571 executeWrapper(id, QtPassSettings::getGitExecutable(), args, std::move(input),
572 readStdout, readStderr);
573}
574
585void ImitatePass::finished(int id, int exitCode, const QString &out,
586 const QString &err) {
587#ifdef QT_DEBUG
588 dbg() << "Imitate Pass";
589#endif
590 static QString transactionOutput;
591 PROCESS pid = transactionIsOver(static_cast<PROCESS>(id));
592 transactionOutput.append(out);
593
594 if (exitCode == 0) {
595 if (pid == INVALID)
596 return;
597 } else {
598 while (pid == INVALID) {
599 id = exec.cancelNext();
600 if (id == -1) {
601 // this is probably irrecoverable and shall not happen
602#ifdef QT_DEBUG
603 dbg() << "No such transaction!";
604#endif
605 return;
606 }
607 pid = transactionIsOver(static_cast<PROCESS>(id));
608 }
609 }
610 Pass::finished(pid, exitCode, transactionOutput, err);
611 transactionOutput.clear();
612}
613
623void ImitatePass::executeWrapper(PROCESS id, const QString &app,
624 const QStringList &args, QString input,
625 bool readStdout, bool readStderr) {
626 transactionAdd(id);
627 Pass::executeWrapper(id, app, args, input, readStdout, readStderr);
628}
static int executeBlocking(QString app, const QStringList &args, QString input=QString(), QString *process_out=Q_NULLPTR, QString *process_err=Q_NULLPTR)
Executor::executeBlocking blocking version of the executor, takes input and presents it as stdin.
Definition: executor.cpp:171
int cancelNext()
Executor::cancelNext cancels execution of first process in queue if it's not already running.
Definition: executor.cpp:233
virtual void OtpGenerate(QString file) Q_DECL_OVERRIDE
ImitatePass::OtpGenerate generates an otp code.
Definition: imitatepass.cpp:75
virtual void Show(QString file) Q_DECL_OVERRIDE
ImitatePass::Show shows content of file.
Definition: imitatepass.cpp:65
ImitatePass()
ImitatePass::ImitatePass for situaions when pass is not available we imitate the behavior of pass htt...
virtual void GitPush() Q_DECL_OVERRIDE
ImitatePass::GitPush git init wrapper.
Definition: imitatepass.cpp:56
void Copy(const QString src, const QString dest, const bool force=false) Q_DECL_OVERRIDE
virtual void executeWrapper(PROCESS id, const QString &app, const QStringList &args, QString input, bool readStdout=true, bool readStderr=true) Q_DECL_OVERRIDE
executeWrapper overrided so that every execution is a transaction
virtual void GitInit() Q_DECL_OVERRIDE
ImitatePass::GitInit git init wrapper.
Definition: imitatepass.cpp:37
virtual void GitPull_b() Q_DECL_OVERRIDE
ImitatePass::GitPull_b git pull wrapper.
Definition: imitatepass.cpp:49
virtual void Insert(QString file, QString newValue, bool overwrite=false) Q_DECL_OVERRIDE
ImitatePass::Insert create new file with encrypted content.
Definition: imitatepass.cpp:91
virtual void GitPull() Q_DECL_OVERRIDE
ImitatePass::GitPull git init wrapper.
Definition: imitatepass.cpp:44
void endReencryptPath()
virtual void Remove(QString file, bool isDir=false) Q_DECL_OVERRIDE
ImitatePass::Remove custom implementation of "pass remove".
void Move(const QString src, const QString dest, const bool force=false) Q_DECL_OVERRIDE
virtual void Init(QString path, const QList< UserInfo > &users) Q_DECL_OVERRIDE
ImitatePass::Init initialize pass repository.
void startReencryptPath()
void reencryptPath(const QString &dir)
ImitatePass::reencryptPath reencrypt all files under the chosen directory.
void statusMsg(QString, int)
void critical(QString, QString)
void executeWrapper(PROCESS id, const QString &app, const QStringList &args, bool readStdout=true, bool readStderr=true)
Definition: pass.cpp:31
static QStringList getRecipientList(QString for_file)
Pass::getRecipientList return list of gpg-id's to encrypt for.
Definition: pass.cpp:318
Executor exec
Definition: pass.h:25
static QString getGpgIdPath(QString for_file)
Pass::getGpgIdPath return gpgid file path for some file (folder).
Definition: pass.cpp:290
virtual void finished(int id, int exitCode, const QString &out, const QString &err)
Pass::processFinished reemits specific signal based on what process has finished.
Definition: pass.cpp:195
static QString getPassSigningKey(const QString &defaultValue=QVariant().toString())
static bool isUseGit(const bool &defaultValue=QVariant().toBool())
static bool isAutoPull(const bool &defaultValue=QVariant().toBool())
static bool isUseWebDav(const bool &defaultValue=QVariant().toBool())
static QString getGpgExecutable(const QString &defaultValue=QVariant().toString())
static bool isAutoPush(const bool &defaultValue=QVariant().toBool())
static QString getPassStore(const QString &defaultValue=QVariant().toString())
static QString getGitExecutable(const QString &defaultValue=QVariant().toString())
static bool isAddGPGId(const bool &defaultValue=QVariant().toBool())
static const QRegularExpression & endsWithGpg()
Definition: util.cpp:198
void transactionAdd(Enums::PROCESS)
transactionAdd If called after call to transactionStart() and before transactionEnd(),...
Enums::PROCESS transactionIsOver(Enums::PROCESS)
transactionIsOver checks wheather currently finished process is last in current transaction
#define dbg()
Definition: debughelper.h:7
Enumerators for configuration and runtime items.
PROCESS
Definition: enums.h:16
@ PASS_INIT
Definition: enums.h:26
@ PASS_INSERT
Definition: enums.h:24
@ GIT_INIT
Definition: enums.h:17
@ PASS_COPY
Definition: enums.h:29
@ PASS_MOVE
Definition: enums.h:28
@ GIT_MOVE
Definition: enums.h:30
@ PASS_REMOVE
Definition: enums.h:25
@ GIT_COPY
Definition: enums.h:31
@ INVALID
Definition: enums.h:33
@ GIT_COMMIT
Definition: enums.h:19
@ GIT_RM
Definition: enums.h:20
@ PASS_SHOW
Definition: enums.h:23
@ GIT_ADD
Definition: enums.h:18
@ GIT_PULL
Definition: enums.h:21
@ GIT_PUSH
Definition: enums.h:22
Stores key info lines including validity, creation date and more.
Definition: userinfo.h:11
bool have_secret
UserInfo::have_secret secret key is available (can decrypt with this key)
Definition: userinfo.h:46
bool enabled
UserInfo::enabled.
Definition: userinfo.h:50
QString key_id
UserInfo::key_id hexadecimal representation.
Definition: userinfo.h:36