CryptTool   D
last analyzed

Complexity

Total Complexity 58

Size/Duplication

Total Lines 539
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 15

Importance

Changes 0
Metric Value
wmc 58
lcom 2
cbo 15
dl 0
loc 539
rs 4.5599
c 0
b 0
f 0

35 Methods

Rating   Name   Duplication   Size   Complexity  
makeBox() 0 1 ?
makeSecretBox() 0 1 ?
openBox() 0 1 ?
openSecretBox() 0 1 ?
generateKeyPair() 0 1 ?
createRandom() 0 1 ?
derivePublicKey() 0 1 ?
isSupported() 0 1 ?
validate() 0 1 ?
getName() 0 1 ?
getDescription() 0 1 ?
A getInstance() 0 19 4
A createInstance() 0 17 6
A __construct() 0 1 1
A __clone() 0 1 1
A encryptMessageText() 0 12 1
A encryptImageMessage() 0 18 1
A encryptFileMessage() 0 32 3
C decryptMessage() 0 77 15
A hashEmail() 0 4 1
A hashPhoneNo() 0 4 1
A randomNonce() 0 3 1
A symmetricKey() 0 3 1
A encryptFile() 0 5 1
A decryptFile() 0 4 2
A encryptFileThumbnail() 0 4 1
A decryptFileThumbnail() 0 4 2
A encryptImage() 0 12 1
A decryptImage() 0 6 1
A generatePadBytes() 0 7 3
A __toString() 0 3 1
A bin2hex() 0 4 1
A hex2bin() 0 7 2
B stringCompare() 0 28 6
A removeVar() 0 8 1

How to fix   Complexity   

Complex Class

Complex classes like CryptTool often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CryptTool, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @author Threema GmbH
4
 * @copyright Copyright (c) 2015-2016 Threema GmbH
5
 */
6
7
namespace Threema\MsgApi\Tools;
8
9
use Threema\Core\Exception;
10
use Threema\Core\KeyPair;
11
use Threema\Core\AssocArray;
12
use Threema\MsgApi\Commands\Results\UploadFileResult;
13
use Threema\MsgApi\Exceptions\BadMessageException;
14
use Threema\MsgApi\Exceptions\DecryptionFailedException;
15
use Threema\MsgApi\Exceptions\UnsupportedMessageTypeException;
16
use Threema\MsgApi\Messages\DeliveryReceipt;
17
use Threema\MsgApi\Messages\FileMessage;
18
use Threema\MsgApi\Messages\ImageMessage;
19
use Threema\MsgApi\Messages\TextMessage;
20
use Threema\MsgApi\Messages\ThreemaMessage;
21
22
/**
23
 * Interface CryptTool
24
 * Contains static methods to do various Threema cryptography related tasks.
25
 *
26
 * @package Threema\MsgApi\Tool
27
 */
