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