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

IJHack / QtPass / 1480

pending completion
1480

push

travis-ci-com

web-flow
Merge pull request #559 from ichthyosaurus/fix-520

Keep suffices when moving (to) a directory while imitiating pass

6 of 6 new or added lines in 1 file covered. (100.0%)

334 of 4803 relevant lines covered (6.95%)

0.96 hits per line

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

0.0
/src/imitatepass.cpp
1
#include "imitatepass.h"
2
#include "qtpasssettings.h"
3
#include <QDirIterator>
4
#include <QRegularExpression>
5
#include <utility>
6

7
#ifdef QT_DEBUG
8
#include "debughelper.h"
9
#endif
10

11
using namespace Enums;
12

13
/**
14
 * @brief ImitatePass::ImitatePass for situaions when pass is not available
15
 * we imitate the behavior of pass https://www.passwordstore.org/
16
 */
17
ImitatePass::ImitatePass() = default;
18

19
static QString pgit(const QString &path) {
×
20
  if (!QtPassSettings::getGitExecutable().startsWith("wsl "))
×
21
    return path;
22
  QString res = "$(wslpath " + path + ")";
×
23
  return res.replace('\\', '/');
×
24
}
25

26
static QString pgpg(const QString &path) {
×
27
  if (!QtPassSettings::getGpgExecutable().startsWith("wsl "))
×
28
    return path;
29
  QString res = "$(wslpath " + path + ")";
×
30
  return res.replace('\\', '/');
×
31
}
32

33
/**
34
 * @brief ImitatePass::GitInit git init wrapper
35
 */
36
void ImitatePass::GitInit() {
×
37
  executeGit(GIT_INIT, {"init", pgit(QtPassSettings::getPassStore())});
×
38
}
×
39

40
/**
41
 * @brief ImitatePass::GitPull git init wrapper
42
 */
43
void ImitatePass::GitPull() { executeGit(GIT_PULL, {"pull"}); }
×
44

45
/**
46
 * @brief ImitatePass::GitPull_b git pull wrapper
47
 */
48
void ImitatePass::GitPull_b() {
×
49
  exec.executeBlocking(QtPassSettings::getGitExecutable(), {"pull"});
×
50
}
×
51

52
/**
53
 * @brief ImitatePass::GitPush git init wrapper
54
 */
55
void ImitatePass::GitPush() {
×
56
  if (QtPassSettings::isUseGit()) {
×
57
    executeGit(GIT_PUSH, {"push"});
×
58
  }
59
}
×
60

61
/**
62
 * @brief ImitatePass::Show shows content of file
63
 */
64
void ImitatePass::Show(QString file) {
×
65
  file = QtPassSettings::getPassStore() + file + ".gpg";
×
66
  QStringList args = {"-d",      "--quiet",     "--yes",   "--no-encrypt-to",
67
                      "--batch", "--use-agent", pgpg(file)};
×
68
  executeGpg(PASS_SHOW, args);
×
69
}
×
70

71
/**
72
 * @brief ImitatePass::OtpGenerate generates an otp code
73
 */
74
void ImitatePass::OtpGenerate(QString file) {
×
75
#ifdef QT_DEBUG
76
  dbg() << "No OTP generation code for fake pass yet, attempting for file: " +
77
               file;
78
#else
79
  Q_UNUSED(file)
80
#endif
81
}
×
82

83
/**
84
 * @brief ImitatePass::Insert create new file with encrypted content
85
 *
86
 * @param file      file to be created
87
 * @param newValue  value to be stored in file
88
 * @param overwrite whether to overwrite existing file
89
 */
90
void ImitatePass::Insert(QString file, QString newValue, bool overwrite) {
×
91
  file = file + ".gpg";
×
92
  transactionHelper trans(this, PASS_INSERT);
×
93
  QStringList recipients = Pass::getRecipientList(file);
×
94
  if (recipients.isEmpty()) {
×
95
    //  TODO(bezet): probably throw here
96
    emit critical(tr("Can not edit"),
×
97
                  tr("Could not read encryption key to use, .gpg-id "
×
98
                     "file missing or invalid."));
99
    return;
100
  }
101
  QStringList args = {"--batch", "-eq", "--output", pgpg(file)};
×
102
  for (auto &r : recipients) {
×
103
    args.append("-r");
×
104
    args.append(r);
×
105
  };
106
  if (overwrite)
×
107
    args.append("--yes");
×
108
  args.append("-");
×
109
  executeGpg(PASS_INSERT, args, newValue);
×
110
  if (!QtPassSettings::isUseWebDav() && QtPassSettings::isUseGit()) {
×
111
    //    TODO(bezet) why not?
112
    if (!overwrite)
×
113
      executeGit(GIT_ADD, {"add", pgit(file)});
×
114
    QString path = QDir(QtPassSettings::getPassStore()).relativeFilePath(file);
×
115
    path.replace(QRegularExpression("\\.gpg$"), "");
×
116
    QString msg =
117
        QString(overwrite ? "Edit" : "Add") + " for " + path + " using QtPass.";
×
118
    GitCommit(file, msg);
×
119
  }
120
}
121