28
abstract class CryptTool {
29
	const TYPE_SODIUM = 'sodium';
30
	const TYPE_SALT = 'salt';
31
32
	const FILE_NONCE = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01";
33
	const FILE_THUMBNAIL_NONCE = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02";
34
	/**
35
	 * @var CryptTool
36
	 */
37
	private static $instance = null;
38
39
	/**
40
	 * Prior libsodium
41
	 *
42
	 * @return CryptTool
43
	 */
44
	public static function getInstance() {
45
		if(null === self::$instance) {
46
			foreach(array(
47
				function() {
48
					return self::createInstance(self::TYPE_SODIUM);
49
				},
50
				function() {
51
					return self::createInstance(self::TYPE_SALT);
52
				}) as $instanceGenerator) {
53
				$i = $instanceGenerator->__invoke();
54
				if(null !== $i) {
55
					self::$instance = $i;
56
					break;
57
				}
58
			}
59
		}
60
61
		return self::$instance;
62
	}
63
64
	/**
65
	 * @param string $type
66
	 * @return null|CryptTool null on unknown type
67
	 */
68
	public static function createInstance($type) {
69
		switch($type) {
70
			case self::TYPE_SODIUM:
71
				$instance = new CryptToolSodium();
72
				if(false === $instance->isSupported()) {
73
					//try to instance old version of sodium wrapper
74
					/** @noinspection PhpDeprecationInspection */
75
					$instance = new CryptToolSodiumDep();
0 ignored issues
show
Deprecated Code introduced by
The class Threema\MsgApi\Tools\CryptToolSodiumDep has been deprecated with message: please update your libsodium package to >= 0.2.0

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
76
				}
77
				return $instance->isSupported() ? $instance :null;
78
			case self::TYPE_SALT:
79
				$instance = new CryptToolSalt();
80
				return $instance->isSupported() ? $instance :null;
81
			default:
82
				return null;
83
		}
84
	}
85
86
	const MESSAGE_ID_LEN = 8;
87
	const BLOB_ID_LEN = 16;
88
	const IMAGE_FILE_SIZE_LEN = 4;
89
	const IMAGE_NONCE_LEN = 24;
90
91
	const EMAIL_HMAC_KEY = "\x30\xa5\x50\x0f\xed\x97\x01\xfa\x6d\xef\xdb\x61\x08\x41\x90\x0f\xeb\xb8\xe4\x30\x88\x1f\x7a\xd8\x16\x82\x62\x64\xec\x09\xba\xd7";
92
	const PHONENO_HMAC_KEY = "\x85\xad\xf8\x22\x69\x53\xf3\xd9\x6c\xfd\x5d\x09\xbf\x29\x55\x5e\xb9\x55\xfc\xd8\xaa\x5e\xc4\xf9\xfc\xd8\x69\xe2\x58\x37\x07\x23";
93
94
	protected  function __construct() {}
95
	protected  function __clone() {}
96
97
	/**
98
	 * Encrypt a text message.
99
	 *
100
	 * @param string $text the text to be encrypted (max. 3500 bytes)
101
	 * @param string $senderPrivateKey the private key of the sending ID
102
	 * @param string $recipientPublicKey the public key of the receiving ID
103
	 * @param string $nonce the nonce to be used for the encryption (usually 24 random bytes)
104
	 * @return string encrypted box
105
	 */
106
	final public function encryptMessageText($text, $senderPrivateKey, $recipientPublicKey, $nonce) {
107
		/* prepend type byte (0x01) to message data */
108
		$textBytes = "\x01" . $text;
109
110
		/* determine random amount of PKCS7 padding */
111
		$padbytes = $this->generatePadBytes();
112
113
		/* append padding */
114
		$textBytes .= str_repeat(chr($padbytes), $padbytes);
115
116
		return $this->makeBox($textBytes, $nonce, $senderPrivateKey, $recipientPublicKey);
117
	}
118
119
	/**
120
	 * @param UploadFileResult $uploadFileResult the result of the upload
121
	 * @param EncryptResult $encryptResult the result of the image encryption
122
	 * @param string $senderPrivateKey the private key of the sending ID (as binary)
123
	 * @param string $recipientPublicKey the public key of the receiving ID (as binary)
124
	 * @param string $nonce the nonce to be used for the encryption (usually 24 random bytes)
125
	 * @return string
126
	 */
127
	final public function encryptImageMessage(
128
			UploadFileResult $uploadFileResult,
129
			EncryptResult $encryptResult,
130
			$senderPrivateKey,
131
			$recipientPublicKey,
132
			$nonce) {
133
		$message = "\x02" . $this->hex2bin($uploadFileResult->getBlobId());
134
		$message .= pack('V', $encryptResult->getSize());
135
		$message .= $encryptResult->getNonce();
136
137
		/* determine random amount of PKCS7 padding */
138
		$padbytes = $this->generatePadBytes();
139
140
		/* append padding */
141
		$message .= str_repeat(chr($padbytes), $padbytes);
142
143
		return $this->makeBox($message, $nonce, $senderPrivateKey, $recipientPublicKey);
144
	}
145
146
	final public function encryptFileMessage(UploadFileResult $uploadFileResult,
147
											 EncryptResult $encryptResult,
148
											 UploadFileResult $thumbnailUploadFileResult = null,
149
											 FileAnalysisResult $fileAnalysisResult,
150
											 $senderPrivateKey,
151
											 $recipientPublicKey,
152
											 $nonce) {
153
154
155
		$messageContent = array(
156
			'b' => $uploadFileResult->getBlobId(),
157
			'k' => $this->bin2hex($encryptResult->getKey()),
158
			'm' => $fileAnalysisResult->getMimeType(),
159
			'n' => $fileAnalysisResult->getFileName(),
160
			's' => $fileAnalysisResult->getSize(),
161
			'i' => 0
162
		);
163
164
		if($thumbnailUploadFileResult !== null && strlen($thumbnailUploadFileResult->getBlobId()) > 0) {
165
			$messageContent['t'] = $thumbnailUploadFileResult->getBlobId();
166
		}
167
168
		$message = "\x17" . json_encode($messageContent);
169
170
		/* determine random amount of PKCS7 padding */
171
		$padbytes = $this->generatePadBytes();
172
173
		/* append padding */
174
		$message .= str_repeat(chr($padbytes), $padbytes);
175
176
		return $this->makeBox($message, $nonce, $senderPrivateKey, $recipientPublicKey);
177
	}
178
179
	/**
180
	 * make a box
181
	 *
182
	 * @param string $data
183
	 * @param string $nonce
184
	 * @param string $senderPrivateKey
185
	 * @param string $recipientPublicKey
186
	 * @return string encrypted box
187
	 */
188
	abstract protected function makeBox($data, $nonce, $senderPrivateKey, $recipientPublicKey);
189
190
	/**
191
	 * make a secret box
192
	 *
193
	 * @param $data
194
	 * @param $nonce
195
	 * @param $key
196
	 * @return mixed
197
	 */
198
	abstract protected function makeSecretBox($data, $nonce, $key);
199
200
	/**
201
	 * decrypt a box
202
	 *
203
	 * @param string $box as binary
204
	 * @param string $recipientPrivateKey as binary
205
	 * @param string $senderPublicKey as binary
206
	 * @param string $nonce as binary
207
	 * @return string
208
	 */
209
	abstract protected function openBox($box, $recipientPrivateKey, $senderPublicKey, $nonce);
210
211
	/**
212
	 * decrypt a secret box
213
	 *
214
	 * @param string $box as binary
215
	 * @param string $nonce as binary
216
	 * @param string $key as binary
217
	 * @return string as binary
218
	 */
219
	abstract protected function openSecretBox($box, $nonce, $key);
220
221
	/**
222
	 * @param string $box
223
	 * @param string $recipientPrivateKey
224
	 * @param string $senderPublicKey
225
	 * @param string $nonce
226
	 * @return ThreemaMessage the decrypted message
227
	 * @throws BadMessageException
228
	 * @throws DecryptionFailedException
229
	 * @throws UnsupportedMessageTypeException
230
	 */
231
	final public function decryptMessage($box, $recipientPrivateKey, $senderPublicKey, $nonce) {
232
233
		$data = $this->openBox($box, $recipientPrivateKey, $senderPublicKey, $nonce);
234
235
		if (null === $data || strlen($data) == 0) {
236
			throw new DecryptionFailedException();
237
		}
238
239
		/* remove padding */
240
		$padbytes = ord($data[strlen($data)-1]);
241
		$realDataLength = strlen($data) - $padbytes;
242
		if ($realDataLength < 1) {
243
			throw new BadMessageException();
244
		}
245
		$data = substr($data, 0, $realDataLength);
246
247
		/* first byte of data is type */
248
		$type = ord($data[0]);
249
250
		$pos = 1;
251
		$piece = function($length) use(&$pos, $data) {
252
			$d = substr($data, $pos, $length);
253
			$pos += $length;
254
			return $d;
255
		};
256
257
		switch ($type) {
258
			case TextMessage::TYPE_CODE:
259
				/* Text message */
260
				if ($realDataLength < 2) {
261
					throw new BadMessageException();
262
				}
263
264
				return new TextMessage(substr($data, 1));
265
			case DeliveryReceipt::TYPE_CODE:
266
				/* Delivery receipt */
267
				if ($realDataLength < (self::MESSAGE_ID_LEN-2) || (($realDataLength - 2) % self::MESSAGE_ID_LEN) != 0)  {
268
					throw new BadMessageException();
269
				}
270
271
				$receiptType = ord($data[1]);
272
				$messageIds = str_split(substr($data, 2), self::MESSAGE_ID_LEN);
273
274
				return new DeliveryReceipt($receiptType, $messageIds);
275
			case ImageMessage::TYPE_CODE:
276
				/* Image Message */
277
				if ($realDataLength != 1 + self::BLOB_ID_LEN + self::IMAGE_FILE_SIZE_LEN + self::IMAGE_NONCE_LEN)  {
278
					throw new BadMessageException();
279
				}
280
281
				$blobId = $piece->__invoke(self::BLOB_ID_LEN);
282
				$length = $piece->__invoke(self::IMAGE_FILE_SIZE_LEN);
283
				$nonce = $piece->__invoke(self::IMAGE_NONCE_LEN);
284
				return new ImageMessage($this->bin2hex($blobId), $this->bin2hex($length), $nonce);
285
			case FileMessage::TYPE_CODE:
286
				/* Image Message */
287
				$decodeResult = json_decode(substr($data, 1), true);
288
				if(null === $decodeResult || false === $decodeResult) {
289
					throw new BadMessageException();
290
				}
291
292
				$values = AssocArray::byJsonString(substr($data, 1), array('b', 't', 'k', 'm', 'n', 's'));
293
				if(null === $values) {
294
					throw new BadMessageException();
295
				}
296
297
				return new FileMessage(
298
					$values->getValue('b'),
299
					$values->getValue('t'),
300
					$values->getValue('k'),
301
					$values->getValue('m'),
302
					$values->getValue('n'),
303
					$values->getValue('s'));
304
			default:
305
				throw new UnsupportedMessageTypeException();
306
		}
307
	}
308
309
	/**
310
	 * Generate a new key pair.
311
	 *
312
	 * @return KeyPair the new key pair
313
	 */
314
	abstract public function generateKeyPair();
315
316
	/**
317
	 * Hashes an email address for identity lookup.
318
	 *
319
	 * @param string $email the email address
320
	 * @return string the email hash (hex)
321
	 */
322
	final public function hashEmail($email) {
323
		$emailClean = strtolower(trim($email));
324
		return hash_hmac('sha256', $emailClean, self::EMAIL_HMAC_KEY);
325
	}
326
327
	/**
328
	 * Hashes an phone number address for identity lookup.
329
	 *
330
	 * @param string $phoneNo the phone number (in E.164 format, no leading +)
331
	 * @return string the phone number hash (hex)
332
	 */
333
	final public function hashPhoneNo($phoneNo) {
334
		$phoneNoClean = preg_replace("/[^0-9]/", "", $phoneNo);
335
		return hash_hmac('sha256', $phoneNoClean, self::PHONENO_HMAC_KEY);
336
	}
337
338
	abstract protected function createRandom($size);
339
340
	/**
341
	 * Generate a random nonce.
342
	 *
343
	 * @return string random nonce
344
	 */
345
	final public function randomNonce() {
346
		return $this->createRandom(\Salt::box_NONCE);
347
	}
348
349
	/**
350
	 * Generate a symmetric key
351
	 * @return mixed
352
	 */
353
	final public function symmetricKey() {
354
		return $this->createRandom(32);
355
	}
356
357
	/**
358
	 * Derive the public key
359
	 *
360
	 * @param string $privateKey as binary
361
	 * @return string as binary
362
	 */
363
	abstract public function derivePublicKey($privateKey);
364
365
	/**
366
	 * Check if implementation supported
367
	 * @return bool
368
	 */
369
	abstract public function isSupported();
370
371
	/**
372
	 * Validate crypt tool
373
	 *
374
	 * @return bool
375
	 * @throws Exception
376
	 */
377
	abstract public function validate();
378
379
	/**
380
	 * @param $data
381
	 * @return EncryptResult
382
	 */
383
	public final function encryptFile($data) {
384
		$key = $this->symmetricKey();
385
		$box = $this->makeSecretBox($data, self::FILE_NONCE, $key);
386
		return new EncryptResult($box, $key, self::FILE_NONCE, strlen($box));
387
	}
388
389
	/**
390
	 * @param string $data as binary
391
	 * @param string $key as binary
392
	 * @return null|string
393
	 */
394
	public final function decryptFile($data, $key) {
395
		$result =  $this->openSecretBox($data, self::FILE_NONCE, $key);
396
		return false === $result ? null : $result;
397
	}
398
399
	/**
400
	 * @param string $data
401
	 * @param string $key
402
	 * @return EncryptResult
403
	 */
404
	public final function encryptFileThumbnail($data, $key) {
405
		$box = $this->makeSecretBox($data, self::FILE_THUMBNAIL_NONCE, $key);
406
		return new EncryptResult($box, $key,  self::FILE_THUMBNAIL_NONCE, strlen($box));
407
	}
408
409
	public final function decryptFileThumbnail($data, $key) {
410
		$result = $this->openSecretBox($data, self::FILE_THUMBNAIL_NONCE, $key);
411
		return false === $result ? null : $result;
412
	}
413
414
	/**
415
	 * @param string $imageData
416
	 * @param string $privateKey as binary
417
	 * @param string $publicKey as binary
418
	 * @return EncryptResult
419
	 */
420
	public final function encryptImage($imageData, $privateKey, $publicKey) {
421
		$nonce = $this->randomNonce();
422
423
		$box = $this->makeBox(
424
			$imageData,
425
			$nonce,
426
			$privateKey,
427
			$publicKey
428
		);
429
430
		return new EncryptResult($box, null, $nonce, strlen($box));
431
	}
432
433
	/**
434
	 * @param string $data as binary
435
	 * @param string $publicKey as binary
436
	 * @param string $privateKey as binary
437
	 * @param string $nonce as binary
438
	 * @return string
439
	 */
440
	public final function decryptImage($data, $publicKey, $privateKey, $nonce) {
441
		return $this->openBox($data,
442
			$privateKey,
443
			$publicKey,
444
			$nonce);
445
	}
446
447
	/**
448
	 * determine random amount of PKCS7 padding
449
	 * @return int
450
	 */
451
	private function generatePadBytes() {
452
		$padbytes = 0;
453
		while($padbytes < 1 || $padbytes > 255) {
454
			$padbytes = ord($this->createRandom(1));
455
		}
456
		return $padbytes;
457
	}
458
459
	public function __toString() {
460
		return 'CryptTool '.$this->getName();
461
	}
462
463
	/**
464
	 * Converts a binary string to an hexdecimal string.
465
	 *
466
	 * This is the same as PHP's bin2hex() implementation, but it is resistant to
467
	 * timing attacks.
468
	 *
469
	 * @param  string $binaryString The binary string to convert
470
	 * @return string
471
	 */
472
	public function bin2hex($binaryString)
473
	{
474
		return bin2hex($binaryString);
475
	}
476
477
	/**
478
	 * Converts an hexdecimal string to a binary string.
479
	 *
480
	 * This is the same as PHP's hex2bin() implementation, but it is resistant to
481
	 * timing attacks.
482
	 * Note that the default implementation does not support $ignore currrently and will
483
	 * throw an error. Only when libsodium >= 0.22 is used, this is supported.
484
	 *
485
	 * @param  string $hexString The hex string to convert
486
	 * @param  string|null $ignore	(optional) Characters to ignore
487
	 * @throws \Threema\Core\Exception
488
	 * @return string
489
	 */
490
	public function hex2bin($hexString, $ignore = null)
491
	{
492
		if ($ignore !== null) {
493
			throw new Exception('$ignore parameter is not supported');
494
		}
495
		return hex2bin($hexString);
496
	}
497
498
	/**
499
	 * Compares two strings in a secure way.
500
	 *
501
	 * This is the same as PHP's strcmp() implementation, but it is resistant to
502
	 * timing attacks.
503
	 *
504
	 * @link https://paragonie.com/book/pecl-libsodium/read/03-utilities-helpers.md#compare
505
	 * @param  string $str1 The first string
506
	 * @param  string $str2 The second string
507
	 * @return bool
508
	 */
509
	public function stringCompare($str1, $str2)
510
	{
511
		if (function_exists('hash_equals')) {
512
			return hash_equals($str1, $str2);
513
		} else {
514
			// check variable type manually
515
			if (!is_string($str1) || !is_string($str2)) {
516
				return false;
517
			}
518
519
			// fast comparison: check string length
520
			if (strlen($str1) != strlen($str2)) {
521
				return false;
522
			}
523
524
			# PHP implementation of hash_equals
525
			# partly taken from https://github.com/symfony/polyfill-php56/blob/master/Php56.php#L45-L51
526
			#
527
			# Note that this is really slow!!
528
			#
529
			$ret = 0;
530
			$length = strlen($str1);
531
			for ($i = 0; $i < $length; ++$i) {
532
	            $ret |= ord($str1[$i]) ^ ord($str2[$i]);
533
	        }
534
			return 0 === $ret;
535
		}
536
	}
537
538
	/**
539
	 * Unsets/removes a variable.
540
	 *
541
	 * Note: the PHP implementation here provides no security, but if you use
542
	 * Libsodium, the variable will be deleted in a better way.
543
	 *
544
	 * @param  string $var A variable, passed by reference
545
	 */
546
	public function removeVar(&$var)
547
	{
548
		// overwrite var (128x0), quite certainly not secure at all
549
		$var = '00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000';
550
		$var = null;
551
		// actually this does not erase the content of the variable
552
		unset($var);
553
	}
554
555
	/**
556
	 * Name of the CryptTool
557
	 * @return string
558
	 */
559
	abstract public function getName();
560
561
	/**
562
	 * Description of the CryptTool
563
	 * @return string
564
	 */
565
	abstract public function getDescription();
566
}
567