Completed
Push — official ( ce7ca4...b4d2c6 )
by rugk
03:07
created

CryptTool::generatePadBytes()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 7
rs 9.4286
cc 3
eloc 5
nc 2
nop 0
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
	 * Name of the CryptTool
464
	 * @return string
465
	 */
466
	abstract public function getName();
467
468
	/**
469
	 * Description of the CryptTool
470
	 * @return string
471
	 */
472
	abstract public function getDescription();
473
}
474