122
/**
123
 * @brief ImitatePass::GitCommit commit a file to git with an appropriate commit
124
 * message
125
 * @param file
126
 * @param msg
127
 */
128
void ImitatePass::GitCommit(const QString &file, const QString &msg) {
×
129
  if (file.isEmpty())
×
130
      executeGit(GIT_COMMIT, {"commit", "-m", msg});
×
131
  else
132
      executeGit(GIT_COMMIT, {"commit", "-m", msg, "--", pgit(file)});
×
133
}
×
134

135
/**
136
 * @brief ImitatePass::Remove custom implementation of "pass remove"
137
 */
138
void ImitatePass::Remove(QString file, bool isDir) {
×
139
  file = QtPassSettings::getPassStore() + file;
×
140
  transactionHelper trans(this, PASS_REMOVE);
×
141
  if (!isDir)
×
142
    file += ".gpg";
×
143
  if (QtPassSettings::isUseGit()) {
×
144
    executeGit(GIT_RM, {"rm", (isDir ? "-rf" : "-f"), pgit(file)});
×
145
    //  TODO(bezet): commit message used to have pass-like file name inside(ie.
146
    //  getFile(file, true)
147
    GitCommit(file, "Remove for " + file + " using QtPass.");
×
148
  } else {
149
    if (isDir) {
×
150
#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
151
      QDir dir(file);
×
152
      dir.removeRecursively();
×
153
#else
154
      removeDir(QtPassSettings::getPassStore() + file);
155
#endif
156
    } else
157
      QFile(file).remove();
×
158
  }
159
}
×
160

161
/**
162
 * @brief ImitatePass::Init initialize pass repository
163
 *
164
 * @param path      path in which new password-store will be created
165
 * @param users     list of users who shall be able to decrypt passwords in
166
 * path
167
 */
168
void ImitatePass::Init(QString path, const QList<UserInfo> &users) {
×
169
  QString gpgIdFile = path + ".gpg-id";
×
170
  QFile gpgId(gpgIdFile);
×
171
  bool addFile = false;
172
  transactionHelper trans(this, PASS_INIT);
×
173
  if (QtPassSettings::isAddGPGId(true)) {
×
174
    QFileInfo checkFile(gpgIdFile);
×
175
    if (!checkFile.exists() || !checkFile.isFile())
×
176
      addFile = true;
177
  }
178
  if (!gpgId.open(QIODevice::WriteOnly | QIODevice::Text)) {
×
179
    emit critical(tr("Cannot update"),
×
180
                  tr("Failed to open .gpg-id for writing."));
×
181
    return;
×
182
  }
183
  bool secret_selected = false;
184
  foreach (const UserInfo &user, users) {
×
185
    if (user.enabled) {
×
186
      gpgId.write((user.key_id + "\n").toUtf8());
×
187
      secret_selected |= user.have_secret;
×
188
    }
189
  }
190
  gpgId.close();
×
191
  if (!secret_selected) {
×
192
    emit critical(
×
193
        tr("Check selected users!"),
×
194
        tr("None of the selected keys have a secret key available.\n"
×
195
           "You will not be able to decrypt any newly added passwords!"));
196
    return;
×
197
  }
198

199
  if (!QtPassSettings::isUseWebDav() && QtPassSettings::isUseGit() &&
×
200
      !QtPassSettings::getGitExecutable().isEmpty()) {
×
201
    if (addFile)
×
202
      executeGit(GIT_ADD, {"add", pgit(gpgIdFile)});
×
203
    QString commitPath = gpgIdFile;
×
204
    commitPath.replace(QRegularExpression("\\.gpg$"), "");
×
205
    GitCommit(gpgIdFile, "Added " + commitPath + " using QtPass.");
×
206
  }
207
  reencryptPath(path);
×
208
}
209

