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) + "\"> ");
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) + "\"> ");
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) + "\"> ");
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) + "\"> ");
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