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