Completed
Pull Request — master (#42)
by rugk
07:19
created

CryptTool::derivePublicKey()

Size

Total Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 1
c 0
b 0
f 0
nc 1
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
96
	/**
97
	 * Encrypt a text message.
98
	 *
99
	 * @param string $text the text to be encrypted (max. 3500 bytes)
100
	 * @param string $senderPrivateKey the private key of the sending ID
101
	 * @param string $recipientPublicKey the public key of the receiving ID
102
	 * @param string $nonce the nonce to be used for the encryption (usually 24 random bytes)
103
	 * @return string encrypted box
104
	 */
105
	final public function encryptMessageText($text, $senderPrivateKey, $recipientPublicKey, $nonce) {
106
		/* prepend type byte (0x01) to message data */
107
		$textBytes = "\x01" . $text;
108
109
		/* determine random amount of PKCS7 padding */
110
		$padbytes = $this->generatePadBytes();
111
112
		/* append padding */
113
		$textBytes .= str_repeat(chr($padbytes), $padbytes);
114
115
		return $this->makeBox($textBytes, $nonce, $senderPrivateKey, $recipientPublicKey);
116
	}
117
118
	/**
119
	 * @param UploadFileResult $uploadFileResult the result of the upload
120
	 * @param EncryptResult $encryptResult the result of the image encryption
121
	 * @param string $senderPrivateKey the private key of the sending ID (as binary)
122
	 * @param string $recipientPublicKey the public key of the receiving ID (as binary)
123
	 * @param string $nonce the nonce to be used for the encryption (usually 24 random bytes)
124
	 * @return string
125
	 */
126
	final public function encryptImageMessage(
127
			UploadFileResult $uploadFileResult,
128
			EncryptResult $encryptResult,
129
			$senderPrivateKey,
130
			$recipientPublicKey,
131
			$nonce) {
132
		$message = "\x02" . hex2bin($uploadFileResult->getBlobId());
133
		$message .= pack('V', $encryptResult->getSize());
134
		$message .= $encryptResult->getNonce();
135
136
		/* determine random amount of PKCS7 padding */
137
		$padbytes = $this->generatePadBytes();
138
139
		/* append padding */
140
		$message .= str_repeat(chr($padbytes), $padbytes);
141
142
		return $this->makeBox($message, $nonce, $senderPrivateKey, $recipientPublicKey);
143
	}
144
145
	final public function encryptFileMessage(UploadFileResult $uploadFileResult,
146
											 EncryptResult $encryptResult,
147
											 UploadFileResult $thumbnailUploadFileResult = null,
148
											 FileAnalysisResult $fileAnalysisResult,
149
											 $senderPrivateKey,
150
											 $recipientPublicKey,
151
											 $nonce) {
152
153
154
		$messageContent = array(
155
			'b' => $uploadFileResult->getBlobId(),
156
			'k' => bin2hex($encryptResult->getKey()),
157
			'm' => $fileAnalysisResult->getMimeType(),
158
			'n' => $fileAnalysisResult->getFileName(),
159
			's' => $fileAnalysisResult->getSize(),
160
			'i' => 0
161
		);
162
163
		if($thumbnailUploadFileResult != null && strlen($thumbnailUploadFileResult->getBlobId()) > 0) {
164
			$messageContent['t'] = $thumbnailUploadFileResult->getBlobId();
165
		}
166
167
		$message = "\x17" . json_encode($messageContent);
168
169
		/* determine random amount of PKCS7 padding */
170
		$padbytes = $this->generatePadBytes();
171
172
		/* append padding */
173
		$message .= str_repeat(chr($padbytes), $padbytes);
174
175
		return $this->makeBox($message, $nonce, $senderPrivateKey, $recipientPublicKey);
176
	}
177
178
	/**
179
	 * make a box
180
	 *
181
	 * @param string $data
182
	 * @param string $nonce
183
	 * @param string $senderPrivateKey
184
	 * @param string $recipientPublicKey
185
	 * @return string encrypted box
186
	 */
187
	abstract protected function makeBox($data, $nonce, $senderPrivateKey, $recipientPublicKey);
188
189
	/**
190
	 * make a secret box
191
	 *
192
	 * @param $data
193
	 * @param $nonce
194
	 * @param $key
195
	 * @return mixed
196
	 */
197
	abstract protected function makeSecretBox($data, $nonce, $key);
198
199
	/**
200
	 * decrypt a box
201
	 *
202
	 * @param string $box as binary
203
	 * @param string $recipientPrivateKey as binary
204
	 * @param string $senderPublicKey as binary
205
	 * @param string $nonce as binary
206
	 * @return string
207
	 */
208
	abstract protected function openBox($box, $recipientPrivateKey, $senderPublicKey, $nonce);
209
210
	/**
211
	 * decrypt a secret box
212
	 *
213
	 * @param string $box as binary
214
	 * @param string $nonce as binary
215
	 * @param string $key as binary
216
	 * @return string as binary
217
	 */
218
	abstract protected function openSecretBox($box, $nonce, $key);
219
220
	/**
221
	 * @param string $box
222
	 * @param string $recipientPrivateKey
223
	 * @param string $senderPublicKey
224
	 * @param string $nonce
225
	 * @return ThreemaMessage the decrypted message
226
	 * @throws BadMessageException
227
	 * @throws DecryptionFailedException
228
	 * @throws UnsupportedMessageTypeException
229
	 */
230
	final public function decryptMessage($box, $recipientPrivateKey, $senderPublicKey, $nonce) {
231
232
		$data = $this->openBox($box, $recipientPrivateKey, $senderPublicKey, $nonce);
233
234
		if (null === $data || strlen($data) == 0) {
235
			throw new DecryptionFailedException();
236
		}
237
238
		/* remove padding */
239
		$padbytes = ord($data[strlen($data)-1]);
240
		$realDataLength = strlen($data) - $padbytes;
241
		if ($realDataLength < 1) {
242
			throw new BadMessageException();
243
		}
244
		$data = substr($data, 0, $realDataLength);
245
246
		/* first byte of data is type */
247
		$type = ord($data[0]);
248
249
		$pos = 1;
250
		$piece = function($length) use(&$pos, $data) {
251
			$d = substr($data, $pos, $length);
252
			$pos += $length;
253
			return $d;
254
		};
255
256
		switch ($type) {
257
			case TextMessage::TYPE_CODE:
258
				/* Text message */
259
				if ($realDataLength < 2) {
260
					throw new BadMessageException();
261
				}
262
263
				return new TextMessage(substr($data, 1));
264
			case DeliveryReceipt::TYPE_CODE:
265
				/* Delivery receipt */
266
				if ($realDataLength < (self::MESSAGE_ID_LEN-2) || (($realDataLength - 2) % self::MESSAGE_ID_LEN) != 0)  {
267
					throw new BadMessageException();
268
				}
269
270
				$receiptType = ord($data[1]);
271
				$messageIds = str_split(substr($data, 2), self::MESSAGE_ID_LEN);
272
273
				return new DeliveryReceipt($receiptType, $messageIds);
274
			case ImageMessage::TYPE_CODE:
275
				/* Image Message */
276
				if ($realDataLength != 1 + self::BLOB_ID_LEN + self::IMAGE_FILE_SIZE_LEN + self::IMAGE_NONCE_LEN)  {
277
					throw new BadMessageException();
278
				}
279
280
				$blobId = $piece->__invoke(self::BLOB_ID_LEN);
281
				$length = $piece->__invoke(self::IMAGE_FILE_SIZE_LEN);
282
				$nonce = $piece->__invoke(self::IMAGE_NONCE_LEN);
283
				return new ImageMessage(bin2hex($blobId), bin2hex($length), $nonce);
284
			case FileMessage::TYPE_CODE:
285
				/* Image Message */
286
				$decodeResult = json_decode(substr($data, 1), true);
287
				if(null === $decodeResult || false === $decodeResult) {
288
					throw new BadMessageException();
289
				}
290
291
				$values = AssocArray::byJsonString(substr($data, 1), array('b', 't', 'k', 'm', 'n', 's'));
292
				if(null === $values) {
293
					throw new BadMessageException();
294
				}
295
296
				return new FileMessage(
297
					$values->getValue('b'),
298
					$values->getValue('t'),
299
					$values->getValue('k'),
300
					$values->getValue('m'),
301
					$values->getValue('n'),
302
					$values->getValue('s'));
303
			default:
304
				throw new UnsupportedMessageTypeException();
305
		}
306
	}
307
308
	/**
309
	 * Generate a new key pair.
310
	 *
311
	 * @return KeyPair the new key pair
312
	 */
313
	abstract public function generateKeyPair();
314
315
	/**
316
	 * Hashes an email address for identity lookup.
317
	 *
318
	 * @param string $email the email address
319
	 * @return string the email hash (hex)
320
	 */
321
	final public function hashEmail($email) {
322
		$emailClean = strtolower(trim($email));
323
		return hash_hmac('sha256', $emailClean, self::EMAIL_HMAC_KEY);
324
	}
325
326
	/**
327
	 * Hashes an phone number address for identity lookup.
328
	 *
329
	 * @param string $phoneNo the phone number (in E.164 format, no leading +)
330
	 * @return string the phone number hash (hex)
331
	 */
332
	final public function hashPhoneNo($phoneNo) {
333
		$phoneNoClean = preg_replace("/[^0-9]/", "", $phoneNo);
334
		return hash_hmac('sha256', $phoneNoClean, self::PHONENO_HMAC_KEY);
335
	}
336
337
	abstract protected function createRandom($size);
338
339
	/**
340
	 * Generate a random nonce.
341
	 *
342
	 * @return string random nonce
343
	 */
344
	final public function randomNonce() {
345
		return $this->createRandom(\Salt::box_NONCE);
346
	}
347
348
	/**
349
	 * Generate a symmetric key
350
	 * @return mixed
351
	 */
352
	final public function symmetricKey() {
353
		return $this->createRandom(32);
354
	}
355
356
	/**
357
	 * Derive the public key
358
	 *
359
	 * @param string $privateKey as binary
360
	 * @return string as binary
361
	 */
362
	abstract public function derivePublicKey($privateKey);
363
364
	/**
365
	 * Check if implementation supported
366
	 * @return bool
367
	 */
368
	abstract public function isSupported();
369
370
	/**
371
	 * Validate crypt tool
372
	 *
373
	 * @return bool
374
	 * @throws Exception
375
	 */
376
	abstract public function validate();
377
378
	/**
379
	 * @param $data
380
	 * @return EncryptResult
381
	 */
382
	public final function encryptFile($data) {
383
		$key = $this->symmetricKey();
384
		$box = $this->makeSecretBox($data, self::FILE_NONCE, $key);
385
		return new EncryptResult($box, $key, self::FILE_NONCE, strlen($box));
386
	}
387
388
	/**
389
	 * @param string $data as binary
390
	 * @param string $key as binary
391
	 * @return null|string
392
	 */
393
	public final function decryptFile($data, $key) {
394
		$result =  $this->openSecretBox($data, self::FILE_NONCE, $key);
395
		return false === $result ? null : $result;
396
	}
397
398
	/**
399
	 * @param string $data
400
	 * @param string $key
401
	 * @return EncryptResult
402
	 */
403
	public final function encryptFileThumbnail($data, $key) {
404
		$box = $this->makeSecretBox($data, self::FILE_THUMBNAIL_NONCE, $key);
405
		return new EncryptResult($box, $key,  self::FILE_THUMBNAIL_NONCE, strlen($box));
406
	}
407
408
	public final function decryptFileThumbnail($data, $key) {
409
		$result = $this->openSecretBox($data, self::FILE_THUMBNAIL_NONCE, $key);
410
		return false === $result ? null : $result;
411
	}
412
413
	/**
414
	 * @param string $imageData
415
	 * @param string $privateKey as binary
416
	 * @param string $publicKey as binary
417
	 * @return EncryptResult
418
	 */
419
	public final function encryptImage($imageData, $privateKey, $publicKey) {
420
		$nonce = $this->randomNonce();
421
422
		$box = $this->makeBox(
423
			$imageData,
424
			$nonce,
425
			$privateKey,
426
			$publicKey
427
		);
428
429
		return new EncryptResult($box, null, $nonce, strlen($box));
430
	}
431
432
	/**
433
	 * @param string $data as binary
434
	 * @param string $publicKey as binary
435
	 * @param string $privateKey as binary
436
	 * @param string $nonce as binary
437
	 * @return string
438
	 */
439
	public final function decryptImage($data, $publicKey, $privateKey, $nonce) {
440
		return $this->openBox($data,
441
			$privateKey,
442
			$publicKey,
443
			$nonce);
444
	}
445
446
	/**
447
	 * determine random amount of PKCS7 padding
448
	 * @return int
449
	 */
450
	private function generatePadBytes() {
451
		$padbytes = 0;
452
		while($padbytes < 1 || $padbytes > 255) {
453
			$padbytes = ord($this->createRandom(1));
454
		}
455
		return $padbytes;
456
	}
457
458
	function __toString() {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
459
		return 'CryptTool '.$this->getName();
460
	}
461
462
	/**
463
	 * Converts a binary string to an hexdecimal string.
464
	 *
465
	 * This is the same as PHP's bin2hex() implementation, but it is resistant to
466
	 * timing attacks.
467
	 *
468
	 * @param  string $binaryString The binary string to convert
469
	 * @return string
470
	 */
471
	public function bin2hex($binaryString)
472
	{
473
		return bin2hex($binaryString);
474
	}
475
476
	/**
477
	 * Converts an hexdecimal string to a binary string.
478
	 *
479
	 * This is the same as PHP's hex2bin() implementation, but it is resistant to
480
	 * timing attacks.
481
	 * Note that the default implementation does not support $ignore currrently and will
482
	 * throw an error. Only when libsodium >= 0.22 is used, this is supported.
483
	 *
484
	 * @param  string $hexString The hex string to convert
485
	 * @param  string|null $ignore	(optional) Characters to ignore
486
	 * @throws \Threema\Core\Exception
487
	 * @return string
488
	 */
489
	public function hex2bin($hexString, $ignore = null)
490
	{
491
		if ($ignore !== null) {
492
			throw new Exception('$ignore parameter is not supported');
493
		}
494
		return hex2bin($hexString);
495
	}
496
497
	/**
498
	 * Compares two strings in a secure way.
499
	 *
500
	 * This is the same as PHP's strcmp() implementation, but it is resistant to
501
	 * timing attacks.
502
	 *
503
	 * @link https://paragonie.com/book/pecl-libsodium/read/03-utilities-helpers.md#compare
504
	 * @param  string $str1 The first string
505
	 * @param  string $str2 The second string
506
	 * @return int
507
	 */
508
	public function stringCompare($str1, $str2)
509
	{
510
		if (function_exists('hash_equals')) {
511
			return hash_equals($str1, $str2);
512
		} else {
513
			// check variable type manually
514
			if (!is_string($str1) || !is_string($str2)) {
515
				return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by Threema\MsgApi\Tools\CryptTool::stringCompare of type integer.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
516
			}
517
518
			// fast comparison: check string length
519
			if (strlen($str1) != strlen($str2)) {
520
				return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by Threema\MsgApi\Tools\CryptTool::stringCompare of type integer.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
521
			}
522
523
			# PHP implementation of hash_equals
524
			# partly taken from https://github.com/symfony/polyfill-php56/blob/master/Php56.php#L45-L51
525
			$ret = 0;
526
			for ($i = 0; $i < strlen($str1); ++$i) {
527
	            $ret |= ord($str1[$i]) ^ ord($str2[$i]);
528
	        }
529
			return 0 === $result;
0 ignored issues
show
Bug introduced by
The variable $result does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug Best Practice introduced by
The return type of return 0 === $result; (boolean) is incompatible with the return type documented by Threema\MsgApi\Tools\CryptTool::stringCompare of type integer.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
530
		}
531
	}
532
533
	/**
534
	 * Name of the CryptTool
535
	 * @return string
536
	 */
537
	abstract public function getName();
538
539
	/**
540
	 * Description of the CryptTool
541
	 * @return string
542
	 */
543
	abstract public function getDescription();
544
}
545