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()) { |
|
|
|
|
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()) { |
|
|
|
|
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()) { |
|
|
|
|
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()) { |
|
|
|
|
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()) { |
|
|
|
|
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
|
|
|
|
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.