210
/**
211
 * @brief ImitatePass::removeDir delete folder recursive.
212
 * @param dirName which folder.
213
 * @return was removal succesful?
214
 */
215
bool ImitatePass::removeDir(const QString &dirName) {
×
216
  bool result = true;
217
  QDir dir(dirName);
×
218

219
  if (dir.exists(dirName)) {
×
220
    Q_FOREACH (QFileInfo info,
×
221
               dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System |
222
                                     QDir::Hidden | QDir::AllDirs | QDir::Files,
223
                                 QDir::DirsFirst)) {
224
      if (info.isDir())
×
225
        result = removeDir(info.absoluteFilePath());
×
226
      else
227
        result = QFile::remove(info.absoluteFilePath());
×
228

229
      if (!result)
×
230
        return result;
×
231
    }
232
    result = dir.rmdir(dirName);
×
233
  }
234
  return result;
235
}
236

237
/**
238
 * @brief ImitatePass::reencryptPath reencrypt all files under the chosen
239
 * directory
240
 *
241
 * This is stil quite experimental..
242
 * @param dir
243
 */
244
void ImitatePass::reencryptPath(const QString &dir) {
×
245
  emit statusMsg(tr("Re-encrypting from folder %1").arg(dir), 3000);
×
246
  emit startReencryptPath();
×
247
  if (QtPassSettings::isAutoPull()) {
×
248
    //  TODO(bezet): move statuses inside actions?
249
    emit statusMsg(tr("Updating password-store"), 2000);
×
250
    GitPull_b();
×
251
  }
252
  QDir currentDir;
×
253
  QDirIterator gpgFiles(dir, QStringList() << "*.gpg", QDir::Files,
×
254
                        QDirIterator::Subdirectories);
×
255
  QStringList gpgId;
256
  while (gpgFiles.hasNext()) {
×
257
    QString fileName = gpgFiles.next();
×
258
    if (gpgFiles.fileInfo().path() != currentDir.path()) {
×
259
      gpgId = getRecipientList(fileName);
×
260
      gpgId.sort();
261
    }
262
    //  TODO(bezet): enable --with-colons for better future-proofness?
263
    QStringList args = {
264
        "-v",          "--no-secmem-warning", "--no-permission-warning",
265
        "--list-only", "--keyid-format=long", pgpg(fileName)};
×
266
    QString keys, err;
×
267
    exec.executeBlocking(QtPassSettings::getGpgExecutable(), args, &keys, &err);
×
268
    QStringList actualKeys;
269
    keys += err;
270
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
271
    QStringList key = keys.split(QRegularExpression("[\r\n]"), Qt::SkipEmptyParts);
×
272
#else
273
    QStringList key = keys.split(QRegularExpression("[\r\n]"), QString::SkipEmptyParts);
274
#endif
275
    QListIterator<QString> itr(key);
276
    while (itr.hasNext()) {
×
277
      QString current = itr.next();
×
278
      QStringList cur = current.split(" ");
×
279
      if (cur.length() > 4) {
×
280
        QString actualKey = cur.takeAt(4);
×
281
        if (actualKey.length() == 16) {
×
282
          actualKeys << actualKey;
283
        }
284
      }
285
    }
286
    actualKeys.sort();
287
    if (actualKeys != gpgId) {
×
288
      // dbg()<< actualKeys << gpgId << getRecipientList(fileName);
289
#ifdef QT_DEBUG
290
      dbg() << "reencrypt " << fileName << " for " << gpgId;
291
#endif
292
      QString local_lastDecrypt = "Could not decrypt";
×
293
      args = QStringList{
×
294
          "-d",      "--quiet",     "--yes",       "--no-encrypt-to",
295
          "--batch", "--use-agent", pgpg(fileName)};
×
296
      exec.executeBlocking(QtPassSettings::getGpgExecutable(), args,
×
297
                           &local_lastDecrypt);
298

299
      if (!local_lastDecrypt.isEmpty() &&
×
300
          local_lastDecrypt != "Could not decrypt") {
×
301
        if (local_lastDecrypt.right(1) != "\n")
×
302
          local_lastDecrypt += "\n";
×
303

304
        QStringList recipients = Pass::getRecipientList(fileName);
×
305
        if (recipients.isEmpty()) {
×
306
          emit critical(tr("Can not edit"),
×
307
                        tr("Could not read encryption key to use, .gpg-id "
×
308
                           "file missing or invalid."));
309
          return;
310
        }
311
        args =
312
            QStringList{"--yes", "--batch", "-eq", "--output", pgpg(fileName)};
×
313
        for (auto &i : recipients) {
×
314
          args.append("-r");
×
315
          args.append(i);
×
316
        }
317
        args.append("-");
×
318
        exec.executeBlocking(QtPassSettings::getGpgExecutable(), args,
×
319
                             local_lastDecrypt);
320

321
        if (!QtPassSettings::isUseWebDav() && QtPassSettings::isUseGit()) {
×
322
          exec.executeBlocking(QtPassSettings::getGitExecutable(),
×
323
                               {"add", pgit(fileName)});
×
324
          QString path =
325
              QDir(QtPassSettings::getPassStore()).relativeFilePath(fileName);
×
326
          path.replace(QRegularExpression("\\.gpg$"), "");
×
327
          exec.executeBlocking(QtPassSettings::getGitExecutable(),
×
328
                               {"commit", pgit(fileName), "-m",
329
                                "Edit for " + path + " using QtPass."});
×
330
        }
331

332
      } else {
333
#ifdef QT_DEBUG
334
        dbg() << "Decrypt error on re-encrypt";
335
#endif
336
      }
337
    }
338
  }
339
  if (QtPassSettings::isAutoPush()) {
×
340
    emit statusMsg(tr("Updating password-store"), 2000);
×
341
    //  TODO(bezet): this is non-blocking and shall be done outside
342
    GitPush();
×
343
  }
344
  emit endReencryptPath();
×
345
}
346

