E2EHelper::sendFileMessage()   B
last analyzed

Complexity

Conditions 8
Paths 5

Size

Total Lines 48

Duplication

Lines 6
Ratio 12.5 %

Importance

Changes 0
Metric Value
dl 6
loc 48
rs 7.8901
c 0
b 0
f 0
cc 8
nc 5
nop 3
1
<?php
2
/**
3
 * @author Threema GmbH
4
 * @copyright Copyright (c) 2015-2016 Threema GmbH
5
 */
6
7
8
namespace Threema\MsgApi\Helpers;
9
10
use Threema\MsgApi\Commands\Results\CapabilityResult;
11
use Threema\MsgApi\Connection;
12
use Threema\MsgApi\Messages\FileMessage;
13
use Threema\MsgApi\Messages\ImageMessage;
14
use Threema\MsgApi\Messages\ThreemaMessage;
15
use Threema\MsgApi\Tools\CryptTool;
16
use Threema\Core\Exception;
17
use Threema\MsgApi\Tools\FileAnalysisTool;
18
19
class E2EHelper {
20
	/**
21
	 * @var Connection
22
	 */
23
	private $connection;
24
25
	/**
26
	 * @var CryptTool
27
	 */
28
	private $cryptTool;
29
30
	/**
31
	 * @var string (bin)
32
	 */
33
	private $privateKey;
34
35
	/**
36
	 * @param string $privateKey (binary)
37
	 * @param Connection $connection
38
	 * @param CryptTool $cryptTool
39
	 */
40
	public function __construct($privateKey, Connection $connection, CryptTool $cryptTool = null) {
41
		$this->connection = $connection;
42
		$this->cryptTool = $cryptTool;
43
		$this->privateKey = $privateKey;
44
45
		if(null === $this->cryptTool) {
46
			$this->cryptTool = CryptTool::getInstance();
47
		}
48
	}
49
50
	/**
51
	 * Encrypt a text message and send it to the threemaId
52
	 *
53
	 * @param string $threemaId
54
	 * @param string $text
55
	 * @throws \Threema\Core\Exception
56
	 * @return \Threema\MsgApi\Commands\Results\SendE2EResult
57
	 */
58
	public final function sendTextMessage($threemaId, $text) {
59
		//random nonce first
60
		$nonce = $this->cryptTool->randomNonce();
61
62
		//fetch the public key
63
		$receiverPublicKey = $this->fetchPublicKeyAndCheckCapability($threemaId, null);
64
65
		//create a box
66
		$textMessage = $this->cryptTool->encryptMessageText(
67
			$text,
68
			$this->privateKey,
69
			$receiverPublicKey,
70
			$nonce);
71
72
		return $this->connection->sendE2E($threemaId, $nonce, $textMessage);
73
	}
74
75
	/**
76
	 * Encrypt an image file, upload the blob and send the image message to the threemaId
77
	 *
78
	 * @param string $threemaId
79
	 * @param string $imagePath
80
	 * @return \Threema\MsgApi\Commands\Results\SendE2EResult
81
	 * @throws \Threema\Core\Exception
82
	 */
83
	public final function sendImageMessage($threemaId, $imagePath) {
84
		//analyse the file
85
		$fileAnalyzeResult = FileAnalysisTool::analyse($imagePath);
86
87
		if(null === $fileAnalyzeResult) {
88
			throw new Exception('could not analyze the file');
89
		}
90
91
		if(false === in_array($fileAnalyzeResult->getMimeType(), array(
92
				'image/jpg',
93
				'image/jpeg',
94
				'image/png' ))) {
95
			throw new Exception('file is not a jpg or png');
96
		}
97
98
		//fetch the public key
99
		$receiverPublicKey = $this->fetchPublicKeyAndCheckCapability($threemaId, function(CapabilityResult $capabilityResult) {
100
			return true === $capabilityResult->canImage();
101
		});
102
103
		//encrypt the image file
104
		$encryptionResult = $this->cryptTool->encryptImage(file_get_contents($imagePath), $this->privateKey, $receiverPublicKey);
105
		$uploadResult =  $this->connection->uploadFile($encryptionResult->getData());
106
107 View Code Duplication
		if($uploadResult === null || !$uploadResult->isSuccess()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
108
			throw new Exception('could not upload the image ('.$uploadResult->getErrorCode().' '.$uploadResult->getErrorMessage().') '.$uploadResult->getRawResponse());
109
		}
110
111
		$nonce = $this->cryptTool->randomNonce();
112
113
		//create a image message box
114
		$imageMessage = $this->cryptTool->encryptImageMessage(
115
			$uploadResult,
116
			$encryptionResult,
117
			$this->privateKey,
118
			$receiverPublicKey,
119
			$nonce);
120
121
		return $this->connection->sendE2E($threemaId, $nonce, $imageMessage);
122
	}
123
124
	/**
125
	 * Encrypt a file (and thumbnail if given), upload the blob and send it to the given threemaId
126
	 *
127
	 * @param string $threemaId
128
	 * @param string $filePath
129
	 * @param null|string $thumbnailPath
130
	 * @throws \Threema\Core\Exception
131
	 * @return \Threema\MsgApi\Commands\Results\SendE2EResult
132
	 */
133
	public final function sendFileMessage($threemaId, $filePath, $thumbnailPath = null) {
134
		//analyse the file
135
		$fileAnalyzeResult = FileAnalysisTool::analyse($filePath);
136
137
		if(null === $fileAnalyzeResult) {
138
			throw new Exception('could not analyze the file');
139
		}
140
141
		//fetch the public key
142
		$receiverPublicKey = $this->fetchPublicKeyAndCheckCapability($threemaId, function(CapabilityResult $capabilityResult) {
143
			return true === $capabilityResult->canFile();
144
		});
145
146
		//encrypt the main file
147
		$encryptionResult = $this->cryptTool->encryptFile(file_get_contents($filePath));
148
		$uploadResult =  $this->connection->uploadFile($encryptionResult->getData());
149
150 View Code Duplication
		if($uploadResult === null || !$uploadResult->isSuccess()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
151
			throw new Exception('could not upload the file ('.$uploadResult->getErrorCode().' '.$uploadResult->getErrorMessage().') '.$uploadResult->getRawResponse());
152
		}
153
154
		$thumbnailUploadResult = null;
155
156
		//encrypt the thumbnail file (if exists)
157
		if(strlen($thumbnailPath) > 0 && true === file_exists($thumbnailPath)) {
158
			//encrypt the main file
159
			$thumbnailEncryptionResult = $this->cryptTool->encryptFileThumbnail(file_get_contents($thumbnailPath), $encryptionResult->getKey());
160
			$thumbnailUploadResult = $this->connection->uploadFile($thumbnailEncryptionResult->getData());
161
162 View Code Duplication
			if($thumbnailUploadResult === null || !$thumbnailUploadResult->isSuccess()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
163
				throw new Exception('could not upload the thumbnail file ('.$thumbnailUploadResult->getErrorCode().' '.$thumbnailUploadResult->getErrorMessage().') '.$thumbnailUploadResult->getRawResponse());
164
			}
165
		}
166
167
		$nonce = $this->cryptTool->randomNonce();
168
169
		//create a file message box
170
		$fileMessage = $this->cryptTool->encryptFileMessage(
171
			$uploadResult,
172
			$encryptionResult,
173
			$thumbnailUploadResult,
174
			$fileAnalyzeResult,
175
			$this->privateKey,
176
			$receiverPublicKey,
177
			$nonce);
178
179
		return $this->connection->sendE2E($threemaId, $nonce, $fileMessage);
180
	}
181
182
183
	/**
184
	 * Decrypt a message and download the files of the message to the $outputFolder
185
	 *
186
	 * Note: This does not check the MAC before, which you should always do when
187
	 * you want to use this in your own application! Use {@link checkMac()} for doing so.
188
	 *
189
	 * @param string $threemaId The sender ID (= the ID the message came from)
190
	 * @param string $messageId
191
	 * @param string $box box as binary string
192
	 * @param string $nonce nonce as binary string
193
	 * @param string|null|false $outputFolder folder for storing the files,
194
	 * 							null=current folder, false=do not download files
195
	 * @param \Closure $downloadMessage
196
	 * @return ReceiveMessageResult
197
	 * @throws Exception
198
	 * @throws \Threema\MsgApi\Exceptions\BadMessageException
199
	 * @throws \Threema\MsgApi\Exceptions\DecryptionFailedException
200
	 * @throws \Threema\MsgApi\Exceptions\UnsupportedMessageTypeException
201
	 */
202
	public final function receiveMessage($threemaId,
203
										 $messageId,
204
										 $box,
205
										 $nonce,
206
										 $outputFolder = null,
207
										 \Closure $downloadMessage = null) {
208
209
		//fetch the public key
210
		$receiverPublicKey = $this->connection->fetchPublicKey($threemaId);
211
212
		if(null === $receiverPublicKey || !$receiverPublicKey->isSuccess()) {
213
			throw new Exception('Invalid threema id');
214
		}
215
216
		$message = $this->cryptTool->decryptMessage(
217
			$box,
218
			$this->privateKey,
219
			$this->cryptTool->hex2bin($receiverPublicKey->getPublicKey()),
220
			$nonce
221
		);
222
223
		if(null === $message || false === is_object($message)) {
224
			throw new Exception('Could not encrypt box');
225
		}
226
227
		$receiveResult = new ReceiveMessageResult($messageId, $message);
228
229
		if($outputFolder === false) {
230
			return $receiveResult;
231
		}
232
		if($outputFolder === null || strlen($outputFolder) == 0) {
233
			$outputFolder = '.';
234
		}
235
236
		if($message instanceof ImageMessage) {
237
			$result = $this->downloadFile($message, $message->getBlobId(), $downloadMessage);
238
			if(null !== $result && true === $result->isSuccess()) {
239
				$image = $this->cryptTool->decryptImage(
240
					$result->getData(),
241
					$this->cryptTool->hex2bin($receiverPublicKey->getPublicKey()),
242
					$this->privateKey,
243
					$message->getNonce()
244
				);
245
246
				if (null === $image) {
247
					throw new Exception('decryption of image failed');
248
				}
249
				//save file
250
				$filePath = $outputFolder . '/' . $messageId . '.jpg';
251
				$f = fopen($filePath, 'w+');
252
				fwrite($f, $image);
253
				fclose($f);
254
255
				$receiveResult->addFile('image', $filePath);
256
			}
257
		}
258
		else if($message instanceof FileMessage) {
259
			$result = $this->downloadFile($message, $message->getBlobId(), $downloadMessage);
260
261 View Code Duplication
			if(null !== $result && true === $result->isSuccess()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
262
				$file = $this->cryptTool->decryptFile(
263
					$result->getData(),
264
					$this->cryptTool->hex2bin($message->getEncryptionKey()));
265
266
				if (null === $file) {
267
					throw new Exception('file decryption failed');
268
				}
269
270
				//save file
271
				$filePath = $outputFolder . '/' . $messageId . '-' . $message->getFilename();
272
				file_put_contents($filePath, $file);
273
274
				$receiveResult->addFile('file', $filePath);
275
			}
276
277
			if(null !== $message->getThumbnailBlobId() && strlen($message->getThumbnailBlobId()) > 0) {
278
				$result = $this->downloadFile($message, $message->getThumbnailBlobId(), $downloadMessage);
279 View Code Duplication
				if(null !== $result && true === $result->isSuccess()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
280
					$file = $this->cryptTool->decryptFileThumbnail(
281
						$result->getData(),
282
						$this->cryptTool->hex2bin($message->getEncryptionKey()));
283
284
					if(null === $file) {
285
						throw new Exception('thumbnail decryption failed');
286
					}
287
					//save file
288
					$filePath = $outputFolder.'/'.$messageId.'-thumbnail-'.$message->getFilename();
289
					file_put_contents($filePath, $file);
290
291
					$receiveResult->addFile('thumbnail', $filePath);
292
				}
293
			}
294
		}
295
296
		return $receiveResult;
297
	}
298
299
	/**
300
	 * Check the HMAC of an ingoing Threema request. Always do this before de-
301
	 * crypting the message.
302
	 *
303
	 * @param string $threemaId
304
	 * @param string $gatewayId
305
	 * @param string $messageId
306
	 * @param string $date
307
	 * @param string $nonce nonce as hex encoded string
308
	 * @param string $box box as hex encoded string
309
	 * @param string $mac the original one send by the server
310
	 *
311
	 * @return bool true if check was successfull, false if not
312
	 */
313
	public final function checkMac($threemaId, $gatewayId, $messageId, $date, $nonce, $box, $mac, $secret) {
314
		$calculatedMac = hash_hmac('sha256', $threemaId.$gatewayId.$messageId.$date.$nonce.$box, $secret);
315
		return $this->cryptTool->stringCompare($calculatedMac, $mac) === true;
316
	}
317
318
	/**
319
	 * Fetch a public key and check the capability of the threemaId
320
	 *
321
	 * @param string $threemaId
322
	 * @param \Closure $capabilityCheck
323
	 * @return string Public key as binary
324
	 * @throws Exception
325
	 */
326
	private final function fetchPublicKeyAndCheckCapability($threemaId, \Closure $capabilityCheck = null) {
327
		//fetch the public key
328
		$receiverPublicKey = $this->connection->fetchPublicKey($threemaId);
329
330
		if(null === $receiverPublicKey || !$receiverPublicKey->isSuccess()) {
331
			throw new Exception('Invalid threema id');
332
		}
333
334
		if(null !== $capabilityCheck) {
335
			//check capability
336
			$capability = $this->connection->keyCapability($threemaId);
337
			if(null === $capability || false === $capabilityCheck->__invoke($capability)) {
338
				throw new Exception('threema id does not have the capability');
339
			}
340
		}
341
342
		return $this->cryptTool->hex2bin($receiverPublicKey->getPublicKey());
343
	}
344
345
	/**
346
	 * @param ThreemaMessage $message
347
	 * @param string $blobId blob id as hex
348
	 * @param \Closure|null $downloadMessage
349
	 * @return null|\Threema\MsgApi\Commands\Results\DownloadFileResult
350
	 * @throws Exception
351
	 */
352
	private final function downloadFile(ThreemaMessage $message, $blobId, \Closure $downloadMessage = null) {
353
		if(null === $downloadMessage
354
			|| true === $downloadMessage->__invoke($message, $blobId)) {
355
			//make a download
356
			$result = $this->connection->downloadFile($blobId);
357
			if(null === $result || false === $result->isSuccess()) {
358
				throw new Exception('could not download the file with blob id '.$blobId);
359
			}
360
361
			return $result;
362
		}
363
		return null;
364
	}
365
}
366