Changing the behavior how Quassel Events are processed.
[quassel.git] / src / core / cipher.cpp
1 /*
2   This file has been derived from Konversation, the KDE IRC client.
3   You can redistribute it and/or modify it under the terms of the
4   GNU General Public License as published by the Free Software Foundation;
5   either version 2 of the License, or (at your option) any later version.
6 */
7
8 /*
9   Copyright (C) 1997 Robey Pointer <robeypointer@gmail.com>
10   Copyright (C) 2005 Ismail Donmez <ismail@kde.org>
11   Copyright (C) 2009 Travis McHenry <tmchenryaz@cox.net>
12   Copyright (C) 2009 Johannes Huber <johu@gmx.de>
13 */
14
15 #include "cipher.h"
16 #include "logger.h"
17
18
19 Cipher::Cipher()
20 {
21   m_primeNum = QCA::BigInteger("12745216229761186769575009943944198619149164746831579719941140425076456621824834322853258804883232842877311723249782818608677050956745409379781245497526069657222703636504651898833151008222772087491045206203033063108075098874712912417029101508315117935752962862335062591404043092163187352352197487303798807791605274487594646923");
22   setType("blowfish");
23 }
24
25 Cipher::Cipher(QByteArray key, QString cipherType)
26 {
27   m_primeNum = QCA::BigInteger("12745216229761186769575009943944198619149164746831579719941140425076456621824834322853258804883232842877311723249782818608677050956745409379781245497526069657222703636504651898833151008222772087491045206203033063108075098874712912417029101508315117935752962862335062591404043092163187352352197487303798807791605274487594646923");
28   setKey(key);
29   setType(cipherType);
30 }
31
32 Cipher::~Cipher()
33 {}
34
35 bool Cipher::setKey(QByteArray key)
36 {
37   if(key.isEmpty())
38     return false;
39
40   if(key.mid(0,4).toLower() == "ecb:")
41   {
42     m_cbc = false;
43     m_key = key.mid(4);
44   }
45   //strip cbc: if included
46   else if(key.mid(0,4).toLower() == "cbc:")
47   {
48     m_cbc = true;
49     m_key = key.mid(4);
50   }
51   else
52   {
53 //    if(Preferences::self()->encryptionType())
54 //      m_cbc = true;
55 //    else
56     m_cbc = false;
57     m_key = key;
58   }
59   return true;
60 }
61
62 bool Cipher::setType(const QString &type)
63 {
64   //TODO check QCA::isSupported()
65   m_type = type;
66   return true;
67 }
68
69 QByteArray Cipher::decrypt(QByteArray cipherText)
70 {
71   QByteArray pfx = "";
72   bool error = false; // used to flag non cbc, seems like good practice not to parse w/o regard for set encryption type
73
74   //if we get cbc
75   if(cipherText.mid(0,5) == "+OK *")
76   {
77     //if we have cbc
78     if(m_cbc)
79       cipherText = cipherText.mid(5);
80     //if we don't
81     else
82     {
83       cipherText = cipherText.mid(5);
84       pfx = "ERROR_NONECB: ";
85       error = true;
86     }
87   }
88   //if we get ecb
89   else if(cipherText.mid(0,4) == "+OK " || cipherText.mid(0,5) == "mcps ")
90   {
91     //if we had cbc
92     if(m_cbc)
93     {
94       cipherText = (cipherText.mid(0,4) == "+OK ") ? cipherText.mid(4) : cipherText.mid(5);
95       pfx = "ERROR_NONCBC: ";
96       error = true;
97     }
98     //if we don't
99     else
100     {
101       if(cipherText.mid(0,4) == "+OK ")
102         cipherText = cipherText.mid(4);
103       else
104         cipherText = cipherText.mid(5);
105     }
106   }
107   //all other cases we fail
108   else
109     return cipherText;
110
111   QByteArray temp;
112   // (if cbc and no error we parse cbc) || (if ecb and error we parse cbc)
113   if((m_cbc && !error) || (!m_cbc && error))
114   {
115     cipherText = cipherText;
116     temp = blowfishCBC(cipherText, false);
117
118     if(temp == cipherText)
119     {
120       // kDebug("Decryption from CBC Failed");
121       return cipherText+' '+'\n';
122     }
123     else
124       cipherText = temp;
125   }
126   else
127   {
128     temp = blowfishECB(cipherText, false);
129
130     if(temp == cipherText)
131     {
132       // kDebug("Decryption from ECB Failed");
133       return cipherText+' '+'\n';
134     }
135     else
136       cipherText = temp;
137   }
138   // TODO FIXME the proper fix for this is to show encryption differently e.g. [nick] instead of <nick>
139   // don't hate me for the mircryption reference there.
140   if (cipherText.at(0) == 1)
141     pfx = "\x0";
142   cipherText = pfx+cipherText+' '+'\n'; // FIXME(??) why is there an added space here?
143   return cipherText;
144 }
145
146 QByteArray Cipher::initKeyExchange()
147 {
148   QCA::Initializer init;
149   m_tempKey = QCA::KeyGenerator().createDH(QCA::DLGroup(m_primeNum, QCA::BigInteger(2))).toDH();
150
151   if(m_tempKey.isNull())
152     return QByteArray();
153
154   QByteArray publicKey = m_tempKey.toPublicKey().toDH().y().toArray().toByteArray();
155
156   //remove leading 0
157   if(publicKey.length() > 135 && publicKey.at(0) == '\0')
158     publicKey = publicKey.mid(1);
159
160   return publicKey.toBase64().append('A');
161 }
162
163 QByteArray Cipher::parseInitKeyX(QByteArray key)
164 {
165   QCA::Initializer init;
166
167   if(key.length() != 181)
168     return QByteArray();
169
170   QCA::SecureArray remoteKey = QByteArray::fromBase64(key.left(180));
171   QCA::DLGroup group(m_primeNum, QCA::BigInteger(2));
172   QCA::DHPrivateKey privateKey = QCA::KeyGenerator().createDH(group).toDH();
173
174   if(privateKey.isNull())
175     return QByteArray();
176
177   QByteArray publicKey = privateKey.y().toArray().toByteArray();
178
179   //remove leading 0
180   if(publicKey.length() > 135 && publicKey.at(0) == '\0')
181     publicKey = publicKey.mid(1);
182
183   QCA::DHPublicKey remotePub(group, remoteKey);
184
185   if(remotePub.isNull())
186     return QByteArray();
187
188   QByteArray sharedKey = privateKey.deriveKey(remotePub).toByteArray();
189   sharedKey = QCA::Hash("sha256").hash(sharedKey).toByteArray().toBase64();
190
191   //remove trailing = because mircryption and fish think it's a swell idea.
192   while(sharedKey.endsWith('=')) sharedKey.chop(1);
193
194   bool success = setKey(sharedKey);
195
196   if(!success)
197     return QByteArray();
198
199   return publicKey.toBase64().append('A');
200 }
201
202 bool Cipher::parseFinishKeyX(QByteArray key)
203 {
204   QCA::Initializer init;
205
206   if(key.length() != 181)
207     return false;
208
209   QCA::SecureArray remoteKey = QByteArray::fromBase64(key.left(180));
210   QCA::DLGroup group(m_primeNum, QCA::BigInteger(2));
211
212   QCA::DHPublicKey remotePub(group, remoteKey);
213
214   if(remotePub.isNull())
215     return false;
216
217   if(m_tempKey.isNull())
218     return false;
219
220   QByteArray sharedKey = m_tempKey.deriveKey(remotePub).toByteArray();
221   sharedKey = QCA::Hash("sha256").hash(sharedKey).toByteArray().toBase64();
222
223   //remove trailng = because mircryption and fish think it's a swell idea.
224   while(sharedKey.endsWith('=')) sharedKey.chop(1);
225
226   bool success = setKey(sharedKey);
227
228   return success;
229 }
230
231 QByteArray Cipher::decryptTopic(QByteArray cipherText)
232 {
233   if(cipherText.mid(0,4) == "+OK ")// FiSH style topic
234     cipherText = cipherText.mid(4);
235   else if(cipherText.left(5) == "«m«")
236     cipherText = cipherText.mid(5,cipherText.length()-10);
237   else
238     return cipherText;
239
240   QByteArray temp;
241   //TODO currently no backwards sanity checks for topic, it seems to use different standards
242   //if somebody figures them out they can enable it and add "ERROR_NONECB/CBC" warnings
243   if(m_cbc)
244     temp = blowfishCBC(cipherText.mid(1), false);
245   else
246     temp = blowfishECB(cipherText, false);
247
248   if(temp == cipherText)
249   {
250     return cipherText;
251   }
252   else
253     cipherText = temp;
254
255   if(cipherText.mid(0,2) == "@@")
256     cipherText = cipherText.mid(2);
257
258   return cipherText;
259 }
260
261 bool Cipher::encrypt(QByteArray& cipherText)
262 {
263   if (cipherText.left(3) == "+p ") //don't encode if...?
264     cipherText = cipherText.mid(3);
265   else
266   {
267     if(m_cbc) //encode in ecb or cbc decide how to determine later
268     {
269       QByteArray temp = blowfishCBC(cipherText, true);
270
271       if(temp == cipherText)
272       {
273         // kDebug("CBC Encoding Failed");
274         return false;
275       }
276
277       cipherText = "+OK *" + temp;
278     }
279     else
280     {
281       QByteArray temp = blowfishECB(cipherText, true);
282
283       if(temp == cipherText)
284       {
285         // kDebug("ECB Encoding Failed");
286         return false;
287       }
288
289       cipherText = "+OK " + temp;
290      }
291   }
292   return true;
293 }
294
295 //THE BELOW WORKS AKA DO NOT TOUCH UNLESS YOU KNOW WHAT YOU'RE DOING
296 QByteArray Cipher::blowfishCBC(QByteArray cipherText, bool direction)
297 {
298   QCA::Initializer init;
299   QByteArray temp = cipherText;
300   if(direction)
301   {
302     // make sure cipherText is an interval of 8 bits. We do this before so that we
303     // know there's at least 8 bytes to en/decryption this ensures QCA doesn't fail
304     while((temp.length() % 8) != 0) temp.append('\0');
305
306     QCA::InitializationVector iv(8);
307     temp.prepend(iv.toByteArray()); // prefix with 8bits of IV for mircryptions *CUSTOM* cbc implementation
308   }
309   else
310   {
311     temp = QByteArray::fromBase64(temp);
312     //supposedly nescessary if we get a truncated message also allows for decryption of 'crazy'
313     //en/decoding clients that use STANDARDIZED PADDING TECHNIQUES
314     while((temp.length() % 8) != 0) temp.append('\0');
315   }
316
317   QCA::Direction dir = (direction) ? QCA::Encode : QCA::Decode;
318   QCA::Cipher cipher(m_type, QCA::Cipher::CBC, QCA::Cipher::NoPadding, dir, m_key, QCA::InitializationVector(QByteArray("0")));
319   QByteArray temp2 = cipher.update(QCA::MemoryRegion(temp)).toByteArray();
320   temp2 += cipher.final().toByteArray();
321
322   if(!cipher.ok())
323     return cipherText;
324
325   if(direction) //send in base64
326     temp2 = temp2.toBase64();
327   else //cut off the 8bits of IV
328     temp2 = temp2.remove(0,8);
329
330   return temp2;
331 }
332
333 QByteArray Cipher::blowfishECB(QByteArray cipherText, bool direction)
334 {
335   QCA::Initializer init;
336   QByteArray temp = cipherText;
337
338   //do padding ourselves
339   if(direction)
340   {
341     while((temp.length() % 8) != 0) temp.append('\0');
342   }
343   else
344   {
345     temp = b64ToByte(temp);
346     while((temp.length() % 8) != 0) temp.append('\0');
347   }
348
349   QCA::Direction dir = (direction) ? QCA::Encode : QCA::Decode;
350   QCA::Cipher cipher(m_type, QCA::Cipher::ECB, QCA::Cipher::NoPadding, dir, m_key);
351   QByteArray temp2 = cipher.update(QCA::MemoryRegion(temp)).toByteArray();
352   temp2 += cipher.final().toByteArray();
353
354   if(!cipher.ok())
355     return cipherText;
356
357   if(direction)
358     temp2 = byteToB64(temp2);
359
360   return temp2;
361 }
362
363 //Custom non RFC 2045 compliant Base64 enc/dec code for mircryption / FiSH compatibility
364 QByteArray Cipher::byteToB64(QByteArray text)
365 {
366   int left = 0;
367   int right = 0;
368   int k = -1;
369   int v;
370   QString base64 = "./0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
371   QByteArray encoded;
372   while (k < (text.length() - 1)) {
373     k++;
374     v=text.at(k); if (v<0) v+=256;
375     left = v << 24;
376     k++;
377     v=text.at(k); if (v<0) v+=256;
378     left += v << 16;
379     k++;
380     v=text.at(k); if (v<0) v+=256;
381     left += v << 8;
382     k++;
383     v=text.at(k); if (v<0) v+=256;
384     left += v;
385
386     k++;
387     v=text.at(k); if (v<0) v+=256;
388     right = v << 24;
389     k++;
390     v=text.at(k); if (v<0) v+=256;
391     right += v << 16;
392     k++;
393     v=text.at(k); if (v<0) v+=256;
394     right += v << 8;
395     k++;
396     v=text.at(k); if (v<0) v+=256;
397     right += v;
398
399     for (int i = 0; i < 6; i++) {
400       encoded.append(base64.at(right & 0x3F).toAscii());
401       right = right >> 6;
402     }
403
404     //TODO make sure the .toascii doesn't break anything
405     for (int i = 0; i < 6; i++) {
406       encoded.append(base64.at(left & 0x3F).toAscii());
407       left = left >> 6;
408     }
409   }
410   return encoded;
411 }
412
413 QByteArray Cipher::b64ToByte(QByteArray text)
414 {
415   QString base64 = "./0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
416   QByteArray decoded;
417   int k = -1;
418   while (k < (text.length() - 1)) {
419     int right = 0;
420     int left = 0;
421     int v = 0;
422     int w = 0;
423     int z = 0;
424
425     for (int i = 0; i < 6; i++) {
426       k++;
427       v = base64.indexOf(text.at(k));
428       right |= v << (i * 6);
429     }
430
431     for (int i = 0; i < 6; i++) {
432       k++;
433       v = base64.indexOf(text.at(k));
434       left |= v << (i * 6);
435     }
436
437     for (int i = 0; i < 4; i++) {
438       w = ((left & (0xFF << ((3 - i) * 8))));
439       z = w >> ((3 - i) * 8);
440       if(z < 0) {z = z + 256;}
441       decoded.append(z);
442     }
443
444     for (int i = 0; i < 4; i++) {
445       w = ((right & (0xFF << ((3 - i) * 8))));
446       z = w >> ((3 - i) * 8);
447       if(z < 0) {z = z + 256;}
448       decoded.append(z);
449     }
450   }
451   return decoded;
452 }
453
454 bool Cipher::neededFeaturesAvailable()
455 {
456   QCA::Initializer init;
457
458   if (QCA::isSupported("blowfish-ecb") && QCA::isSupported("blowfish-cbc") && QCA::isSupported("dh"))
459     return true;
460
461   qWarning() << "QCA provider plugin not found. It is usually provided by the qca-ossl plugin.";
462   return false;
463 }