347
void ImitatePass::Move(const QString src, const QString dest,
×
348
                       const bool force) {
349
  transactionHelper trans(this, PASS_MOVE);
×
350
  QFileInfo srcFileInfo(src);
×
351
  QFileInfo destFileInfo(dest);
×
352
  QString destFile;
×
353
  QString srcFileBaseName = srcFileInfo.fileName();
×
354

355
  if (srcFileInfo.isFile()) {
×
356
    if (destFileInfo.isFile()) {
×
357
      if (!force) {
×
358
#ifdef QT_DEBUG
359
        dbg() << "Destination file already exists";
360
#endif
361
        return;
×
362
      }
363
    } else if (destFileInfo.isDir()) {
×
364
      destFile = QDir(dest).filePath(srcFileBaseName);
×
365
    } else {
366
      destFile = dest;
×
367
    }
368

369
    if (destFile.endsWith(".gpg", Qt::CaseInsensitive))
×
370
      destFile.chop(4); // make sure suffix is lowercase
×
371
    destFile.append(".gpg");
×
372
  } else if (srcFileInfo.isDir()) {
×
373
    if (destFileInfo.isDir()) {
×
374
      destFile = QDir(dest).filePath(srcFileBaseName);
×
375
    } else if (destFileInfo.isFile()) {
×
376
#ifdef QT_DEBUG
377
      dbg() << "Destination is a file";
378
#endif
379
      return;
380
    } else {
381
      destFile = dest;
×
382
    }
383
  } else {
384
#ifdef QT_DEBUG
385
    dbg() << "Source file does not exist";
386
#endif
387
    return;
388
  }
389

390
#ifdef QT_DEBUG
391
  dbg() << "Move Source: " << src;
392
  dbg() << "Move Destination: " << destFile;
393
#endif
394

395
  if (QtPassSettings::isUseGit()) {
×
396
    QStringList args;
397
    args << "mv";
×
398
    if (force) {
×
399
      args << "-f";
×
400
    }
401
    args << pgit(src);
×
402
    args << pgit(destFile);
×
403
    executeGit(GIT_MOVE, args);
×
404

405
    QString relSrc = QDir(QtPassSettings::getPassStore()).relativeFilePath(src);
×
406
    relSrc.replace(QRegularExpression("\\.gpg$"), "");
×
407
    QString relDest = QDir(QtPassSettings::getPassStore()).relativeFilePath(destFile);
×
408
    relDest.replace(QRegularExpression("\\.gpg$"), "");
×
409
    QString message = QString("Moved for %1 to %2 using QtPass.");
×
410
    message = message.arg(relSrc).arg(relDest);
×
411
    GitCommit("", message);
×
412
  } else {
413
    QDir qDir;
×
414
    if (force) {
×
415
      qDir.remove(destFile);
×
416
    }
417
    qDir.rename(src, destFile);
×
418
  }
419
}
420

