8#include <QElapsedTimer>
10#include <QRegularExpression>
48 static constexpr int kGrepThreadTimeoutMs = 5000;
49 for (QThread *t : std::as_const(m_grepThreads))
50 if (t && t->isRunning())
51 t->requestInterruption();
52 QElapsedTimer elapsed;
54 for (QThread *t : std::as_const(m_grepThreads)) {
55 if (t && t->isRunning()) {
57 kGrepThreadTimeoutMs -
static_cast<int>(elapsed.elapsed());
64static auto pgit(
const QString &path) -> QString {
68 QString res =
"$(wslpath " + path +
")";
69 return res.replace(
'\\',
'/');
72static auto pgpg(
const QString &path) -> QString {
76 QString res =
"$(wslpath " + path +
")";
77 return res.replace(
'\\',
'/');
113 QStringList args = {
"-d",
"--quiet",
"--yes",
"--no-encrypt-to",
114 "--batch",
"--use-agent", pgpg(file)};
123 dbg() <<
"No OTP generation code for fake pass yet, attempting for file: " +
138 file = file +
".gpg";
141 emit
critical(tr(
"Check .gpgid file signature!"),
142 tr(
"Signature for %1 is invalid.").arg(gpgIdPath));
147 if (recipients.isEmpty()) {
150 tr(
"Could not read encryption key to use, .gpg-id "
151 "file missing or invalid."));
154 QStringList args = {
"--batch",
"--status-fd",
"2",
155 "-eq",
"--output", pgpg(file)};
156 for (
auto &r : recipients) {
161 args.append(
"--yes");
173 QString(overwrite ?
"Edit" :
"Add") +
" for " + path +
" using QtPass.";
185 if (file.isEmpty()) {
206 gitCommit(file,
"Remove for " + path +
" using QtPass.");
210 dir.removeRecursively();
212 QFile(file).remove();
227 QStringList{
"--status-fd=1",
"--list-secret-keys"} + signingKeys;
232 dbg() <<
"GPG list-secret-keys failed with code:" << result;
236 for (
auto &key : signingKeys) {
237 if (out.contains(
"[GNUPG:] KEY_CONSIDERED " + key)) {
257 const QList<UserInfo> &users) {
258 QFile gpgId(gpgIdFile);
259 if (!gpgId.open(QIODevice::WriteOnly | QIODevice::Text)) {
261 tr(
"Failed to open .gpg-id for writing."));
264 bool secret_selected =
false;
265 for (
const UserInfo &user : users) {
267 gpgId.write((user.key_id +
"\n").toUtf8());
268 secret_selected |= user.have_secret;
272 if (!secret_selected) {
274 tr(
"Check selected users!"),
275 tr(
"None of the selected keys have a secret key available.\n"
276 "You will not be able to decrypt any newly added passwords!"));
294 const QStringList &signingKeys) ->
bool {
298 if (!signingKeys.isEmpty()) {
300 if (signingKeys.size() > 1) {
301 dbg() <<
"Multiple signing keys configured; using only the first key:"
302 << signingKeys.first();
305 args.append(QStringList{
"--default-key", signingKeys.first()});
307 args.append(QStringList{
"--yes",
"--detach-sign", gpgIdFile});
312 dbg() <<
"GPG signing failed with code:" << result;
314 emit
critical(tr(
"GPG signing failed!"),
315 tr(
"Failed to sign %1.").arg(gpgIdFile));
319 emit
critical(tr(
"Check .gpgid file signature!"),
320 tr(
"Signature for %1 is invalid.").arg(gpgIdFile));
340 const QString &gpgIdSigFile,
bool addFile,
345 QString commitPath = gpgIdFile;
347 gitCommit(gpgIdFile,
"Added " + commitPath +
" using QtPass.");
352 commitPath = gpgIdSigFile;
353 commitPath.replace(QRegularExpression(
"\\.gpg$"),
"");
354 gitCommit(gpgIdSigFile,
"Added " + commitPath +
" using QtPass.");
371#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
372 QStringList signingKeys =
375 QStringList signingKeys =
378 QString gpgIdSigFile = path +
".gpg-id.sig";
379 bool addSigFile =
false;
380 if (!signingKeys.isEmpty()) {
382 emit
critical(tr(
"No signing key!"),
383 tr(
"None of the secret signing keys is available.\n"
384 "You will not be able to change the user list!"));
387 QFileInfo checkFile(gpgIdSigFile);
388 if (!checkFile.exists() || !checkFile.isFile()) {
393 QString gpgIdFile = path +
".gpg-id";
394 bool addFile =
false;
397 QFileInfo checkFile(gpgIdFile);
398 if (!checkFile.exists() || !checkFile.isFile()) {
404 if (!signingKeys.isEmpty()) {
412 gitAddGpgId(gpgIdFile, gpgIdSigFile, addFile, addSigFile);
423#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
424 QStringList signingKeys =
427 QStringList signingKeys =
430 if (signingKeys.isEmpty()) {
435 QStringList{
"--verify",
"--status-fd=1", pgpg(file) +
".sig", pgpg(file)};
440 dbg() <<
"GPG verify failed with code:" << result;
444 QRegularExpression re(
445 R
"(^\[GNUPG:\] VALIDSIG ([A-F0-9]{40}) .* ([A-F0-9]{40})\r?$)",
446 QRegularExpression::MultilineOption);
447 QRegularExpressionMatch m = re.match(out);
451 QStringList fingerprints = m.capturedTexts();
452 fingerprints.removeFirst();
453 for (
auto &key : signingKeys) {
454 if (fingerprints.contains(key)) {
470 if (dir.exists(dirName)) {
471 for (
const QFileInfo &info :
472 dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden |
473 QDir::AllDirs | QDir::Files,
476 result =
removeDir(info.absoluteFilePath());
478 result = QFile::remove(info.absoluteFilePath());
485 result = dir.rmdir(dirName);
498 QStringList &gpgIdFilesVerified,
499 QStringList &gpgId) ->
bool {
501 if (gpgIdFilesVerified.contains(gpgIdPath)) {
505 emit
critical(tr(
"Check .gpgid file signature!"),
506 tr(
"Signature for %1 is invalid.").arg(gpgIdPath));
509 gpgIdFilesVerified.append(gpgIdPath);
529 "-v",
"--no-secmem-warning",
"--no-permission-warning",
530 "--list-only",
"--keyid-format=long", pgpg(fileName)};
535 if (result != 0 && keys.isEmpty() && err.isEmpty()) {
536 return QStringList();
538 QStringList actualKeys;
540#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
545 QListIterator<QString> itr(key);
546 while (itr.hasNext()) {
547 QString current = itr.next();
548 QStringList cur = current.split(
" ");
549 if (cur.length() > 4) {
550 QString actualKey = cur.takeAt(4);
551 if (actualKey.length() == 16) {
552 actualKeys << actualKey;
574 const QStringList &recipients) ->
bool {
576 dbg() <<
"reencrypt " << fileName <<
" for " << recipients;
578 QString local_lastDecrypt;
580 "-d",
"--quiet",
"--yes",
"--no-encrypt-to",
581 "--batch",
"--use-agent", pgpg(fileName)};
583 args, &local_lastDecrypt);
585 if (result != 0 || local_lastDecrypt.isEmpty()) {
587 dbg() <<
"Decrypt error on re-encrypt for:" << fileName;
592 if (local_lastDecrypt.right(1) !=
"\n") {
593 local_lastDecrypt +=
"\n";
597 if (recipients.isEmpty()) {
599 tr(
"Could not read encryption key to use, .gpg-id "
600 "file missing or invalid."));
605 QString tempPath = fileName +
".reencrypt.tmp";
606 args = QStringList{
"--yes",
"--batch",
"-eq",
"--output", pgpg(tempPath)};
607 for (
const auto &i : recipients) {
617 dbg() <<
"Encrypt error on re-encrypt for:" << fileName;
619 QFile::remove(tempPath);
624 QString verifyOutput;
625 args = QStringList{
"-d",
"--quiet",
"--batch",
"--use-agent", pgpg(tempPath)};
628 if (result != 0 || verifyOutput.isEmpty()) {
630 dbg() <<
"Verification failed for:" << tempPath;
632 QFile::remove(tempPath);
636 if (verifyOutput.trimmed() != local_lastDecrypt.trimmed()) {
638 dbg() <<
"Verification content mismatch for:" << tempPath;
640 QFile::remove(tempPath);
646 QString backupPath = fileName +
".reencrypt.bak";
647 if (!QFile::rename(fileName, backupPath)) {
649 dbg() <<
"Failed to backup original file:" << fileName;
651 QFile::remove(tempPath);
654 if (!QFile::rename(tempPath, fileName)) {
656 dbg() <<
"Failed to rename temp file to:" << fileName;
659 QFile::rename(backupPath, fileName);
660 QFile::remove(tempPath);
662 tr(
"Re-encryption failed"),
663 tr(
"Failed to replace %1. Original has been restored.").arg(fileName));
667 QFile::remove(backupPath);
671 {
"add", pgit(fileName)});
676 {
"commit", pgit(fileName),
"-m",
677 "Re-encrypt for " + path +
" using QtPass."});
692 emit
statusMsg(tr(
"Creating backup commit"), 2000);
698 tr(
"Backup commit failed"),
699 tr(
"Could not inspect git status. Re-encryption was aborted."));
702 if (!statusOut.trimmed().isEmpty()) {
705 git, {
"commit",
"-m",
"Backup before re-encryption"}) != 0) {
706 emit
critical(tr(
"Backup commit failed"),
707 tr(
"Re-encryption was aborted because a git backup could "
729 emit
statusMsg(tr(
"Re-encrypting from folder %1").arg(dir), 3000);
732 emit
statusMsg(tr(
"Updating password-store"), 2000);
743 QDirIterator gpgFiles(dir, QStringList() <<
"*.gpg", QDir::Files,
744 QDirIterator::Subdirectories);
745 QStringList gpgIdFilesVerified;
747 int successCount = 0;
749 while (gpgFiles.hasNext()) {
750 QString fileName = gpgFiles.next();
751 if (gpgFiles.fileInfo().path() != currentDir.path()) {
756 if (gpgId.isEmpty() && !gpgIdFilesVerified.isEmpty()) {
757 emit
critical(tr(
"GPG ID verification failed"),
758 tr(
"Could not verify .gpg-id for directory."));
764 if (actualKeys != gpgId) {
769 emit
critical(tr(
"Re-encryption failed"),
770 tr(
"Failed to re-encrypt %1").arg(fileName));
776 emit
statusMsg(tr(
"Re-encryption completed: %1 succeeded, %2 failed")
782 tr(
"Re-encryption completed: %1 files re-encrypted").arg(successCount),
787 emit
statusMsg(tr(
"Updating password-store"), 2000);
808 const QString &dest,
bool force)
810 QFileInfo srcFileInfo(src);
811 QFileInfo destFileInfo(dest);
813 QString srcFileBaseName = srcFileInfo.fileName();
815 if (srcFileInfo.isFile()) {
816 if (destFileInfo.isFile()) {
819 dbg() <<
"Destination file already exists";
824 }
else if (destFileInfo.isDir()) {
825 destFile = QDir(dest).filePath(srcFileBaseName);
830 if (destFile.endsWith(
".gpg", Qt::CaseInsensitive)) {
833 destFile.append(
".gpg");
834 }
else if (srcFileInfo.isDir()) {
835 if (destFileInfo.isDir()) {
836 destFile = QDir(dest).filePath(srcFileBaseName);
837 }
else if (destFileInfo.isFile()) {
839 dbg() <<
"Destination is a file";
847 dbg() <<
"Source file does not exist";
873 args << pgit(destFile);
881 QString message = QString(
"Moved for %1 to %2 using QtPass.");
882 message = message.arg(relSrc, relDest);
901 if (destFile.isEmpty()) {
906 dbg() <<
"Move Source: " << src;
907 dbg() <<
"Move Destination: " << destFile;
915 qDir.remove(destFile);
917 qDir.rename(src, destFile);
935 QFileInfo destFileInfo(dest);
947 QString message = QString(
"Copied from %1 to %2 using QtPass.");
948 message = message.arg(src, dest);
955 QFile::copy(src, dest);
958 if (destFileInfo.isDir()) {
960 }
else if (destFileInfo.isFile()) {
970 bool readStdout,
bool readStderr) {
972 readStdout, readStderr);
980 bool readStdout,
bool readStderr) {
982 readStdout, readStderr);
996 const QString &err) {
998 dbg() <<
"Imitate Pass";
1000 static QString transactionOutput;
1002 transactionOutput.append(out);
1004 if (exitCode == 0) {
1010 id =
exec.cancelNext();
1014 dbg() <<
"No such transaction!";
1022 transactionOutput.clear();
1035 const QStringList &args, QString input,
1036 bool readStdout,
bool readStderr) {
1044auto ImitatePass::grepMatchFile(
const QStringList &env,
const QString &gpgExe,
1045 const QString &filePath,
1046 const QRegularExpression &rx) -> QStringList {
1050 {
"-d",
"--quiet",
"--yes",
"--no-encrypt-to",
1051 "--batch",
"--use-agent", pgpg(filePath)},
1053 if (rc != 0 || plaintext.isEmpty())
1055 QStringList matches;
1056 for (
const QString &line : plaintext.split(
'\n')) {
1057 QString candidate = line;
1058 if (candidate.endsWith(
'\r'))
1060 const QString t = candidate.trimmed();
1061 if (!t.isEmpty() && candidate.contains(rx))
1070auto ImitatePass::grepScanStore(
const QStringList &env,
const QString &gpgExe,
1071 const QString &storeDir,
1072 const QRegularExpression &rx)
1073 -> QList<QPair<QString, QStringList>> {
1074 QList<QPair<QString, QStringList>> results;
1075 QDirIterator it(storeDir, QStringList() <<
"*.gpg", QDir::Files,
1076 QDirIterator::Subdirectories);
1077 while (it.hasNext()) {
1078 if (QThread::currentThread()->isInterruptionRequested())
1080 const QString filePath = it.next();
1081 const QStringList matches = grepMatchFile(env, gpgExe, filePath, rx);
1082 if (!matches.isEmpty()) {
1083 QString entry = QDir(storeDir).relativeFilePath(filePath);
1084 if (entry.endsWith(QLatin1String(
".gpg")))
1086 results.append({entry, matches});
1100 for (QThread *t : std::as_const(m_grepThreads))
1101 if (t && t->isRunning())
1102 t->requestInterruption();
1108 const int seq = ++m_grepSeq;
1117 if (pattern.trimmed().isEmpty()) {
1118 QMetaObject::invokeMethod(
1121 if (m_grepSeq == seq)
1124 Qt::QueuedConnection);
1128 const QRegularExpression rx(
1129 pattern, caseInsensitive ? QRegularExpression::CaseInsensitiveOption
1130 : QRegularExpression::PatternOptions{});
1131 if (!rx.isValid()) {
1132 QMetaObject::invokeMethod(
1135 if (m_grepSeq == seq)
1138 Qt::QueuedConnection);
1143 const QStringList env =
exec.environment();
1144 QPointer<ImitatePass> self(
this);
1146 auto emitResults = [self, seq](QList<QPair<QString, QStringList>> results) {
1149 QMetaObject::invokeMethod(
1151 [self, seq, results = std::move(results)]() {
1152 if (self && self->m_grepSeq == seq)
1153 emit self->finishedGrep(results);
1155 Qt::QueuedConnection);
1158 QThread *thread = QThread::create([gpgExe, storeDir, env, rx, emitResults]() {
1159 emitResults(grepScanStore(env, gpgExe, storeDir, rx));
1162 m_grepThreads.append(thread);
1163 connect(thread, &QThread::finished, thread, &QObject::deleteLater);
1164 connect(thread, &QThread::finished,
this,
1165 [
this, thread]() { m_grepThreads.removeOne(thread); });
static auto executeBlocking(const QString &app, const QStringList &args, const QString &input=QString(), QString *process_out=nullptr, QString *process_err=nullptr) -> int
Executor::executeBlocking blocking version of the executor, takes input and presents it as stdin.
RAII helper for wrapping operations in transactions.
auto createBackupCommit() -> bool
Create git backup commit before re-encryption.
void OtpGenerate(QString file) override
Generate OTP.
void GitInit() override
Initialize Git repository.
void executeGpg(PROCESS id, const QStringList &args, QString input=QString(), bool readStdout=true, bool readStderr=true)
Execute GPG command.
void GitPull_b() override
Pull with rebase.
~ImitatePass() override
Destructor.
auto checkSigningKeys(const QStringList &signingKeys) -> bool
Check if signing keys are valid.
ImitatePass()
Construct ImitatePass instance.
auto reencryptSingleFile(const QString &fileName, const QStringList &recipients) -> bool
Re-encrypt single file with new recipients.
auto verifyGpgIdFile(const QString &file) -> bool
Verify .gpg-id file exists and is valid.
void executeGit(PROCESS id, const QStringList &args, QString input=QString(), bool readStdout=true, bool readStderr=true)
Execute git command.
void Init(QString path, const QList< UserInfo > &users) override
Initialize store.
void Grep(QString pattern, bool caseInsensitive=false) override
Search all password content by GPG-decrypting each .gpg file.
auto verifyGpgIdForDir(const QString &file, QStringList &gpgIdFilesVerified, QStringList &gpgId) -> bool
Verify .gpg-id file for a directory.
void Insert(QString file, QString newValue, bool overwrite=false) override
Insert new password.
auto removeDir(const QString &dirName) -> bool
Remove directory recursively.
void executeMoveGit(const QString &src, const QString &destFile, bool force)
Execute git move operation.
void Move(const QString src, const QString dest, const bool force=false) override
Move password file.
void Copy(const QString src, const QString dest, const bool force=false) override
Copy password file.
void endReencryptPath()
Emitted after finishing re-encryption.
void finished(int id, int exitCode, const QString &out, const QString &err) override
Handle process completion.
void executeWrapper(PROCESS id, const QString &app, const QStringList &args, QString input, bool readStdout=true, bool readStderr=true) override
Execute command wrapper.
auto signGpgIdFile(const QString &gpgIdFile, const QStringList &signingKeys) -> bool
Sign .gpg-id file with signing keys.
void Remove(QString file, bool isDir=false) override
Remove password.
void GitPull() override
Pull from remote.
void Show(QString file) override
Show decrypted password.
auto resolveMoveDestination(const QString &src, const QString &dest, bool force) -> QString
Resolve destination for move operation.
void GitPush() override
Push to remote.
void writeGpgIdFile(const QString &gpgIdFile, const QList< UserInfo > &users)
Write recipients to .gpg-id file.
void gitAddGpgId(const QString &gpgIdFile, const QString &gpgIdSigFile, bool addFile, bool addSigFile)
Add .gpg-id to git staging.
void startReencryptPath()
Emitted before starting re-encryption.
auto getKeysFromFile(const QString &fileName) -> QStringList
Read recipients from file.
void reencryptPath(const QString &dir)
Re-encrypt entire directory.
void gitCommit(const QString &file, const QString &msg)
Commit changes to git.
void critical(const QString &, const QString &)
Emit critical error.
void statusMsg(const QString &, int)
Emit status message.
void executeWrapper(PROCESS id, const QString &app, const QStringList &args, bool readStdout=true, bool readStderr=true)
Execute external wrapper command.
static auto getRecipientList(const QString &for_file) -> QStringList
Get list of recipients for a password file.
void finishedGrep(const QList< QPair< QString, QStringList > > &results)
Emitted when grep finishes with matching results.
static auto getGpgIdPath(const QString &for_file) -> QString
Get .gpg-id file path for a password file.
virtual void finished(int id, int exitCode, const QString &out, const QString &err)
Handle process completion.
static auto isAutoPull(const bool &defaultValue=QVariant().toBool()) -> bool
Check whether automatic pull is enabled.
static auto isUseGit(const bool &defaultValue=QVariant().toBool()) -> bool
Check whether Git integration is enabled.
static auto getPassStore(const QString &defaultValue=QVariant().toString()) -> QString
Get password store directory path.
static auto isAddGPGId(const bool &defaultValue=QVariant().toBool()) -> bool
Get whether to auto-add GPG ID when receiving files.
static auto getGpgExecutable(const QString &defaultValue=QVariant().toString()) -> QString
Get GPG executable path.
static auto getPassSigningKey(const QString &defaultValue=QVariant().toString()) -> QString
Get GPG signing key for pass.
static auto isUseWebDav(const bool &defaultValue=QVariant().toBool()) -> bool
Check whether WebDAV integration is enabled.
static auto isAutoPush(const bool &defaultValue=QVariant().toBool()) -> bool
Check whether automatic push is enabled.
static auto getGitExecutable(const QString &defaultValue=QVariant().toString()) -> QString
Get git executable path.
static auto endsWithGpg() -> const QRegularExpression &
Returns a regex to match .gpg file extensions.
static auto newLinesRegex() -> const QRegularExpression &
Returns a regex to match newline characters.
auto transactionIsOver(Enums::PROCESS) -> Enums::PROCESS
transactionIsOver checks wheather currently finished process is last in current transaction
void transactionAdd(Enums::PROCESS)
transactionAdd If called after call to transactionStart() and before transactionEnd(),...
Debug utilities for QtPass.
#define dbg()
Simple debug macro that includes file and line number.
Stores key info lines including validity, creation date and more.