1 /*
2     cryptographyplugin.cpp  -  description
3 
4     Copyright (c) 2002-2004 by Olivier Goffart        <ogoffart@kde.org>
5     Copyright (c) 2007      by Charles Connell        <charles@connells.org>
6 
7     Kopete    (c) 2002-2007 by the Kopete developers <kopete-devel@kde.org>
8 
9     ***************************************************************************
10     *                                                                         *
11     *   This program is free software; you can redistribute it and/or modify  *
12     *   it under the terms of the GNU General Public License as published by  *
13     *   the Free Software Foundation; either version 2 of the License, or     *
14     *   (at your option) any later version.                                   *
15     *                                                                         *
16     ***************************************************************************
17 */
18 #include "cryptographyplugin.h"
19 
20 // qt stuff
21 #include <QList>
22 
23 // kde stuff
24 #include <kapplication.h>
25 #include <kdebug.h>
26 #include <QAction>
27 #include <kpluginfactory.h>
28 #include <kaboutdata.h>
29 #include <kiconloader.h>
30 #include <kmessagebox.h>
31 #include <kactioncollection.h>
32 
33 // kopete stuff
34 #include "kopetemetacontact.h"
35 #include "kopetecontactlist.h"
36 #include "kopetechatsessionmanager.h"
37 #include "kopetesimplemessagehandler.h"
38 #include "kopeteuiglobal.h"
39 #include "kopetecontact.h"
40 #include "kopeteprotocol.h"
41 #include "kopetemessageevent.h"
42 #include "kabcpersistence.h"
43 
44 // crypto stuff
45 //#include <qgpgme/cryptobackendfactory.h>
46 #include <qgpgme/decryptverifyjob.h>
47 #include <qgpgme/decryptjob.h>
48 #include <qgpgme/verifyopaquejob.h>
49 #include <qgpgme/encryptjob.h>
50 #include <qgpgme/signencryptjob.h>
51 #include <qgpgme/signjob.h>
52 #include <qgpgme/keylistjob.h>
53 #include <qgpgme/job.h>
54 #include <gpgme++/decryptionresult.h>
55 #include <gpgme++/verificationresult.h>
56 #include <gpgme++/keylistresult.h>
57 #include <gpgme++/signingresult.h>
58 #include <gpgme++/encryptionresult.h>
59 #include <gpgme++/key.h>
60 
61 // kabc stuff
62 //#include <kabc/addressbook.h>
63 
64 // our own stuff
65 #include "cryptographyselectuserkey.h"
66 #include "cryptographyguiclient.h"
67 #include "exportkeys.h"
68 #include "cryptographymessagehandler.h"
69 #include "cryptographysettings.h"
70 #include "ui_kabckeyselectorbase.h"
71 
72 CryptographyPlugin *CryptographyPlugin::mPluginStatic = 0L;
73 
K_PLUGIN_FACTORY(CryptographyPluginFactory,registerPlugin<CryptographyPlugin> ();)74 K_PLUGIN_FACTORY(CryptographyPluginFactory, registerPlugin<CryptographyPlugin>();
75                  )
76 K_EXPORT_PLUGIN(CryptographyPluginFactory("kopete_cryptography"))
77 
78 CryptographyPlugin::CryptographyPlugin (QObject *parent, const QVariantList & /*args*/)
79     : Kopete::Plugin(CryptographyPluginFactory::componentData(), parent)
80 {
81     if (!mPluginStatic) {
82         mPluginStatic = this;
83     }
84 
85     // set up slots to handle incoming and outgoing messages
86     mInboundHandler = new CryptographyMessageHandlerFactory(Kopete::Message::Inbound,
87                                                             Kopete::MessageHandlerFactory::InStageToSent, this, SLOT(slotIncomingMessage(Kopete::MessageEvent*)));
88     connect(Kopete::ChatSessionManager::self(),
89             SIGNAL(aboutToSend(Kopete::Message&)),
90             SLOT(slotOutgoingMessage(Kopete::Message&)));
91 
92     // actions in the contact list
93     QAction *action = new QAction(QIcon::fromTheme("document-encrypt"), i18nc("@action", "&Select Public Key..."), this);
94     actionCollection()->addAction("contactSelectKey", action);
95     connect(action, SIGNAL(triggered(bool)), this, SLOT(slotSelectContactKey()));
96     connect(Kopete::ContactList::self(), SIGNAL(metaContactSelected(bool)), action, SLOT(setEnabled(bool)));
97     action->setEnabled(Kopete::ContactList::self()->selectedMetaContacts().count() == 1);
98 
99     action = new QAction(QIcon::fromTheme("document-export-key"), i18nc("@action", "&Export Public Keys To Address Book..."), this);
100     actionCollection()->addAction("exportKey", action);
101     connect(action, SIGNAL(triggered(bool)), this, SLOT(slotExportSelectedMetaContactKeys()));
102     connect(Kopete::ContactList::self(), SIGNAL(metaContactSelected(bool)), action, SLOT(setEnabled(bool)));
103     action->setEnabled(Kopete::ContactList::self()->selectedMetaContacts().count() == 1);
104 
105     setXMLFile("cryptographyui.rc");
106 
107     // add functionality to chat window when one opens
108     connect(Kopete::ChatSessionManager::self(), SIGNAL(chatSessionCreated(Kopete::ChatSession*)), SLOT(slotNewKMM(Kopete::ChatSession*)));
109 
110     //Add GUI action to all already existing kmm (if the plugin is launched when kopete already running)
111     QList<Kopete::ChatSession *> sessions = Kopete::ChatSessionManager::self()->sessions();
112     foreach (Kopete::ChatSession *session, sessions) {
113         slotNewKMM(session);
114     }
115 }
116 
~CryptographyPlugin()117 CryptographyPlugin::~CryptographyPlugin()
118 {
119     delete mInboundHandler;
120     mPluginStatic = 0L;
121 }
122 
123 // just like ::self(). return pointer to singleton
plugin()124 CryptographyPlugin *CryptographyPlugin::plugin()
125 {
126     return mPluginStatic;
127 }
128 
129 // handling an "incoming" message. this means any message to be displayed in the chat window, including ones sent by the user
slotIncomingMessage(Kopete::MessageEvent * messageEvent)130 void CryptographyPlugin::slotIncomingMessage(Kopete::MessageEvent *messageEvent)
131 {
132     Kopete::Message msg = messageEvent->message();
133     QString body = msg.plainBody();
134 
135     if (!((
136               body.startsWith(QString::fromLatin1("-----BEGIN PGP MESSAGE----"))
137               || body.startsWith(QString::fromLatin1("-----BEGIN PGP SIGNED MESSAGE-----"))
138               ) && (
139               body.contains(QString::fromLatin1("-----END PGP MESSAGE----"))
140               || body.contains(QString::fromLatin1("-----END PGP SIGNATURE-----"))
141               ))) {
142         return;
143     }
144 
145     kDebug(14303) << "processing " << body;
146 
147     // launch a crypto job, and store it with the message, so that when the job finishes, we know what message it was for
148     const Kleo::CryptoBackendFactory *cpf = Kleo::CryptoBackendFactory::instance();
149     Q_ASSERT(cpf);
150     const Kleo::CryptoBackend::Protocol *proto = cpf->openpgp();
151     Q_ASSERT(proto);
152 
153     Kleo::DecryptVerifyJob *decryptVerifyJob = proto->decryptVerifyJob();
154     connect(decryptVerifyJob, SIGNAL(result(GpgME::DecryptionResult,GpgME::VerificationResult,QByteArray)), this,
155             SLOT(slotIncomingMessageContinued(GpgME::DecryptionResult,GpgME::VerificationResult,QByteArray)));
156     mCurrentJobs.insert(decryptVerifyJob, msg);
157     decryptVerifyJob->start(body.toLatin1());
158 
159     // message is no longer needed, it is killed. We will see it again when the crypto job is done and it comes out of mCurrentJobs
160     messageEvent->discard();
161 }
162 
163 // this is called when the incoming crypto job is done
slotIncomingMessageContinued(const GpgME::DecryptionResult & decryptionResult,const GpgME::VerificationResult & verificationResult,const QByteArray & plainText)164 void CryptographyPlugin::slotIncomingMessageContinued(const GpgME::DecryptionResult &decryptionResult, const GpgME::VerificationResult &verificationResult, const QByteArray &plainText)
165 {
166     Kopete::Message msg = mCurrentJobs.take(static_cast<Kleo::Job *>(sender()));
167 
168     QString body = plainText;
169 
170     if (!body.isEmpty()) {
171         // if was signed *and* encrypted, this will be true
172         if (verificationResult.numSignatures() && decryptionResult.numRecipients()) {
173             finalizeMessage(msg, body, verificationResult, true /*encrytped*/);
174         }
175         // was not signed *and* encrypted, may be one or the other. launch a job to see about both possibilities
176         else {
177             const Kleo::CryptoBackendFactory *cpf = Kleo::CryptoBackendFactory::instance();
178             Q_ASSERT(cpf);
179             const Kleo::CryptoBackend::Protocol *proto = cpf->openpgp();
180             Q_ASSERT(proto);
181 
182             Kleo::DecryptJob *decryptJob = proto->decryptJob();
183             connect(decryptJob, SIGNAL(result(GpgME::DecryptionResult,QByteArray)), this, SLOT(slotIncomingEncryptedMessageContinued(GpgME::DecryptionResult,QByteArray)));
184             mCurrentJobs.insert(decryptJob, msg);
185             decryptJob->start(msg.plainBody().toLatin1());
186 
187             Kleo::VerifyOpaqueJob *verifyJob = proto->verifyOpaqueJob();
188             connect(verifyJob, SIGNAL(result(GpgME::VerificationResult,QByteArray)), this, SLOT(slotIncomingSignedMessageContinued(GpgME::VerificationResult,QByteArray)));
189             mCurrentJobs.insert(verifyJob, msg);
190             verifyJob->start(msg.plainBody().toLatin1());
191         }
192     }
193 }
194 
195 // if message was only encrypted, this will be called
slotIncomingEncryptedMessageContinued(const GpgME::DecryptionResult & decryptionResult,const QByteArray & plainText)196 void CryptographyPlugin::slotIncomingEncryptedMessageContinued(const GpgME::DecryptionResult &decryptionResult, const QByteArray &plainText)
197 {
198     Kopete::Message msg = mCurrentJobs.take(static_cast<Kleo::Job *>(sender()));
199 
200     QString body = plainText;
201 
202     if (!body.isEmpty()) {
203         if (decryptionResult.numRecipients()) {
204             finalizeMessage(msg, body, GpgME::VerificationResult(), true /*encrypted*/);
205         }
206     }
207 }
208 
209 // if message was only signed, this will be called
slotIncomingSignedMessageContinued(const GpgME::VerificationResult & verificationResult,const QByteArray & plainText)210 void CryptographyPlugin::slotIncomingSignedMessageContinued(const GpgME::VerificationResult &verificationResult, const QByteArray &plainText)
211 {
212     Kopete::Message msg = mCurrentJobs.take(static_cast<Kleo::Job *>(sender()));
213 
214     QString body = plainText;
215 
216     if ((!body.isEmpty()) && (verificationResult.numSignatures())) {
217         finalizeMessage(msg, body, verificationResult, false /*encrypted*/);
218     }
219 }
220 
221 // apply signature icons and put message in chat window
finalizeMessage(Kopete::Message & msg,const QString & intendedBody,const GpgME::VerificationResult & verificationResult,bool encrypted)222 void CryptographyPlugin::finalizeMessage(Kopete::Message &msg, const QString &intendedBody, const GpgME::VerificationResult &verificationResult, bool encrypted)
223 {
224     QString body = intendedBody;
225     // turn our plaintext body into html, so then it makes sense to stick HTML tags in it
226     if (!Qt::mightBeRichText(body)) {
227         body = Qt::convertFromPlainText(body, Qt::WhiteSpaceNormal);
228     }
229     body = body.remove(QRegExp("<p[^>]*>", Qt::CaseInsensitive));
230     body = body.remove(QRegExp("</p>", Qt::CaseInsensitive));
231 
232     // apply crypto state icons
233     // use lowest common denominator; entire message is considered invalid if one signature is
234     GpgME::Signature::Validity validity = (GpgME::Signature::Validity)GpgME::Signature::Unknown;
235     bool firstTime = true;
236     std::vector<GpgME::Signature> signatures = verificationResult.signatures();
237     for (size_t i = 0; i < verificationResult.signatures().size(); i++) {
238         kDebug(14303) << "signature" << i << "validity is" << signatures[i].validityAsString() << "from" << signatures[i].fingerprint();
239         if (validity > signatures[i].validity() || firstTime) {
240             validity = signatures[i].validity();
241             firstTime = false;
242         }
243     }
244 
245     if (verificationResult.numSignatures()) {
246         if (validity == GpgME::Signature::Ultimate || validity == GpgME::Signature::Full) {
247             body.prepend("<img src=\"" + KIconLoader::global()->iconPath("security-high", KIconLoader::Small) + "\">&nbsp;");
248             msg.addClass("cryptography:signedvalid");
249             kDebug(14303) << "message has fully valid signatures";
250         } else if (validity == GpgME::Signature::Marginal) {
251             body.prepend("<img src=\"" + KIconLoader::global()->iconPath("security-medium", KIconLoader::Small) + "\">&nbsp;");
252             msg.addClass("cryptography:signedmarginal");
253             kDebug(14303) << "message has marginally signatures";
254         } else {
255             body.prepend("<img src=\"" + KIconLoader::global()->iconPath("security-low", KIconLoader::Small) + "\">&nbsp;");
256             msg.addClass("crytography:signedinvalid");
257             kDebug(14303) << "message has unverified signatures";
258         }
259     }
260 
261     if (encrypted) {
262         body.prepend("<img src=\"" + KIconLoader::global()->iconPath("object-locked", KIconLoader::Small) + "\">&nbsp;");
263         msg.addClass("cryptography:encrypted");
264         kDebug(14303) << "message was encrypted";
265     }
266 
267     msg.setHtmlBody(body);
268     msg.manager()->appendMessage(msg);
269 }
270 
271 // encrypt and or sign a message to be sent
slotOutgoingMessage(Kopete::Message & msg)272 void CryptographyPlugin::slotOutgoingMessage(Kopete::Message &msg)
273 {
274     if (msg.direction() != Kopete::Message::Outbound) {
275         return;
276     }
277 
278     QStringList encryptingKeysPattern;
279     std::vector<GpgME::Key> encryptingKeys;
280     std::vector<GpgME::Key> signingKeys;
281     QList<Kopete::Contact *> contactlist = msg.to();
282     bool signing = ((msg.to().first()->pluginData(this, "sign_messages")) == "on");
283     bool encrypting = ((msg.to().first()->pluginData(this, "encrypt_messages")) == "on");
284     QByteArray result;
285 
286     if (!(signing || encrypting)) {
287         return;
288     }
289 
290     kDebug(14303) << (signing ? "signing" : "") << (encrypting ? "encrypting" : "") << "message " << msg.plainBody();
291 
292     const Kleo::CryptoBackendFactory *cpf = Kleo::CryptoBackendFactory::instance();
293     Q_ASSERT(cpf);
294     const Kleo::CryptoBackend::Protocol *proto = cpf->openpgp();
295     Q_ASSERT(proto);
296 
297     // create std::vector with list of signing keys (really just the one the user has set)
298     if (signing) {
299         Kleo::KeyListJob *listJob = proto->keyListJob();
300         listJob->exec(QStringList(CryptographySettings::privateKeyFingerprint()), true /*secretOnly*/, signingKeys);
301     }
302 
303     // create QStringList of recpients' keys, then convert that (using a KeyListJob) into a std::vector
304     if (encrypting) {
305         foreach (Kopete::Contact *c, contactlist) {
306             QString tmpKey;
307             if (c->metaContact()) {
308                 tmpKey = c->metaContact()->pluginData(this, "gpgKey");
309             }
310             encryptingKeysPattern.append(tmpKey);
311         }
312         // encrypt to self so we can decrypt during slotIncomingMessage()
313         encryptingKeysPattern.append(CryptographySettings::privateKeyFingerprint());
314         Kleo::KeyListJob *listJob = proto->keyListJob();
315         listJob->exec(encryptingKeysPattern, false /*secretOnly*/, encryptingKeys);
316     }
317     // do both signing and encrypting at once
318     if (signing && encrypting) {
319         Kleo::SignEncryptJob *job = proto->signEncryptJob(true /*armor*/);
320         job->exec(signingKeys, encryptingKeys, msg.plainBody().toLatin1(), true, result);
321     }
322     // sign message body
323     else if (signing) {
324         kDebug(14303) << "clearsign:" << (CryptographySettings::clearSignMode() ? "true" : "false");
325         Kleo::SignJob *job = proto->signJob(true /*armor*/);
326         job->exec(signingKeys, msg.plainBody().toLatin1(), CryptographySettings::clearSignMode() ? GpgME::Clearsigned : GpgME::NormalSignatureMode, result);
327     }
328     // encrypt message body to all recipients
329     else if (encrypting) {
330         Kleo::EncryptJob *job = proto->encryptJob(true /*armor*/);
331         job->exec(encryptingKeys, msg.plainBody().toLatin1(), true, result);
332     }
333 
334     if (!result.isEmpty()) {
335         msg.setPlainBody(result);
336         if (encrypting) {
337             msg.addClass("encrypted");
338         }
339         if (signing) {
340             msg.addClass("signed");
341         }
342     } else {
343         kDebug(14303) << "empty result";
344     }
345 }
346 
347 // Contruct dialog to show/edit metacontact's public key.
slotSelectContactKey()348 void CryptographyPlugin::slotSelectContactKey()
349 {
350     Kopete::MetaContact *m = Kopete::ContactList::self()->selectedMetaContacts().first();
351     if (!m) {
352         return;
353     }
354     // key we already have
355     QString key = m->pluginData(this, "gpgKey");
356     QPointer <CryptographySelectUserKey> opts = new CryptographySelectUserKey(key, m);
357     opts->exec();
358     if (opts && opts->result()) {
359         key = opts->publicKey();
360         m->setPluginData(this, "gpgKey", key);
361     }
362     delete opts;
363 }
364 
365 // construct crytography toolbars/buttons in a newly opened chat window
slotNewKMM(Kopete::ChatSession * KMM)366 void CryptographyPlugin::slotNewKMM(Kopete::ChatSession *KMM)
367 {
368     CryptographyGUIClient *gui = new CryptographyGUIClient(KMM);
369     connect(this, SIGNAL(destroyed(QObject*)), gui, SLOT(deleteLater()));
370 
371     // warn about unfriendly protocols
372     if (KMM->protocol()) {
373         QString protocol(KMM->protocol()->metaObject()->className());
374         if (protocol != "Kopete::Protocol") {
375             if (!supportedProtocols().contains(protocol)) {
376                 KMessageBox::information(0,
377                                          i18nc("@info",
378                                                "This protocol %1 may not work with messages that are encrypted. This is because encrypted messages are very long, and the server or peer may reject them due to their length. To avoid being signed off or your account being warned or temporarily suspended, turn off encryption.",
379                                                protocol),
380                                          i18n("Cryptography Unsupported Protocol %1", protocol), "Warn about unsupported " + protocol);
381             }
382         }
383     }
384 }
385 
getKabcKeys(QString uid)386 QStringList CryptographyPlugin::getKabcKeys(QString uid)
387 {
388     KContacts::Addressee addressee = Kopete::KABCPersistence::self()->addressBook()->findByUid(uid);
389     QStringList keys;
390 
391     // each 'if' block here is one way of getting a key.
392 
393     // this is the field used by kaddressbook
394     if (!(addressee.custom("KADDRESSBOOK", "OPENPGPFP")).isEmpty()) {
395         keys << addressee.custom("KADDRESSBOOK", "OPENPGPFP");
396     }
397 
398     // this is the standard key field in Addressee, (which no app seems to use, but here it is anyways)
399     if (!(addressee.key(KContacts::Key::PGP).textData()).isEmpty()) {
400         keys << addressee.key(KContacts::Key::PGP).textData();
401     }
402 
403     // remove duplicates
404     if (keys.count() >= 2) {
405         if (keys.at(0) == keys.at(1)) {
406             keys.removeAt(1);
407         }
408     }
409 
410     kDebug(14303) << "keys found in address book for contact " << addressee.assembledName() << ": " << keys;
411 
412     return keys;
413 }
414 
415 // show dialog to allow user to check which key they want
kabcKeySelector(QString displayName,QString addresseeName,QStringList keys,QWidget * parent)416 QString CryptographyPlugin::kabcKeySelector(QString displayName, QString addresseeName, QStringList keys, QWidget *parent)
417 {
418     // just a Yes/No about whether to accept the key
419     if (keys.count() == 1) {
420         if (KMessageBox::questionYesNo(parent,
421                                        i18nc("@info", "Cryptography plugin has found an encryption key for %1 (%2) in your KDE address book. Do you want to use key %3 as this contact's public key? ",
422                                              displayName, addresseeName,
423                                              keys.first().right(8).prepend("0x")),
424                                        i18n("Public Key Found")) == KMessageBox::Yes) {
425             return keys.first();
426         }
427     }
428     // allow for selection of key out of many
429     if (keys.count() > 1) {
430         QPointer <KDialog> dialog = new KDialog(parent);
431         QPointer <QWidget> w = new QWidget(dialog);
432         Ui::KabcKeySelectorUI ui;
433         ui.setupUi(w);
434         dialog->setCaption(i18n("Public Keys Found"));
435         dialog->setButtons(KDialog::Ok | KDialog::Cancel);
436         dialog->setMainWidget(w);
437         ui.label->setText(i18nc("@info", "Cryptography plugin has found multiple encryption keys for %1 (%2) in your KDE address book. To use one of these keys, select it and choose OK.",
438                                 displayName, addresseeName));
439         for (int i = 0; i < keys.count(); i++) {
440             ui.keyList->addItem(new QListWidgetItem(QIcon::fromTheme("application-pgp-keys"), keys[i].right(8).prepend("0x"), ui.keyList));
441         }
442         ui.keyList->addItems(keys);
443         QString ret;
444         if (dialog->exec()) {
445             ret = ui.keyList->currentItem()->text();
446         }
447         delete dialog;
448         if (!ret.isEmpty()) {
449             return ret;
450         }
451     }
452     return QString();
453 }
454 
455 // export the public keys of the chat session's contacts
slotExportSelectedMetaContactKeys()456 void CryptographyPlugin::slotExportSelectedMetaContactKeys()
457 {
458     QPointer <ExportKeys> dialog = new ExportKeys(Kopete::ContactList::self()->selectedMetaContacts(), Kopete::UI::Global::mainWidget());
459     dialog->exec();
460     delete dialog;
461 }
462 
463 #include "cryptographyplugin.moc"
464 
465 // vim: set noet ts=4 sts=4 sw=4:
466