421
void ImitatePass::Copy(const QString src, const QString dest,
×
422
                       const bool force) {
423
  QFileInfo destFileInfo(dest);
×
424
  transactionHelper trans(this, PASS_COPY);
×
425
  if (QtPassSettings::isUseGit()) {
×
426
    QStringList args;
427
    args << "cp";
×
428
    if (force) {
×
429
      args << "-f";
×
430
    }
431
    args << pgit(src);
×
432
    args << pgit(dest);
×
433
    executeGit(GIT_COPY, args);
×
434

435
    QString message = QString("copied from %1 to %2 using QTPass.");
×
436
    message = message.arg(src).arg(dest);
×
437
    GitCommit("", message);
×
438
  } else {
439
    QDir qDir;
×
440
    if (force) {
×
441
      qDir.remove(dest);
×
442
    }
443
    QFile::copy(src, dest);
×
444
  }
445
  // reecrypt all files under the new folder
446
  if (destFileInfo.isDir()) {
×
447
    reencryptPath(destFileInfo.absoluteFilePath());
×
448
  } else if (destFileInfo.isFile()) {
×
449
    reencryptPath(destFileInfo.dir().path());
×
450
  }
451
}
×
452

453
/**
454
 * @brief ImitatePass::executeGpg easy wrapper for running gpg commands
455
 * @param args
456
 */
457
void ImitatePass::executeGpg(PROCESS id, const QStringList &args, QString input,
×
458
                             bool readStdout, bool readStderr) {
459
  executeWrapper(id, QtPassSettings::getGpgExecutable(), args, std::move(input),
×
460
                 readStdout, readStderr);
×
461
}
×
462
/**
463
 * @brief ImitatePass::executeGit easy wrapper for running git commands
464
 * @param args
465
 */
466
void ImitatePass::executeGit(PROCESS id, const QStringList &args, QString input,
×
467
                             bool readStdout, bool readStderr) {
468
  executeWrapper(id, QtPassSettings::getGitExecutable(), args, std::move(input),
×
469
                 readStdout, readStderr);
×
470
}
×
471

472
/**
473
 * @brief ImitatePass::finished this function is overloaded to ensure
474
 *                              identical behaviour to RealPass ie. only PASS_*
475
 *                              processes are visible inside Pass::finish, so
476
 *                              that interface-wise it all looks the same
477
 * @param id
478
 * @param exitCode
479
 * @param out
480
 * @param err
481
 */
482
void ImitatePass::finished(int id, int exitCode, const QString &out,
×
483
                           const QString &err) {
484
#ifdef QT_DEBUG
485
  dbg() << "Imitate Pass";
486
#endif
487
  static QString transactionOutput;
×
488
  PROCESS pid = transactionIsOver(static_cast<PROCESS>(id));
×
489
  transactionOutput.append(out);
×
490

491
  if (exitCode == 0) {
×
492
    if (pid == INVALID)
×
493
      return;
494
  } else {
495
    while (pid == INVALID) {
×
496
      id = exec.cancelNext();
×
497
      if (id == -1) {
×
498
        //  this is probably irrecoverable and shall not happen
499
#ifdef QT_DEBUG
500
        dbg() << "No such transaction!";
501
#endif
502
        return;
503
      }
504
      pid = transactionIsOver(static_cast<PROCESS>(id));
×
505
    }
506
  }
507
  Pass::finished(pid, exitCode, transactionOutput, err);
×
508
  transactionOutput.clear();
509
}
510

511
/**
512
 * @brief executeWrapper    overrided so that every execution is a transaction
513
 * @param id
514
 * @param app
515
 * @param args
516
 * @param input
517
 * @param readStdout
518
 * @param readStderr
519
 */
520
void ImitatePass::executeWrapper(PROCESS id, const QString &app,
×
521
                                 const QStringList &args, QString input,
522
                                 bool readStdout, bool readStderr) {
523
  transactionAdd(id);
×
524
  Pass::executeWrapper(id, app, args, input, readStdout, readStderr);
×
525
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2023 Coveralls, Inc