1 /*
2     kleopatraapplication.cpp
3 
4     This file is part of Kleopatra, the KDE keymanager
5     SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB
6 
7     SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik
8     SPDX-FileContributor: Intevation GmbH
9 
10     SPDX-License-Identifier: GPL-2.0-or-later
11 */
12 
13 #include <config-kleopatra.h>
14 
15 #include "kleopatraapplication.h"
16 
17 #include "mainwindow.h"
18 #include "kleopatra_options.h"
19 #include "systrayicon.h"
20 #include "settings.h"
21 
22 #include <smartcard/readerstatus.h>
23 #include <conf/configuredialog.h>
24 
25 #include <Libkleo/GnuPG>
26 #include <utils/kdpipeiodevice.h>
27 #include <utils/log.h>
28 
29 #include <gpgme++/key.h>
30 
31 #include <Libkleo/FileSystemWatcher>
32 #include <Libkleo/KeyCache>
33 #include <Libkleo/Classify>
34 
35 #ifdef HAVE_USABLE_ASSUAN
36 # include <uiserver/uiserver.h>
37 #endif
38 
39 #include "commands/signencryptfilescommand.h"
40 #include "commands/decryptverifyfilescommand.h"
41 #include "commands/lookupcertificatescommand.h"
42 #include "commands/checksumcreatefilescommand.h"
43 #include "commands/checksumverifyfilescommand.h"
44 #include "commands/detailscommand.h"
45 #include "commands/newcertificatecommand.h"
46 
47 #include "dialogs/updatenotification.h"
48 
49 #include <KIconLoader>
50 #include <KLocalizedString>
51 #include "kleopatra_debug.h"
52 #include <KMessageBox>
53 #include <KWindowSystem>
54 
55 #include <QFile>
56 #include <QDir>
57 #include <QPointer>
58 
59 #include <memory>
60 #include <KSharedConfig>
61 
62 
63 #ifdef Q_OS_WIN
64 #include <QtPlatformHeaders/QWindowsWindowFunctions>
65 #endif
66 
67 using namespace Kleo;
68 using namespace Kleo::Commands;
69 
add_resources()70 static void add_resources()
71 {
72     KIconLoader::global()->addAppDir(QStringLiteral("libkleopatra"));
73     KIconLoader::global()->addAppDir(QStringLiteral("kwatchgnupg"));
74 }
75 
default_logging_options()76 static QList<QByteArray> default_logging_options()
77 {
78     QList<QByteArray> result;
79     result.push_back("io");
80     return result;
81 }
82 
83 class KleopatraApplication::Private
84 {
85     friend class ::KleopatraApplication;
86     KleopatraApplication *const q;
87 public:
Private(KleopatraApplication * qq)88     explicit Private(KleopatraApplication *qq)
89         : q(qq),
90           ignoreNewInstance(true),
91           firstNewInstance(true),
92           sysTray(nullptr)
93     {
94     }
~Private()95     ~Private() {
96 #ifndef QT_NO_SYSTEMTRAYICON
97         delete sysTray;
98 #endif
99     }
init()100     void init()
101     {
102         KDAB_SET_OBJECT_NAME(readerStatus);
103 #ifndef QT_NO_SYSTEMTRAYICON
104         sysTray = new SysTrayIcon();
105         sysTray->setFirstCardWithNullPin(readerStatus.firstCardWithNullPin());
106         sysTray->setAnyCardCanLearnKeys(readerStatus.anyCardCanLearnKeys());
107 
108         connect(&readerStatus, &SmartCard::ReaderStatus::firstCardWithNullPinChanged,
109                 sysTray, &SysTrayIcon::setFirstCardWithNullPin);
110         connect(&readerStatus, &SmartCard::ReaderStatus::anyCardCanLearnKeysChanged,
111                 sysTray, &SysTrayIcon::setAnyCardCanLearnKeys);
112 #endif
113     }
114 
115 private:
connectConfigureDialog()116     void connectConfigureDialog()
117     {
118         if (configureDialog) {
119             if (q->mainWindow()) {
120                 connect(configureDialog, SIGNAL(configCommitted()),
121                         q->mainWindow(), SLOT(slotConfigCommitted()));
122             }
123             connect(configureDialog, &ConfigureDialog::configCommitted,
124                     q, &KleopatraApplication::configurationChanged);
125         }
126     }
disconnectConfigureDialog()127     void disconnectConfigureDialog()
128     {
129         if (configureDialog) {
130             if (q->mainWindow()) {
131                 disconnect(configureDialog, SIGNAL(configCommitted()),
132                            q->mainWindow(), SLOT(slotConfigCommitted()));
133             }
134             disconnect(configureDialog, &ConfigureDialog::configCommitted,
135                        q, &KleopatraApplication::configurationChanged);
136         }
137     }
138 
139 public:
140     bool ignoreNewInstance;
141     bool firstNewInstance;
142     QPointer<ConfigureDialog> configureDialog;
143     QPointer<MainWindow> mainWindow;
144     SmartCard::ReaderStatus readerStatus;
145 #ifndef QT_NO_SYSTEMTRAYICON
146     SysTrayIcon *sysTray;
147 #endif
148     std::shared_ptr<KeyCache> keyCache;
149     std::shared_ptr<Log> log;
150     std::shared_ptr<FileSystemWatcher> watcher;
151 
152 public:
setupKeyCache()153     void setupKeyCache()
154     {
155         keyCache = KeyCache::mutableInstance();
156         watcher.reset(new FileSystemWatcher);
157 
158         watcher->whitelistFiles(gnupgFileWhitelist());
159         watcher->addPath(gnupgHomeDirectory());
160         watcher->setDelay(1000);
161         keyCache->addFileSystemWatcher(watcher);
162         keyCache->setGroupsConfig(QStringLiteral("kleopatragroupsrc"));
163         keyCache->setGroupsEnabled(Settings().groupsEnabled());
164     }
165 
setupLogging()166     void setupLogging()
167     {
168         log = Log::mutableInstance();
169 
170         const QByteArray envOptions = qgetenv("KLEOPATRA_LOGOPTIONS");
171         const bool logAll = envOptions.trimmed() == "all";
172         const QList<QByteArray> options = envOptions.isEmpty() ? default_logging_options() : envOptions.split(',');
173 
174         const QByteArray dirNative = qgetenv("KLEOPATRA_LOGDIR");
175         if (dirNative.isEmpty()) {
176             return;
177         }
178         const QString dir = QFile::decodeName(dirNative);
179         const QString logFileName = QDir(dir).absoluteFilePath(QStringLiteral("kleopatra.log.%1").arg(QCoreApplication::applicationPid()));
180         std::unique_ptr<QFile> logFile(new QFile(logFileName));
181         if (!logFile->open(QIODevice::WriteOnly | QIODevice::Append)) {
182             qCDebug(KLEOPATRA_LOG) << "Could not open file for logging: " << logFileName << "\nLogging disabled";
183             return;
184         }
185 
186         log->setOutputDirectory(dir);
187         if (logAll || options.contains("io")) {
188             log->setIOLoggingEnabled(true);
189         }
190         qInstallMessageHandler(Log::messageHandler);
191 
192 #ifdef HAVE_USABLE_ASSUAN
193         if (logAll || options.contains("pipeio")) {
194             KDPipeIODevice::setDebugLevel(KDPipeIODevice::Debug);
195         }
196         UiServer::setLogStream(log->logFile());
197 #endif
198 
199     }
200 };
201 
KleopatraApplication(int & argc,char * argv[])202 KleopatraApplication::KleopatraApplication(int &argc, char *argv[])
203     : QApplication(argc, argv), d(new Private(this))
204 {
205 }
206 
init()207 void KleopatraApplication::init()
208 {
209 #ifdef Q_OS_WIN
210     QWindowsWindowFunctions::setWindowActivationBehavior(
211             QWindowsWindowFunctions::AlwaysActivateWindow);
212 #endif
213     d->init();
214     add_resources();
215     d->setupKeyCache();
216     d->setupLogging();
217 #ifndef QT_NO_SYSTEMTRAYICON
218     d->sysTray->show();
219 #endif
220     setQuitOnLastWindowClosed(false);
221     KWindowSystem::allowExternalProcessWindowActivation();
222 }
223 
~KleopatraApplication()224 KleopatraApplication::~KleopatraApplication()
225 {
226     // main window doesn't receive "close" signal and cannot
227     // save settings before app exit
228     delete d->mainWindow;
229 
230     // work around kdelibs bug https://bugs.kde.org/show_bug.cgi?id=162514
231     KSharedConfig::openConfig()->sync();
232 }
233 
234 namespace
235 {
236 using Func = void (KleopatraApplication::*)(const QStringList &, GpgME::Protocol);
237 }
238 
slotActivateRequested(const QStringList & arguments,const QString & workingDirectory)239 void KleopatraApplication::slotActivateRequested(const QStringList &arguments,
240         const QString &workingDirectory)
241 {
242     QCommandLineParser parser;
243 
244     kleopatra_options(&parser);
245     QString err;
246     if (!arguments.isEmpty() && !parser.parse(arguments)) {
247         err = parser.errorText();
248     } else if (arguments.isEmpty()) {
249         // KDBusServices omits the application name if no other
250         // arguments are provided. In that case the parser prints
251         // a warning.
252         parser.parse(QStringList() << QCoreApplication::applicationFilePath());
253     }
254 
255     if (err.isEmpty()) {
256         err = newInstance(parser, workingDirectory);
257     }
258 
259     if (!err.isEmpty()) {
260         KMessageBox::sorry(nullptr, err.toHtmlEscaped(), i18n("Failed to execute command"));
261         Q_EMIT setExitValue(1);
262         return;
263     }
264     Q_EMIT setExitValue(0);
265 }
266 
newInstance(const QCommandLineParser & parser,const QString & workingDirectory)267 QString KleopatraApplication::newInstance(const QCommandLineParser &parser,
268         const QString &workingDirectory)
269 {
270     if (d->ignoreNewInstance) {
271         qCDebug(KLEOPATRA_LOG) << "New instance ignored because of ignoreNewInstance";
272         return QString();
273     }
274 
275     QStringList files;
276     const QDir cwd = QDir(workingDirectory);
277     bool queryMode = parser.isSet(QStringLiteral("query")) || parser.isSet(QStringLiteral("search"));
278 
279     // Query and Search treat positional arguments differently, see below.
280     if (!queryMode) {
281         const auto positionalArguments = parser.positionalArguments();
282         for (const QString &file : positionalArguments) {
283             // We do not check that file exists here. Better handle
284             // these errors in the UI.
285             if (QFileInfo(file).isAbsolute()) {
286                 files << file;
287             } else {
288                 files << cwd.absoluteFilePath(file);
289             }
290         }
291     }
292 
293     GpgME::Protocol protocol = GpgME::UnknownProtocol;
294 
295     if (parser.isSet(QStringLiteral("openpgp"))) {
296         qCDebug(KLEOPATRA_LOG) << "found OpenPGP";
297         protocol = GpgME::OpenPGP;
298     }
299 
300     if (parser.isSet(QStringLiteral("cms"))) {
301         qCDebug(KLEOPATRA_LOG) << "found CMS";
302         if (protocol == GpgME::OpenPGP) {
303             return i18n("Ambiguous protocol: --openpgp and --cms");
304         }
305         protocol = GpgME::CMS;
306     }
307 
308     // Check for Parent Window id
309     WId parentId = 0;
310     if (parser.isSet(QStringLiteral("parent-windowid"))) {
311 #ifdef Q_OS_WIN
312         // WId is not a portable type as it is a pointer type on Windows.
313         // casting it from an integer is ok though as the values are guaranteed to
314         // be compatible in the documentation.
315         parentId = reinterpret_cast<WId>(parser.value(QStringLiteral("parent-windowid")).toUInt());
316 #else
317         parentId = parser.value(QStringLiteral("parent-windowid")).toUInt();
318 #endif
319     }
320 
321     // Handle openpgp4fpr URI scheme
322     QString needle;
323     if (queryMode) {
324         needle = parser.positionalArguments().join(QLatin1Char(' '));
325     }
326     if (needle.startsWith(QLatin1String("openpgp4fpr:"))) {
327         needle.remove(0, 12);
328     }
329 
330     // Check for --search command.
331     if (parser.isSet(QStringLiteral("search"))) {
332         // This is an extra command instead of a combination with the
333         // similar query to avoid changing the older query commands behavior
334         // and query's "show details if a certificate exist or search on a
335         // keyserver" logic is hard to explain and use consistently.
336         if (needle.isEmpty()) {
337             return i18n("No search string specified for --search");
338         }
339         auto const cmd = new LookupCertificatesCommand(needle, nullptr);
340         cmd->setParentWId(parentId);
341         cmd->start();
342         return QString();
343     }
344 
345     // Check for --query command
346     if (parser.isSet(QStringLiteral("query"))) {
347         if (needle.isEmpty()) {
348             return i18n("No fingerprint argument specified for --query");
349         }
350         auto cmd = Command::commandForQuery(needle);
351         cmd->setParentWId(parentId);
352         cmd->start();
353         return QString();
354     }
355 
356     // Check for --gen-key command
357     if (parser.isSet(QStringLiteral("gen-key"))) {
358         auto cmd = new NewCertificateCommand(nullptr);
359         cmd->setParentWId(parentId);
360         cmd->setProtocol(protocol);
361         cmd->start();
362         return QString();
363     }
364 
365     // Check for --config command
366     if (parser.isSet(QStringLiteral("config"))) {
367         openConfigDialogWithForeignParent(parentId);
368         return QString();
369     }
370 
371     static const QMap<QString, Func> funcMap {
372         { QStringLiteral("import-certificate"), &KleopatraApplication::importCertificatesFromFile },
373         { QStringLiteral("encrypt"), &KleopatraApplication::encryptFiles },
374         { QStringLiteral("sign"), &KleopatraApplication::signFiles },
375         { QStringLiteral("encrypt-sign"), &KleopatraApplication::signEncryptFiles },
376         { QStringLiteral("sign-encrypt"), &KleopatraApplication::signEncryptFiles },
377         { QStringLiteral("decrypt"), &KleopatraApplication::decryptFiles },
378         { QStringLiteral("verify"), &KleopatraApplication::verifyFiles },
379         { QStringLiteral("decrypt-verify"), &KleopatraApplication::decryptVerifyFiles },
380         { QStringLiteral("checksum"), &KleopatraApplication::checksumFiles },
381     };
382 
383     QString found;
384     Q_FOREACH (const QString &opt, funcMap.keys()) {
385         if (parser.isSet(opt) && found.isEmpty()) {
386             found = opt;
387         } else if (parser.isSet(opt)) {
388             return i18n(R"(Ambiguous commands "%1" and "%2")", found, opt);
389         }
390     }
391 
392     QStringList errors;
393     if (!found.isEmpty()) {
394         if (files.empty()) {
395             return i18n("No files specified for \"%1\" command", found);
396         }
397         qCDebug(KLEOPATRA_LOG) << "found" << found;
398         (this->*funcMap.value(found))(files, protocol);
399     } else {
400         if (files.empty()) {
401             if (!(d->firstNewInstance && isSessionRestored())) {
402                 qCDebug(KLEOPATRA_LOG) << "openOrRaiseMainWindow";
403                 openOrRaiseMainWindow();
404             }
405         } else {
406             for (const QString& fileName : std::as_const(files)) {
407                 QFileInfo fi(fileName);
408                 if (!fi.isReadable()) {
409                     errors << i18n("Cannot read \"%1\"", fileName);
410                 }
411             }
412             Q_FOREACH (Command *cmd, Command::commandsForFiles(files)) {
413                 if (parentId) {
414                     cmd->setParentWId(parentId);
415                 } else {
416                     MainWindow *mw = mainWindow();
417                     if (!mw) {
418                         mw = new MainWindow;
419                         mw->setAttribute(Qt::WA_DeleteOnClose);
420                         setMainWindow(mw);
421                         d->connectConfigureDialog();
422                     }
423                     cmd->setParentWidget(mw);
424                 }
425                 cmd->start();
426             }
427         }
428     }
429     d->firstNewInstance = false;
430 
431 #ifdef Q_OS_WIN
432     // On Windows we might be started from the
433     // explorer in any working directory. E.g.
434     // a double click on a file. To avoid preventing
435     // the folder from deletion we set the
436     // working directory to the users homedir.
437     QDir::setCurrent(QDir::homePath());
438 #endif
439 
440     return errors.join(QLatin1Char('\n'));
441 }
442 
443 #ifndef QT_NO_SYSTEMTRAYICON
sysTrayIcon() const444 const SysTrayIcon *KleopatraApplication::sysTrayIcon() const
445 {
446     return d->sysTray;
447 }
448 
sysTrayIcon()449 SysTrayIcon *KleopatraApplication::sysTrayIcon()
450 {
451     return d->sysTray;
452 }
453 #endif
454 
mainWindow() const455 const MainWindow *KleopatraApplication::mainWindow() const
456 {
457     return d->mainWindow;
458 }
459 
mainWindow()460 MainWindow *KleopatraApplication::mainWindow()
461 {
462     return d->mainWindow;
463 }
464 
setMainWindow(MainWindow * mainWindow)465 void KleopatraApplication::setMainWindow(MainWindow *mainWindow)
466 {
467     if (mainWindow == d->mainWindow) {
468         return;
469     }
470 
471     d->disconnectConfigureDialog();
472 
473     d->mainWindow = mainWindow;
474 #ifndef QT_NO_SYSTEMTRAYICON
475     d->sysTray->setMainWindow(mainWindow);
476 #endif
477 
478     d->connectConfigureDialog();
479 }
480 
open_or_raise(QWidget * w)481 static void open_or_raise(QWidget *w)
482 {
483     if (w->isMinimized()) {
484         KWindowSystem::unminimizeWindow(w->winId());
485         w->raise();
486     } else if (w->isVisible()) {
487         w->raise();
488     } else {
489         w->show();
490     }
491 }
492 
toggleMainWindowVisibility()493 void KleopatraApplication::toggleMainWindowVisibility()
494 {
495     if (mainWindow()) {
496         mainWindow()->setVisible(!mainWindow()->isVisible());
497     } else {
498         openOrRaiseMainWindow();
499     }
500 }
501 
restoreMainWindow()502 void KleopatraApplication::restoreMainWindow()
503 {
504     qCDebug(KLEOPATRA_LOG) << "restoring main window";
505 
506     // Sanity checks
507     if (!isSessionRestored()) {
508         qCDebug(KLEOPATRA_LOG) << "Not in session restore";
509         return;
510     }
511 
512     if (mainWindow()) {
513         qCDebug(KLEOPATRA_LOG) << "Already have main window";
514         return;
515     }
516 
517     auto mw = new MainWindow;
518     if (KMainWindow::canBeRestored(1)) {
519         // restore to hidden state, Mainwindow::readProperties() will
520         // restore saved visibility.
521         mw->restore(1, false);
522     }
523 
524     mw->setAttribute(Qt::WA_DeleteOnClose);
525     setMainWindow(mw);
526     d->connectConfigureDialog();
527 
528 }
529 
openOrRaiseMainWindow()530 void KleopatraApplication::openOrRaiseMainWindow()
531 {
532     MainWindow *mw = mainWindow();
533     if (!mw) {
534         mw = new MainWindow;
535         mw->setAttribute(Qt::WA_DeleteOnClose);
536         setMainWindow(mw);
537         d->connectConfigureDialog();
538     }
539     open_or_raise(mw);
540     UpdateNotification::checkUpdate(mw);
541 }
542 
openConfigDialogWithForeignParent(WId parentWId)543 void KleopatraApplication::openConfigDialogWithForeignParent(WId parentWId)
544 {
545     if (!d->configureDialog) {
546         d->configureDialog = new ConfigureDialog;
547         d->configureDialog->setAttribute(Qt::WA_DeleteOnClose);
548         d->connectConfigureDialog();
549     }
550 
551     // This is similar to what the commands do.
552     if (parentWId) {
553         if (QWidget *pw = QWidget::find(parentWId)) {
554             d->configureDialog->setParent(pw, d->configureDialog->windowFlags());
555         } else {
556             d->configureDialog->setAttribute(Qt::WA_NativeWindow, true);
557             KWindowSystem::setMainWindow(d->configureDialog->windowHandle(), parentWId);
558         }
559     }
560 
561     open_or_raise(d->configureDialog);
562 
563     // If we have a parent we want to raise over it.
564     if (parentWId) {
565         d->configureDialog->raise();
566     }
567 }
568 
openOrRaiseConfigDialog()569 void KleopatraApplication::openOrRaiseConfigDialog()
570 {
571     openConfigDialogWithForeignParent(0);
572 }
573 
574 #ifndef QT_NO_SYSTEMTRAYICON
startMonitoringSmartCard()575 void KleopatraApplication::startMonitoringSmartCard()
576 {
577     d->readerStatus.startMonitoring();
578 }
579 #endif // QT_NO_SYSTEMTRAYICON
580 
importCertificatesFromFile(const QStringList & files,GpgME::Protocol)581 void KleopatraApplication::importCertificatesFromFile(const QStringList &files, GpgME::Protocol /*proto*/)
582 {
583     openOrRaiseMainWindow();
584     if (!files.empty()) {
585         mainWindow()->importCertificatesFromFile(files);
586     }
587 }
588 
encryptFiles(const QStringList & files,GpgME::Protocol proto)589 void KleopatraApplication::encryptFiles(const QStringList &files, GpgME::Protocol proto)
590 {
591     auto const cmd = new SignEncryptFilesCommand(files, nullptr);
592     cmd->setEncryptionPolicy(Force);
593     cmd->setSigningPolicy(Allow);
594     if (proto != GpgME::UnknownProtocol) {
595         cmd->setProtocol(proto);
596     }
597     cmd->start();
598 }
599 
signFiles(const QStringList & files,GpgME::Protocol proto)600 void KleopatraApplication::signFiles(const QStringList &files, GpgME::Protocol proto)
601 {
602     auto const cmd = new SignEncryptFilesCommand(files, nullptr);
603     cmd->setSigningPolicy(Force);
604     cmd->setEncryptionPolicy(Deny);
605     if (proto != GpgME::UnknownProtocol) {
606         cmd->setProtocol(proto);
607     }
608     cmd->start();
609 }
610 
signEncryptFiles(const QStringList & files,GpgME::Protocol proto)611 void KleopatraApplication::signEncryptFiles(const QStringList &files, GpgME::Protocol proto)
612 {
613     auto const cmd = new SignEncryptFilesCommand(files, nullptr);
614     if (proto != GpgME::UnknownProtocol) {
615         cmd->setProtocol(proto);
616     }
617     cmd->start();
618 }
619 
decryptFiles(const QStringList & files,GpgME::Protocol)620 void KleopatraApplication::decryptFiles(const QStringList &files, GpgME::Protocol /*proto*/)
621 {
622     auto const cmd = new DecryptVerifyFilesCommand(files, nullptr);
623     cmd->setOperation(Decrypt);
624     cmd->start();
625 }
626 
verifyFiles(const QStringList & files,GpgME::Protocol)627 void KleopatraApplication::verifyFiles(const QStringList &files, GpgME::Protocol /*proto*/)
628 {
629     auto const cmd = new DecryptVerifyFilesCommand(files, nullptr);
630     cmd->setOperation(Verify);
631     cmd->start();
632 }
633 
decryptVerifyFiles(const QStringList & files,GpgME::Protocol)634 void KleopatraApplication::decryptVerifyFiles(const QStringList &files, GpgME::Protocol /*proto*/)
635 {
636     auto const cmd = new DecryptVerifyFilesCommand(files, nullptr);
637     cmd->start();
638 }
639 
checksumFiles(const QStringList & files,GpgME::Protocol)640 void KleopatraApplication::checksumFiles(const QStringList &files, GpgME::Protocol /*proto*/)
641 {
642     QStringList verifyFiles, createFiles;
643 
644     for (const QString &file : files) {
645         if (isChecksumFile(file)) {
646             verifyFiles << file;
647         } else {
648             createFiles << file;
649         }
650     }
651 
652     if (!verifyFiles.isEmpty()) {
653         auto const cmd = new ChecksumVerifyFilesCommand(verifyFiles, nullptr);
654         cmd->start();
655     }
656     if (!createFiles.isEmpty()) {
657         auto const cmd = new ChecksumCreateFilesCommand(createFiles, nullptr);
658         cmd->start();
659     }
660 }
661 
setIgnoreNewInstance(bool ignore)662 void KleopatraApplication::setIgnoreNewInstance(bool ignore)
663 {
664     d->ignoreNewInstance = ignore;
665 }
666 
ignoreNewInstance() const667 bool KleopatraApplication::ignoreNewInstance() const
668 {
669     return d->ignoreNewInstance;
670 }
671