Test Failed
Push — master ( 63c9aa...89bf96 )
by
unknown
21:07 queued 14s
created

Pluginsmime::getSenderAddress()   B

Complexity

Conditions 6
Paths 47

Size

Total Lines 30
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 21
c 1
b 0
f 0
nc 47
nop 1
dl 0
loc 30
rs 8.9617
1
<?php
2
3
include_once 'util.php';
4
require_once 'class.certificate.php';
5
6
// Green, everything was good
7
define('SMIME_STATUS_SUCCESS', 0);
8
// Orange, CA is missing or OCSP is not available
9
define('SMIME_STATUS_PARTIAL', 1);
10
// Red, something really went wrong
11
define('SMIME_STATUS_FAIL', 2);
12
// Blue, info message
13
define('SMIME_STATUS_INFO', 3);
14
15
define('SMIME_SUCCESS', 0);
16
define('SMIME_NOPUB', 1);
17
define('SMIME_CERT_EXPIRED', 2);
18
define('SMIME_ERROR', 3);
19
define('SMIME_REVOKED', 4);
20
define('SMIME_CA', 5);
21
define('SMIME_DECRYPT_SUCCESS', 6);
22
define('SMIME_DECRYPT_FAILURE', 7);
23
define('SMIME_UNLOCK_CERT', 8);
24
define('SMIME_OCSP_NOSUPPORT', 9);
25
define('SMIME_OCSP_DISABLED', 10);
26
define('SMIME_OCSP_FAILED', 11);
27
define('SMIME_DECRYPT_CERT_MISMATCH', 12);
28
define('SMIME_USER_DETECT_FAILURE', 13);
29
30
// OpenSSL Error Constants
31
// openssl_error_string() returns error codes when an operation fails, since we return custom error strings
32
// in our plugin we keep a list of openssl error codes in these defines
33
define('OPENSSL_CA_VERIFY_FAIL', '21075075');
34
define('OPENSSL_RECIPIENT_CERTIFICATE_MISMATCH', '21070073');
35
36
class Pluginsmime extends Plugin {
37
	/**
38
	 * decrypted/verified message.
39
	 */
40
	private $message = [];
41
42
	/**
43
	 * Default MAPI Message Store.
44
	 */
45
	private $store;
46
47
	/**
48
	 * Last openssl error string.
49
	 */
50
	private $openssl_error = "";
51
52
	/**
53
	 * Cipher to use.
54
	 */
55
	private $cipher = PLUGIN_SMIME_CIPHER;
56
57
	/**
58
	 * Called to initialize the plugin and register for hooks.
59
	 */
60
	public function init() {
61
		$this->registerHook('server.core.settings.init.before');
62
		$this->registerHook('server.util.parse_smime.signed');
63
		$this->registerHook('server.util.parse_smime.encrypted');
64
		$this->registerHook('server.module.itemmodule.open.after');
65
		$this->registerHook('server.core.operations.submitmessage');
66
		$this->registerHook('server.upload_attachment.upload');
67
		$this->registerHook('server.module.createmailitemmodule.beforesend');
68
		$this->registerHook('server.index.load.custom');
69
70
		if (version_compare(phpversion(), '5.4', '<')) {
71
			$this->cipher = OPENSSL_CIPHER_3DES;
72
		}
73
	}
74
75
	/**
76
	 * Default message store.
77
	 *
78
	 * @return object MAPI Message store
79
	 */
80
	public function getStore() {
81
		if (!$this->store) {
82
			$this->store = $GLOBALS['mapisession']->getDefaultMessageStore();
83
		}
84
85
		return $this->store;
86
	}
87
88
	/**
89
	 * Process the incoming events that where fired by the client.
90
	 *
91
	 * @param string $eventID Identifier of the hook
92
	 * @param array  $data    Reference to the data of the triggered hook
93
	 */
94
	public function execute($eventID, &$data) {
95
		switch ($eventID) {
96
			// Register plugin
97
			case 'server.core.settings.init.before':
98
				$this->onBeforeSettingsInit($data);
99
				break;
100
101
				// Verify a signed or encrypted message when an email is opened
102
			case 'server.util.parse_smime.signed':
103
				$this->onSignedMessage($data);
104
				break;
105
106
			case 'server.util.parse_smime.encrypted':
107
				$this->onEncrypted($data);
108
				break;
109
110
				// Add S/MIME property, which is send to the client
111
			case 'server.module.itemmodule.open.after':
112
				$this->onAfterOpen($data);
113
				break;
114
115
				// Catch uploaded certificate
116
			case 'server.upload_attachment.upload':
117
				$this->onUploadCertificate($data);
118
				break;
119
120
				// Sign email before sending
121
			case 'server.core.operations.submitmessage':
122
				$this->onBeforeSend($data);
123
				break;
124
125
				// Verify that we have public certificates for all recipients
126
			case 'server.module.createmailitemmodule.beforesend':
127
				$this->onCertificateCheck($data);
128
				break;
129
130
			case 'server.index.load.custom':
131
				if ($data['name'] === 'smime_passphrase') {
132
					include 'templates/passphrase.tpl.php';
133
134
					exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
135
				}
136
				if ($data['name'] === 'smime_passphrasecheck') {
137
					// No need to do anything, this is just used to trigger
138
					// the browser's autofill save password dialog.
139
					exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
140
				}
141
				break;
142
		}
143
	}
144
145
	/**
146
	 * Function checks if public certificate exists for all recipients and creates an error
147
	 * message for the frontend which includes the email address of the missing public
148
	 * certificates.
149
	 *
150
	 * If my own certificate is missing, a different error message is shown which informs the
151
	 * user that his own public certificate is missing and required for reading encrypted emails
152
	 * in the 'Sent items' folder.
153
	 *
154
	 * @param array $data Reference to the data of the triggered hook
155
	 */
156
	public function onCertificateCheck($data) {
157
		$entryid = $data['entryid'];
158
		// FIXME: unittests, save trigger will pass $entryid is 0 (which will open the root folder and not the message we want)
159
		if ($entryid === false) {
160
			return;
161
		}
162
163
		if (!isset($data['action']['props']['smime']) || empty($data['action']['props']['smime'])) {
164
			return;
165
		}
166
167
		$message = mapi_msgstore_openentry($data['store'], $entryid);
168
		$module = $data['moduleObject'];
169
		$data['success'] = true;
170
171
		$messageClass = mapi_getprops($message, [PR_MESSAGE_CLASS]);
172
		$messageClass = $messageClass[PR_MESSAGE_CLASS];
173
		if ($messageClass !== 'IPM.Note.SMIME' &&
174
			$messageClass !== 'IPM.Note.SMIME.SignedEncrypt' &&
175
			$messageClass !== 'IPM.Note.deferSMIME' &&
176
			$messageClass !== 'IPM.Note.deferSMIME.SignedEncrypt') {
177
			return;
178
		}
179
180
		$recipients = $data['action']['props']['smime'];
181
		$missingCerts = [];
182
183
		foreach ($recipients as $recipient) {
184
			$email = $recipient['email'];
185
186
			if (!$this->pubcertExists($email, $recipient['internal'])) {
187
				array_push($missingCerts, $email);
188
			}
189
		}
190
191
		if (empty($missingCerts)) {
192
			return;
193
		}
194
195
		function missingMyself($email) {
196
			return $GLOBALS['mapisession']->getSMTPAddress() === $email;
197
		}
198
199
		if (array_filter($missingCerts, "missingMyself") === []) {
200
			$errorMsg = _('Missing public certificates for the following recipients: ') . implode(', ', $missingCerts) . _('. Please contact your system administrator for details');
201
		}
202
		else {
203
			$errorMsg = _("Your public certificate is not installed. Without this certificate, you will not be able to read encrypted messages you have sent to others.");
204
		}
205
206
		$module->sendFeedback(false, ["type" => ERROR_GENERAL, "info" => ['display_message' => $errorMsg]]);
207
		$data['success'] = false;
208
	}
209
210
	/**
211
	 * Function which verifies a message.
212
	 *
213
	 * TODO: Clean up flow
214
	 *
215
	 * @param mixed $message
216
	 * @param mixed $eml
217
	 */
218
	public function verifyMessage($message, $eml) {
219
		$userCert = '';
220
		$tmpUserCert = tempnam(sys_get_temp_dir(), true);
0 ignored issues
show
Bug introduced by
true of type true is incompatible with the type string expected by parameter $prefix of tempnam(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

220
		$tmpUserCert = tempnam(sys_get_temp_dir(), /** @scrutinizer ignore-type */ true);
Loading history...
221
		$importMessageCert = true;
222
		$fromGAB = false;
223
224
		// TODO: worth to split fetching public certificate in a separate function?
225
226
		// If user entry exists in GAB, try to retrieve public cert
227
		// Public certificate from GAB in combination with LDAP saved in PR_EMS_AB_X509_CERT
228
		$userProps = mapi_getprops($message, [PR_SENT_REPRESENTING_ENTRYID, PR_SENT_REPRESENTING_NAME]);
229
		if (isset($userProps[PR_SENT_REPRESENTING_ENTRYID])) {
230
			try {
231
				$user = mapi_ab_openentry($GLOBALS['mapisession']->getAddressbook(), $userProps[PR_SENT_REPRESENTING_ENTRYID]);
232
				$gabCert = $this->getGABCert($user);
233
				if (!empty($gabCert)) {
234
					$fromGAB = true;
235
					// Put empty string into file? dafuq?
236
					file_put_contents($tmpUserCert, $userCert);
237
				}
238
			}
239
			catch (MAPIException $e) {
240
				$msg = "[smime] Unable to open PR_SENT_REPRESENTING_ENTRYID. Maybe %s was does not exists or deleted from server.";
241
				Log::write(LOGLEVEL_ERROR, sprintf($msg, $userProps[PR_SENT_REPRESENTING_NAME]));
242
				error_log("[smime] Unable to open PR_SENT_REPRESENTING_NAME: " . print_r($userProps[PR_SENT_REPRESENTING_NAME], true));
0 ignored issues
show
Bug introduced by
Are you sure print_r($userProps[PR_SE...PRESENTING_NAME], true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

242
				error_log("[smime] Unable to open PR_SENT_REPRESENTING_NAME: " . /** @scrutinizer ignore-type */ print_r($userProps[PR_SENT_REPRESENTING_NAME], true));
Loading history...
243
				$this->message['success'] = SMIME_NOPUB;
244
				$this->message['info'] = SMIME_USER_DETECT_FAILURE;
245
			}
246
		}
247
248
		// When downloading an email as eml, $GLOBALS['operations'] isn't set, so add a check so that downloading works
249
		// If the certificate is already fetch from the GAB, skip checking the userStore.
250
		if (!$fromGAB && isset($GLOBALS['operations'])) {
251
			$senderAddressArray = $this->getSenderAddress($message);
252
			$senderAddressArray = $senderAddressArray['props'];
253
			if ($senderAddressArray['address_type'] === 'SMTP') {
254
				$emailAddr = $senderAddressArray['email_address'];
255
			}
256
			else {
257
				$emailAddr = $senderAddressArray['smtp_address'];
258
			}
259
260
			// User not in AB,
261
			// so get email address from either PR_SENT_REPRESENTING_NAME, PR_SEARCH_KEY or PR_SENT_REPRESENTING_SEARCH_KEY
262
			// of the message
263
			if (!$emailAddr) {
264
				if (!empty($userProps[PR_SENT_REPRESENTING_NAME])) {
265
					$emailAddr = $userProps[PR_SENT_REPRESENTING_NAME];
266
				}
267
				else {
268
					$searchKeys = mapi_getprops($message, [PR_SEARCH_KEY, PR_SENT_REPRESENTING_SEARCH_KEY]);
269
					$searchKey = $searchKeys[PR_SEARCH_KEY] ?? $searchKeys[PR_SENT_REPRESENTING_SEARCH_KEY];
270
					if ($searchKey) {
271
						$sk = strtolower(explode(':', $searchKey)[1]);
272
						$emailAddr = trim($sk);
273
					}
274
				}
275
			}
276
277
			if ($emailAddr) {
278
				// Get all public certificates of $emailAddr stored on the server
279
				$userCerts = $this->getPublicKey($emailAddr, true);
280
			}
281
		}
282
283
		// Save signed message in a random file
284
		$tmpfname = tempnam(sys_get_temp_dir(), true);
285
		file_put_contents($tmpfname, $eml);
286
287
		// Create random file for saving the signed message
288
		$outcert = tempnam(sys_get_temp_dir(), true);
289
290
		// Verify signed message
291
		// Returns True if verified, False if tampered or signing certificate invalid OR -1 on error
292
		if (count($userCerts) > 0) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $userCerts does not seem to be defined for all execution paths leading up to this point.
Loading history...
293
			// Try to verify a certificate in the MAPI store
294
			foreach ($userCerts as $userCert) {
295
				$userCert = base64_decode($userCert);
296
				// Save signed message in a random file
297
				$tmpfname = tempnam(sys_get_temp_dir(), true);
298
				file_put_contents($tmpfname, $eml);
299
300
				// Create random file for saving the signed message
301
				$outcert = tempnam(sys_get_temp_dir(), true);
302
303
				if (!empty($userCert)) { // Check MAPI UserStore
304
					file_put_contents($tmpUserCert, $userCert);
305
				}
306
				$signed_ok = openssl_pkcs7_verify($tmpfname, PKCS7_NOINTERN, $outcert, explode(';', PLUGIN_SMIME_CACERTS), $tmpUserCert);
307
				$openssl_error_code = $this->extract_openssl_error();
308
				$this->validateSignedMessage($signed_ok, $openssl_error_code);
309
				// Check if we need to import a newer certificate
310
				$importCert = file_get_contents($outcert);
311
				$parsedImportCert = openssl_x509_parse($importCert);
312
				$parsedUserCert = openssl_x509_parse($userCert);
313
				if (!$signed_ok || $openssl_error_code === OPENSSL_CA_VERIFY_FAIL) {
314
					continue;
315
				}
316
317
				// CA Checks out
318
				$caCerts = $this->extractCAs($tmpfname);
319
				// If validTo and validFrom are more in the future, emailAddress matches and OCSP check is valid, import newer certificate
320
				if (is_array($parsedImportCert) && is_array($parsedUserCert) &&
321
					$parsedImportCert['validTo'] > $parsedUserCert['validTo'] &&
322
					$parsedImportCert['validFrom'] > $parsedUserCert['validFrom'] &&
323
					getCertEmail($parsedImportCert) === getCertEmail($parsedUserCert) &&
324
					verifyOCSP($importCert, $caCerts, $this->message) &&
325
					$importMessageCert !== false) {
326
					// Redundant
327
					$importMessageCert = true;
328
				}
329
				else {
330
					$importMessageCert = false;
331
					verifyOCSP($userCert, $caCerts, $this->message);
332
					break;
333
				}
334
			}
335
		}
336
		else {
337
			// Works. Just leave it.
338
			$signed_ok = openssl_pkcs7_verify($tmpfname, PKCS7_NOSIGS, $outcert, explode(';', PLUGIN_SMIME_CACERTS));
339
			$openssl_error_code = $this->extract_openssl_error();
340
			$this->validateSignedMessage($signed_ok, $openssl_error_code);
341
342
			// OCSP check
343
			if ($signed_ok && $openssl_error_code !== OPENSSL_CA_VERIFY_FAIL) { // CA Checks out
344
				$userCert = file_get_contents($outcert);
345
				$parsedImportCert = openssl_x509_parse($userCert);
346
347
				$caCerts = $this->extractCAs($tmpfname);
348
				if (!is_array($parsedImportCert) || !verifyOCSP($userCert, $caCerts, $this->message)) {
0 ignored issues
show
introduced by
The condition is_array($parsedImportCert) is always true.
Loading history...
349
					$importMessageCert = false;
350
				}
351
			// We don't have a certificate from the MAPI UserStore or LDAP, so we will set $userCert to $importCert
352
			// so that we can verify the message according to the be imported certificate.
353
			}
354
			else { // No pubkey
355
				$importMessageCert = false;
356
				Log::write(LOGLEVEL_INFO, sprintf("[smime] Unable to verify message without public key, openssl error: '%s'", $this->openssl_error));
0 ignored issues
show
Bug introduced by
It seems like $this->openssl_error can also be of type false; however, parameter $values of sprintf() does only seem to accept double|integer|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

356
				Log::write(LOGLEVEL_INFO, sprintf("[smime] Unable to verify message without public key, openssl error: '%s'", /** @scrutinizer ignore-type */ $this->openssl_error));
Loading history...
357
				$this->message['success'] = SMIME_STATUS_FAIL;
358
				$this->message['info'] = SMIME_CA;
359
			}
360
		}
361
		// Certificate is newer or not yet imported to the user store and not revoked
362
		// If certificate is from the GAB, then don't import it.
363
		if ($importMessageCert && !$fromGAB) {
364
			$signed_ok = openssl_pkcs7_verify($tmpfname, PKCS7_NOSIGS, $outcert, explode(';', PLUGIN_SMIME_CACERTS));
365
			$openssl_error_code = $this->extract_openssl_error();
366
			$this->validateSignedMessage($signed_ok, $openssl_error_code);
367
			$userCert = file_get_contents($outcert);
368
			$parsedImportCert = openssl_x509_parse($userCert);
369
			// FIXME: doing this in importPublicKey too...
370
			$certEmail = getCertEmail($parsedImportCert);
371
			if (!empty($certEmail)) {
372
				$this->importCertificate($userCert, $parsedImportCert, 'public', true);
373
			}
374
		}
375
376
		// Remove extracted certificate from openssl_pkcs7_verify
377
		unlink($outcert);
378
379
		// remove the temporary file
380
		unlink($tmpfname);
381
382
		// Clean up temp cert
383
		unlink($tmpUserCert);
384
	}
385
386
	/**
387
	 * Function which decrypts an encrypted message.
388
	 * The key should be unlocked and stored in the EncryptionStore for a successful decrypt
389
	 * If the key isn't in the session, we give the user a message to unlock his certificate.
390
	 *
391
	 * @param mixed $data array of data from hook
392
	 */
393
	public function onEncrypted($data) {
394
		// Cert unlocked, decode message
395
		$this->message['success'] = SMIME_STATUS_INFO;
396
		$this->message['info'] = SMIME_DECRYPT_FAILURE;
397
398
		$this->message['type'] = 'encrypted';
399
		$encryptionStore = EncryptionStore::getInstance();
400
		$pass = $encryptionStore->get('smime');
401
402
		$tmpFile = tempnam(sys_get_temp_dir(), true);
0 ignored issues
show
Bug introduced by
true of type true is incompatible with the type string expected by parameter $prefix of tempnam(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

402
		$tmpFile = tempnam(sys_get_temp_dir(), /** @scrutinizer ignore-type */ true);
Loading history...
403
		// Write mime header. Because it's not provided in the attachment, otherwise openssl won't parse it
404
		$fp = fopen($tmpFile, 'w');
405
		fwrite($fp, "Content-Type: application/pkcs7-mime; name=\"smime.p7m\"; smime-type=enveloped-data\n");
406
		fwrite($fp, "Content-Transfer-Encoding: base64\nContent-Disposition: attachment; filename=\"smime.p7m\"\n");
407
		fwrite($fp, "Content-Description: S/MIME Encrypted Message\n\n");
408
		fwrite($fp, chunk_split(base64_encode($data['data']), 72) . "\n");
409
		fclose($fp);
410
		if (isset($pass) && !empty($pass)) {
411
			$certs = readPrivateCert($this->getStore(), $pass, false);
0 ignored issues
show
Bug introduced by
$this->getStore() of type object is incompatible with the type resource expected by parameter $store of readPrivateCert(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

411
			$certs = readPrivateCert(/** @scrutinizer ignore-type */ $this->getStore(), $pass, false);
Loading history...
412
			// create random file for saving the encrypted and body message
413
			$tmpDecrypted = tempnam(sys_get_temp_dir(), true);
414
415
			$decryptStatus = false;
416
			// If multiple private certs were decrypted with supplied password
417
			if (!$certs['cert'] && count($certs) > 0) {
418
				foreach ($certs as $cert) {
419
					$decryptStatus = openssl_pkcs7_decrypt($tmpFile, $tmpDecrypted, $cert['cert'], [$cert['pkey'], $pass]);
420
					if ($decryptStatus !== false) {
421
						break;
422
					}
423
				}
424
			}
425
			else {
426
				$decryptStatus = openssl_pkcs7_decrypt($tmpFile, $tmpDecrypted, $certs['cert'], [$certs['pkey'], $pass]);
427
			}
428
429
			$content = file_get_contents($tmpDecrypted);
430
			// Handle OL empty body Outlook Signed & Encrypted mails.
431
			// The S/MIME plugin has to extract the body from the signed message.
432
			if (strpos($content, 'signed-data') !== false) {
433
				$this->message['type'] = 'encryptsigned';
434
				$olcert = tempnam(sys_get_temp_dir(), true);
435
				$olmsg = tempnam(sys_get_temp_dir(), true);
436
				openssl_pkcs7_verify($tmpDecrypted, PKCS7_NOVERIFY, $olcert);
437
				openssl_pkcs7_verify($tmpDecrypted, PKCS7_NOVERIFY, $olcert, [], $olcert, $olmsg);
438
				$content = file_get_contents($olmsg);
439
				unlink($olmsg);
440
				unlink($olcert);
441
			}
442
443
			$copyProps = mapi_getprops($data['message'], [PR_MESSAGE_DELIVERY_TIME, PR_SENDER_ENTRYID, PR_SENT_REPRESENTING_ENTRYID]);
444
			mapi_inetmapi_imtomapi($GLOBALS['mapisession']->getSession(), $data['store'], $GLOBALS['mapisession']->getAddressbook(), $data['message'], $content, ['parse_smime_signed' => true]);
445
			// Manually set time back to the received time, since mapi_inetmapi_imtomapi overwrites this
446
			mapi_setprops($data['message'], $copyProps);
447
448
			// remove temporary files
449
			unlink($tmpFile);
450
			unlink($tmpDecrypted);
451
452
			// mapi_inetmapi_imtomapi removes the PR_MESSAGE_CLASS = 'IPM.Note.SMIME.MultipartSigned'
453
			// So we need to check if the message was also signed by looking at the MIME_TAG in the eml
454
			if (strpos($content, 'multipart/signed') !== false || strpos($content, 'signed-data') !== false) {
455
				$this->message['type'] = 'encryptsigned';
456
				$this->verifyMessage($data['message'], $content);
457
			}
458
			elseif ($decryptStatus) {
459
				$this->message['info'] = SMIME_DECRYPT_SUCCESS;
460
				$this->message['success'] = SMIME_STATUS_SUCCESS;
461
			}
462
			elseif ($this->extract_openssl_error() === OPENSSL_RECIPIENT_CERTIFICATE_MISMATCH) {
463
				error_log("[smime] Error when decrypting email, openssl error: " . print_r($this->openssl_error, true));
0 ignored issues
show
Bug introduced by
Are you sure print_r($this->openssl_error, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

463
				error_log("[smime] Error when decrypting email, openssl error: " . /** @scrutinizer ignore-type */ print_r($this->openssl_error, true));
Loading history...
464
				Log::Write(LOGLEVEL_ERROR, sprintf("[smime] Error when decrypting email, openssl error: '%s'", $this->openssl_error));
0 ignored issues
show
Bug introduced by
It seems like $this->openssl_error can also be of type false; however, parameter $values of sprintf() does only seem to accept double|integer|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

464
				Log::Write(LOGLEVEL_ERROR, sprintf("[smime] Error when decrypting email, openssl error: '%s'", /** @scrutinizer ignore-type */ $this->openssl_error));
Loading history...
465
				$this->message['info'] = SMIME_DECRYPT_CERT_MISMATCH;
466
				$this->message['success'] = SMIME_STATUS_FAIL;
467
			}
468
		}
469
		else {
470
			// it might also be a signed message only. Verify it.
471
			$msg = tempnam(sys_get_temp_dir(), true);
472
			$ret = openssl_pkcs7_verify($tmpFile, PKCS7_NOVERIFY, null, [], null, $msg);
473
			$content = file_get_contents($msg);
474
			unlink($tmpFile);
475
			unlink($msg);
476
			if ($ret === true && !empty($content)) {
477
				$copyProps = mapi_getprops($data['message'], [PR_MESSAGE_DELIVERY_TIME, PR_SENDER_ENTRYID, PR_SENT_REPRESENTING_ENTRYID]);
478
				mapi_inetmapi_imtomapi(
479
					$GLOBALS['mapisession']->getSession(),
480
					$data['store'],
481
					$GLOBALS['mapisession']->getAddressbook(),
482
					$data['message'],
483
					$content,
484
					['parse_smime_signed' => true]
485
				);
486
				// Manually set time back to the received time, since mapi_inetmapi_imtomapi overwrites this
487
				mapi_setprops($data['message'], $copyProps);
488
				$this->message['type'] = 'encryptsigned';
489
				$this->message['info'] = SMIME_DECRYPT_SUCCESS;
490
				$this->message['success'] = SMIME_STATUS_SUCCESS;
491
			}
492
			else {
493
				$this->message['info'] = SMIME_UNLOCK_CERT;
494
			}
495
		}
496
497
		if (!encryptionStoreExpirationSupport()) {
498
			withPHPSession(function () use ($encryptionStore) {
499
				$encryptionStore->add('smime', '');
500
			});
501
		}
502
	}
503
504
	/**
505
	 * Function which calls verifyMessage to verify if the message isn't malformed during transport.
506
	 *
507
	 * @param mixed $data array of data from hook
508
	 */
509
	public function onSignedMessage($data) {
510
		$this->message['type'] = 'signed';
511
		$this->verifyMessage($data['message'], $data['data']);
512
	}
513
514
	/**
515
	 * General function which parses the openssl_pkcs7_verify return value and the errors generated by
516
	 * openssl_error_string().
517
	 *
518
	 * @param mixed $openssl_return
519
	 * @param mixed $openssl_errors
520
	 */
521
	public function validateSignedMessage($openssl_return, $openssl_errors) {
522
		if ($openssl_return === -1) {
523
			$this->message['info'] = SMIME_ERROR;
524
			$this->message['success'] = SMIME_STATUS_FAIL;
525
526
			return;
527
			// Verification was successful
528
		}
529
		if ($openssl_return) {
530
			$this->message['info'] = SMIME_SUCCESS;
531
			$this->message['success'] = SMIME_STATUS_SUCCESS;
532
533
			return;
534
			// Verification was not successful, display extra information.
535
		}
536
		$this->message['success'] = SMIME_STATUS_FAIL;
537
		if ($openssl_errors === OPENSSL_CA_VERIFY_FAIL) {
538
			$this->message['info'] = SMIME_CA;
539
		}
540
		else { // Catch general errors
541
			$this->message['info'] = SMIME_ERROR;
542
		}
543
	}
544
545
	/**
546
	 * Set smime key in $data array, which is send back to client
547
	 * Since we can't create this array key in the hooks:
548
	 * 'server.util.parse_smime.signed'
549
	 * 'server.util.parse_smime.encrypted'.
550
	 *
551
	 * TODO: investigate if we can move away from this hook
552
	 *
553
	 * @param mixed $data
554
	 */
555
	public function onAfterOpen($data) {
556
		if (isset($this->message) && !empty($this->message)) {
557
			$data['data']['item']['props']['smime'] = $this->message;
558
		}
559
	}
560
561
	/**
562
	 * Handles the uploaded certificate in the settingsmenu in grommunio Web
563
	 * - Opens the certificate with provided passphrase
564
	 * - Checks if it can be used for signing/decrypting
565
	 * - Verifies that the email address is equal to the
566
	 * - Verifies that the certificate isn't expired and inform user.
567
	 *
568
	 * @param mixed $data
569
	 */
570
	public function onUploadCertificate($data) {
571
		if ($data['sourcetype'] !== 'certificate') {
572
			return;
573
		}
574
		$passphrase = $_POST['passphrase'];
575
		$saveCert = false;
576
		$tmpname = $data['tmpname'];
577
		$message = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $message is dead and can be removed.
Loading history...
578
		$imported = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $imported is dead and can be removed.
Loading history...
579
580
		$certificate = file_get_contents($tmpname);
581
		$emailAddress = $GLOBALS['mapisession']->getSMTPAddress();
582
		list($message, $publickey, $publickeyData, $imported) = validateUploadedPKCS($certificate, $passphrase, $emailAddress);
583
584
		// All checks completed successful
585
		// Store private cert in users associated store (check for duplicates)
586
		if ($imported) {
587
			$certMessage = getMAPICert($this->getStore());
0 ignored issues
show
Bug introduced by
$this->getStore() of type object is incompatible with the type resource expected by parameter $store of getMAPICert(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

587
			$certMessage = getMAPICert(/** @scrutinizer ignore-type */ $this->getStore());
Loading history...
588
			// TODO: update to serialNumber check
589
			if ($certMessage && $certMessage[0][PR_MESSAGE_DELIVERY_TIME] == $publickeyData['validTo_time_t']) {
590
				$message = _('Certificate is already stored on the server');
591
			}
592
			else {
593
				$saveCert = true;
594
				$root = mapi_msgstore_openentry($this->getStore(), null);
0 ignored issues
show
Unused Code introduced by
The assignment to $root is dead and can be removed.
Loading history...
595
				// Remove old certificate
596
				/*
597
				if($certMessage) {
598
					// Delete private key
599
					mapi_folder_deletemessages($root, array($certMessage[PR_ENTRYID]));
600
601
					// Delete public key
602
					$pubCert = getMAPICert($this->getStore, 'WebApp.Security.Public', getCertEmail($certMessage));
603
					if($pubCert) {
604
						mapi_folder_deletemessages($root, array($pubCert[PR_ENTRYID]));
605
					}
606
					$message = _('New certificate uploaded');
607
				} else {
608
					$message = _('Certificate uploaded');
609
				}*/
610
611
				$this->importCertificate($certificate, $publickeyData, 'private');
612
613
				// Check if the user has a public key in the GAB.
614
				$store_props = mapi_getprops($this->getStore(), [PR_USER_ENTRYID]);
615
				$user = mapi_ab_openentry($GLOBALS['mapisession']->getAddressbook(), $store_props[PR_USER_ENTRYID]);
0 ignored issues
show
Unused Code introduced by
The assignment to $user is dead and can be removed.
Loading history...
616
617
				$this->importCertificate($publickey, $publickeyData, 'public', true);
618
			}
619
		}
620
621
		$returnfiles = [];
622
		$returnfiles[] = [
623
			'props' => [
624
				'attach_num' => -1,
625
				'size' => $data['size'],
626
				'name' => $data['name'],
627
				'cert' => $saveCert,
628
				'cert_warning' => $message,
629
			],
630
		];
631
		$data['returnfiles'] = $returnfiles;
632
	}
633
634
	/**
635
	 * This function handles the 'beforesend' hook which is triggered before sending the email.
636
	 * If the PR_MESSAGE_CLASS is set to a signed email (IPM.Note.SMIME.Multipartsigned), this function
637
	 * will convert the mapi message to RFC822, sign the eml and attach the signed email to the mapi message.
638
	 *
639
	 * @param mixed $data from php hook
640
	 */
641
	public function onBeforeSend(&$data) {
642
		$store = $data['store'];
0 ignored issues
show
Unused Code introduced by
The assignment to $store is dead and can be removed.
Loading history...
643
		$message = $data['message'];
644
645
		// Retrieve message class
646
		$props = mapi_getprops($message, [PR_MESSAGE_CLASS]);
647
		$messageClass = $props[PR_MESSAGE_CLASS];
648
649
		if (!isset($messageClass)) {
650
			return;
651
		}
652
		if (stripos($messageClass, 'IPM.Note.deferSMIME') === false &&
653
			stripos($messageClass, 'IPM.Note.SMIME') === false) {
654
			return;
655
		}
656
657
		// FIXME: for now return when we are going to sign but we don't have the passphrase set
658
		// This should never happen sign
659
		$encryptionStore = EncryptionStore::getInstance();
660
		if (($messageClass === 'IPM.Note.deferSMIME.SignedEncrypt' ||
661
			$messageClass === 'IPM.Note.deferSMIME.MultipartSigned' ||
662
			$messageClass === 'IPM.Note.SMIME.SignedEncrypt' ||
663
			$messageClass === 'IPM.Note.SMIME.MultipartSigned') &&
664
			!$encryptionStore->get('smime')) {
665
			return;
666
		}
667
		// NOTE: setting message class to IPM.Note, so that mapi_inetmapi_imtoinet converts the message to plain email
668
		// and doesn't fail when handling the attachments.
669
		mapi_setprops($message, [PR_MESSAGE_CLASS => 'IPM.Note']);
670
		mapi_savechanges($message);
671
672
		// Read the message as RFC822-formatted e-mail stream.
673
		$emlMessageStream = mapi_inetmapi_imtoinet($GLOBALS['mapisession']->getSession(), $GLOBALS['mapisession']->getAddressbook(), $message, []);
674
675
		// Remove all attachments, since they are stored in the attached signed message
676
		$atable = mapi_message_getattachmenttable($message);
677
		$rows = mapi_table_queryallrows($atable, [PR_ATTACH_MIME_TAG, PR_ATTACH_NUM]);
678
		foreach ($rows as $row) {
679
			$attnum = $row[PR_ATTACH_NUM];
680
			mapi_message_deleteattach($message, $attnum);
681
		}
682
683
		// create temporary files
684
		$tmpSendEmail = tempnam(sys_get_temp_dir(), true);
0 ignored issues
show
Bug introduced by
true of type true is incompatible with the type string expected by parameter $prefix of tempnam(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

684
		$tmpSendEmail = tempnam(sys_get_temp_dir(), /** @scrutinizer ignore-type */ true);
Loading history...
685
		$tmpSendSmimeEmail = tempnam(sys_get_temp_dir(), true);
686
687
		// Save message stream to a file
688
		$stat = mapi_stream_stat($emlMessageStream);
689
690
		$fhandle = fopen($tmpSendEmail, 'w');
691
		$buffer = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $buffer is dead and can be removed.
Loading history...
692
		for ($i = 0; $i < $stat["cb"]; $i += BLOCK_SIZE) {
693
			// Write stream
694
			$buffer = mapi_stream_read($emlMessageStream, BLOCK_SIZE);
695
			fwrite($fhandle, $buffer, strlen($buffer));
696
		}
697
		fclose($fhandle);
698
699
		// Create attachment for S/MIME message
700
		$signedAttach = mapi_message_createattach($message);
701
		$smimeProps = [
702
			PR_ATTACH_LONG_FILENAME => 'smime.p7m',
703
			PR_DISPLAY_NAME => 'smime.p7m',
704
			PR_ATTACH_METHOD => ATTACH_BY_VALUE,
705
			PR_ATTACH_MIME_TAG => 'multipart/signed',
706
			PR_ATTACHMENT_HIDDEN => true,
707
		];
708
709
		// Sign then Encrypt email
710
		switch ($messageClass) {
711
			case 'IPM.Note.deferSMIME.SignedEncrypt':
712
			case 'IPM.Note.SMIME.SignedEncrypt':
713
				$tmpFile = tempnam(sys_get_temp_dir(), true);
714
				$this->sign($tmpSendEmail, $tmpFile, $message, $signedAttach, $smimeProps);
715
				$this->encrypt($tmpFile, $tmpSendSmimeEmail, $message, $signedAttach, $smimeProps);
716
				unlink($tmpFile);
717
				break;
718
719
			case 'IPM.Note.deferSMIME.MultipartSigned':
720
			case 'IPM.Note.SMIME.MultipartSigned':
721
				$this->sign($tmpSendEmail, $tmpSendSmimeEmail, $message, $signedAttach, $smimeProps);
722
				break;
723
724
			case 'IPM.Note.deferSMIME':
725
			case 'IPM.Note.SMIME':
726
				$this->encrypt($tmpSendEmail, $tmpSendSmimeEmail, $message, $signedAttach, $smimeProps);
727
				break;
728
		}
729
730
		// Save the signed message as attachment of the send email
731
		$stream = mapi_openproperty($signedAttach, PR_ATTACH_DATA_BIN, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY);
732
		$handle = fopen($tmpSendSmimeEmail, 'r');
733
		while (!feof($handle)) {
734
			$contents = fread($handle, BLOCK_SIZE);
735
			mapi_stream_write($stream, $contents);
736
		}
737
		fclose($handle);
738
739
		mapi_stream_commit($stream);
740
741
		// remove tmp files
742
		unlink($tmpSendSmimeEmail);
743
		unlink($tmpSendEmail);
744
745
		mapi_savechanges($signedAttach);
746
		mapi_savechanges($message);
747
	}
748
749
	/**
750
	 * Function to sign an email.
751
	 *
752
	 * @param string $infile       File eml to be encrypted
753
	 * @param string $outfile      File
754
	 * @param object $message      Mapi Message Object
755
	 * @param object $signedAttach
756
	 * @param array  $smimeProps
757
	 */
758
	public function sign(&$infile, &$outfile, &$message, &$signedAttach, $smimeProps) {
759
		// Set mesageclass back to IPM.Note.SMIME.MultipartSigned
760
		mapi_setprops($message, [PR_MESSAGE_CLASS => 'IPM.Note.SMIME.MultipartSigned']);
761
		mapi_setprops($signedAttach, $smimeProps);
762
763
		// Obtain private certificate
764
		$encryptionStore = EncryptionStore::getInstance();
765
		// Only the newest one is returned
766
		$certs = readPrivateCert($this->getStore(), $encryptionStore->get('smime'));
0 ignored issues
show
Bug introduced by
$this->getStore() of type object is incompatible with the type resource expected by parameter $store of readPrivateCert(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

766
		$certs = readPrivateCert(/** @scrutinizer ignore-type */ $this->getStore(), $encryptionStore->get('smime'));
Loading history...
767
768
		// Retrieve intermediate CA's for verification, if available
769
		if (isset($certs['extracerts'])) {
770
			$tmpFile = tempnam(sys_get_temp_dir(), true);
0 ignored issues
show
Bug introduced by
true of type true is incompatible with the type string expected by parameter $prefix of tempnam(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

770
			$tmpFile = tempnam(sys_get_temp_dir(), /** @scrutinizer ignore-type */ true);
Loading history...
771
			file_put_contents($tmpFile, implode('', $certs['extracerts']));
772
			$ok = openssl_pkcs7_sign($infile, $outfile, $certs['cert'], [$certs['pkey'], ''], [], PKCS7_DETACHED, $tmpFile);
773
			if (!$ok) {
774
				Log::Write(LOGLEVEL_ERROR, sprintf("[smime] Unable to sign message with intermediate certificates, openssl error: '%s'", @openssl_error_string()));
0 ignored issues
show
Bug introduced by
It seems like @openssl_error_string() can also be of type false; however, parameter $values of sprintf() does only seem to accept double|integer|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

774
				Log::Write(LOGLEVEL_ERROR, sprintf("[smime] Unable to sign message with intermediate certificates, openssl error: '%s'", /** @scrutinizer ignore-type */ @openssl_error_string()));
Loading history...
775
			}
776
			unlink($tmpFile);
777
		}
778
		else {
779
			$ok = openssl_pkcs7_sign($infile, $outfile, $certs['cert'], [$certs['pkey'], ''], [], PKCS7_DETACHED);
780
			if (!$ok) {
781
				Log::Write(LOGLEVEL_ERROR, sprintf("[smime] Unable to sign message, openssl error: '%s'", @openssl_error_string()));
782
			}
783
		}
784
	}
785
786
	/**
787
	 * Function to encrypt an email.
788
	 *
789
	 * @param string $infile       File eml to be encrypted
790
	 * @param string $outfile      File
791
	 * @param object $message      Mapi Message Object
792
	 * @param object $signedAttach
793
	 * @param array  $smimeProps
794
	 */
795
	public function encrypt(&$infile, &$outfile, &$message, &$signedAttach, $smimeProps) {
796
		mapi_setprops($message, [PR_MESSAGE_CLASS => 'IPM.Note.SMIME']);
797
		$smimeProps[PR_ATTACH_MIME_TAG] = "application/pkcs7-mime";
798
		mapi_setprops($signedAttach, $smimeProps);
799
800
		$publicCerts = $this->getPublicKeyForMessage($message);
801
		// Always append our own certificate, so that the mail can be decrypted in 'Sent items'
802
		// Prefer GAB public certificate above MAPI Store certificate.
803
		$email = $GLOBALS['mapisession']->getSMTPAddress();
804
		$user = $this->getGABUser($email);
805
		$cert = $this->getGABCert($user);
806
		if (empty($cert)) {
807
			$cert = base64_decode($this->getPublicKey($email));
808
		}
809
810
		if (!empty($cert)) {
811
			array_push($publicCerts, $cert);
812
		}
813
814
		$ok = openssl_pkcs7_encrypt($infile, $outfile, $publicCerts, [], 0, $this->cipher);
815
		if (!$ok) {
816
			error_log("[smime] unable to encrypt message, openssl error: " . print_r(@openssl_error_string(), true));
0 ignored issues
show
Bug introduced by
Are you sure print_r(@openssl_error_string(), true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

816
			error_log("[smime] unable to encrypt message, openssl error: " . /** @scrutinizer ignore-type */ print_r(@openssl_error_string(), true));
Loading history...
817
			Log::Write(LOGLEVEL_ERROR, sprintf("[smime] unable to encrypt message, openssl error: '%s'", @openssl_error_string()));
0 ignored issues
show
Bug introduced by
It seems like @openssl_error_string() can also be of type false; however, parameter $values of sprintf() does only seem to accept double|integer|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

817
			Log::Write(LOGLEVEL_ERROR, sprintf("[smime] unable to encrypt message, openssl error: '%s'", /** @scrutinizer ignore-type */ @openssl_error_string()));
Loading history...
818
		}
819
		$tmpEml = file_get_contents($outfile);
820
821
		// Grab the base64 data, since MAPI requires it saved as decoded base64 string.
822
		// FIXME: we can do better here
823
		$matches = explode("\n\n", $tmpEml);
824
		$base64 = str_replace("\n", "", $matches[1]);
825
		file_put_contents($outfile, base64_decode($base64));
826
827
		// Empty the body
828
		mapi_setprops($message, [PR_BODY => ""]);
829
	}
830
831
	/**
832
	 * Function which fetches the public certificates for all recipients (TO/CC/BCC) of a message
833
	 * Always get the certificate of an address which expires last.
834
	 *
835
	 * @param object $message Mapi Message Object
836
	 *
837
	 * @return array of public certificates
838
	 */
839
	public function getPublicKeyForMessage($message) {
840
		$recipientTable = mapi_message_getrecipienttable($message);
841
		$recips = mapi_table_queryallrows($recipientTable, [PR_SMTP_ADDRESS, PR_RECIPIENT_TYPE, PR_ADDRTYPE], [RES_OR, [
842
			[RES_PROPERTY,
843
				[
844
					RELOP => RELOP_EQ,
845
					ULPROPTAG => PR_RECIPIENT_TYPE,
846
					VALUE => MAPI_BCC,
847
				],
848
			],
849
			[RES_PROPERTY,
850
				[
851
					RELOP => RELOP_EQ,
852
					ULPROPTAG => PR_RECIPIENT_TYPE,
853
					VALUE => MAPI_CC,
854
				],
855
			],
856
			[RES_PROPERTY,
857
				[
858
					RELOP => RELOP_EQ,
859
					ULPROPTAG => PR_RECIPIENT_TYPE,
860
					VALUE => MAPI_TO,
861
				],
862
			],
863
		]]);
864
865
		$publicCerts = [];
866
		$storeCert = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $storeCert is dead and can be removed.
Loading history...
867
		$gabCert = '';
868
869
		foreach ($recips as $recip) {
870
			$emailAddr = $recip[PR_SMTP_ADDRESS];
871
			$addrType = $recip[PR_ADDRTYPE];
872
873
			if ($addrType === "ZARAFA" || $addrType === "EX") {
874
				$user = $this->getGABUser($emailAddr);
875
				$gabCert = $this->getGABCert($user);
876
			}
877
878
			$storeCert = $this->getPublicKey($emailAddr);
879
880
			if (!empty($gabCert)) {
881
				array_push($publicCerts, $gabCert);
882
			}
883
			elseif (!empty($storeCert)) {
884
				array_push($publicCerts, base64_decode($storeCert));
885
			}
886
		}
887
888
		return $publicCerts;
889
	}
890
891
	/**
892
	 * Retrieves the public certificates stored in the MAPI UserStore and belonging to the
893
	 * emailAdddress, returns "" if there is no certificate for that user.
894
	 *
895
	 * @param string emailAddress
0 ignored issues
show
Bug introduced by
The type emailAddress was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
896
	 * @param mixed $emailAddress
897
	 * @param mixed $multiple
898
	 *
899
	 * @return string $certificate
900
	 */
901
	public function getPublicKey($emailAddress, $multiple = false) {
902
		$certificates = [];
903
904
		$certs = getMAPICert($this->getStore(), 'WebApp.Security.Public', $emailAddress);
0 ignored issues
show
Bug introduced by
$this->getStore() of type object is incompatible with the type resource expected by parameter $store of getMAPICert(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

904
		$certs = getMAPICert(/** @scrutinizer ignore-type */ $this->getStore(), 'WebApp.Security.Public', $emailAddress);
Loading history...
905
906
		if ($certs && count($certs) > 0) {
0 ignored issues
show
Bug introduced by
$certs of type resource|true is incompatible with the type Countable|array expected by parameter $value of count(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

906
		if ($certs && count(/** @scrutinizer ignore-type */ $certs) > 0) {
Loading history...
907
			foreach ($certs as $cert) {
0 ignored issues
show
Bug introduced by
The expression $certs of type resource|true is not traversable.
Loading history...
908
				$pubkey = mapi_msgstore_openentry($this->getStore(), $cert[PR_ENTRYID]);
909
				$certificate = "";
910
				if ($pubkey == false) {
911
					continue;
912
				}
913
				// retrieve pkcs#11 certificate from body
914
				$stream = mapi_openproperty($pubkey, PR_BODY, IID_IStream, 0, 0);
915
				$stat = mapi_stream_stat($stream);
916
				mapi_stream_seek($stream, 0, STREAM_SEEK_SET);
917
				for ($i = 0; $i < $stat['cb']; $i += 1024) {
918
					$certificate .= mapi_stream_read($stream, 1024);
919
				}
920
				array_push($certificates, $certificate);
921
			}
922
		}
923
924
		return $multiple ? $certificates : ($certificates[0] ?? '');
925
	}
926
927
	/**
928
	 * Function which is used to check if there is a public certificate for the provided emailAddress.
929
	 *
930
	 * @param string emailAddress emailAddres of recipient
931
	 * @param bool gabUser is the user of PR_ADDRTYPE == ZARAFA
0 ignored issues
show
Bug introduced by
The type gabUser was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
932
	 * @param mixed $emailAddress
933
	 * @param mixed $gabUser
934
	 *
935
	 * @return bool true if public certificate exists
936
	 */
937
	public function pubcertExists($emailAddress, $gabUser = false) {
938
		if ($gabUser) {
939
			$user = $this->getGABUser($emailAddress);
940
			$gabCert = $this->getGABCert($user);
941
			if ($user && !empty($gabCert)) {
942
				return true;
943
			}
944
		}
945
946
		$root = mapi_msgstore_openentry($this->getStore(), null);
947
		$table = mapi_folder_getcontentstable($root, MAPI_ASSOCIATED);
948
949
		// Restriction for public certificates which are from the recipient of the email, are active and have the correct message_class
950
		$restrict = [RES_AND, [
951
			[RES_PROPERTY,
952
				[
953
					RELOP => RELOP_EQ,
954
					ULPROPTAG => PR_MESSAGE_CLASS,
955
					VALUE => [PR_MESSAGE_CLASS => "WebApp.Security.Public"],
956
				],
957
			],
958
			[RES_PROPERTY,
959
				[
960
					RELOP => RELOP_EQ,
961
					ULPROPTAG => PR_SUBJECT,
962
					VALUE => [PR_SUBJECT => $emailAddress],
963
				],
964
			],
965
		]];
966
		mapi_table_restrict($table, $restrict, TBL_BATCH);
967
		mapi_table_sort($table, [PR_MESSAGE_DELIVERY_TIME => TABLE_SORT_DESCEND], TBL_BATCH);
968
969
		$rows = mapi_table_queryallrows($table, [PR_SUBJECT, PR_ENTRYID, PR_MESSAGE_DELIVERY_TIME, PR_CLIENT_SUBMIT_TIME], $restrict);
970
971
		return !empty($rows);
972
	}
973
974
	/**
975
	 * Helper functions which extracts the errors from openssl_error_string()
976
	 * Example error from openssl_error_string(): error:21075075:PKCS7 routines:PKCS7_verify:certificate verify error
977
	 * Note that openssl_error_string() returns an error when verifying is successful, this is a bug in PHP https://bugs.php.net/bug.php?id=50713.
978
	 *
979
	 * @return string
980
	 */
981
	public function extract_openssl_error() {
982
		// TODO: should catch more errors by using while($error = @openssl_error_string())
983
		$this->openssl_error = @openssl_error_string();
984
		$openssl_error_code = 0;
985
		if ($this->openssl_error) {
986
			$openssl_error_list = explode(":", $this->openssl_error);
987
			$openssl_error_code = $openssl_error_list[1];
988
		}
989
990
		return $openssl_error_code;
991
	}
992
993
	/**
994
	 * Extract the intermediate certificates from the signed email.
995
	 * Uses openssl_pkcs7_verify to extract the PKCS#7 blob and then converts the PKCS#7 blob to
996
	 * X509 certificates using openssl_pkcs7_read.
997
	 *
998
	 * @param string $emlfile - the s/mime message
999
	 *
1000
	 * @return array a list of extracted intermediate certificates
1001
	 */
1002
	public function extractCAs($emlfile) {
1003
		$cas = [];
1004
		$certfile = tempnam(sys_get_temp_dir(), true);
0 ignored issues
show
Bug introduced by
true of type true is incompatible with the type string expected by parameter $prefix of tempnam(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1004
		$certfile = tempnam(sys_get_temp_dir(), /** @scrutinizer ignore-type */ true);
Loading history...
1005
		$outfile = tempnam(sys_get_temp_dir(), true);
1006
		$p7bfile = tempnam(sys_get_temp_dir(), true);
1007
		openssl_pkcs7_verify($emlfile, PKCS7_NOVERIFY, $certfile);
1008
		openssl_pkcs7_verify($emlfile, PKCS7_NOVERIFY, $certfile, [], $certfile, $outfile, $p7bfile);
1009
1010
		$p7b = file_get_contents($p7bfile);
1011
1012
		openssl_pkcs7_read($p7b, $cas);
1013
		unlink($certfile);
1014
		unlink($outfile);
1015
		unlink($p7bfile);
1016
1017
		return $cas;
1018
	}
1019
1020
	/**
1021
	 * Imports certificate in the MAPI Root Associated Folder.
1022
	 *
1023
	 * Private key, always insert certificate
1024
	 * Public key, check if we already have one stored
1025
	 *
1026
	 * @param string $cert     certificate body as a string
1027
	 * @param mixed  $certData an array with the parsed certificate data
1028
	 * @param string $type     certificate type, default 'public'
1029
	 * @param bool   $force    force import the certificate even though we have one already stored in the MAPI Store.
1030
	 *                         FIXME: remove $force in the future and move the check for newer certificate in this function.
1031
	 */
1032
	public function importCertificate($cert, $certData, $type = 'public', $force = false) {
1033
		$certEmail = getCertEmail($certData);
1034
		if ($this->pubcertExists($certEmail) && !$force && $type !== 'private') {
1035
			return;
1036
		}
1037
		$issued_by = "";
1038
		foreach (array_keys($certData['issuer']) as $key) {
1039
			$issued_by .= $key . '=' . $certData['issuer'][$key] . "\n";
1040
		}
1041
1042
		$root = mapi_msgstore_openentry($this->getStore(), null);
1043
		$assocMessage = mapi_folder_createmessage($root, MAPI_ASSOCIATED);
1044
		// TODO: write these properties down.
1045
		mapi_setprops($assocMessage, [
1046
			PR_SUBJECT => $certEmail,
1047
			PR_MESSAGE_CLASS => $type == 'public' ? 'WebApp.Security.Public' : 'WebApp.Security.Private',
1048
			PR_MESSAGE_DELIVERY_TIME => $certData['validTo_time_t'],
1049
			PR_CLIENT_SUBMIT_TIME => $certData['validFrom_time_t'],
1050
			PR_SENDER_NAME => $certData['serialNumber'], // serial
1051
			PR_SENDER_EMAIL_ADDRESS => $issued_by, // Issuer To
1052
			PR_SUBJECT_PREFIX => '',
1053
			PR_RECEIVED_BY_NAME => $this->fingerprint_cert($cert, 'sha1'), // SHA1 Fingerprint
1054
			PR_INTERNET_MESSAGE_ID => $this->fingerprint_cert($cert), // MD5 FingerPrint
1055
		]);
1056
		// Save attachment
1057
		$msgBody = base64_encode($cert);
1058
		$stream = mapi_openproperty($assocMessage, PR_BODY, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY);
1059
		mapi_stream_setsize($stream, strlen($msgBody));
1060
		mapi_stream_write($stream, $msgBody);
1061
		mapi_stream_commit($stream);
1062
		mapi_message_savechanges($assocMessage);
1063
	}
1064
1065
	/**
1066
	 * Function which returns the fingerprint (hash) of the certificate.
1067
	 *
1068
	 * @param string $hash optional hash algorithm
1069
	 * @param mixed  $body
1070
	 */
1071
	public function fingerprint_cert($body, $hash = 'md5') {
1072
		// TODO: Note for PHP > 5.6 we can use openssl_x509_fingerprint
1073
		$body = str_replace('-----BEGIN CERTIFICATE-----', '', $body);
1074
		$body = str_replace('-----END CERTIFICATE-----', '', $body);
1075
		$body = base64_decode($body);
1076
1077
		if ($hash === 'sha1') {
1078
			$fingerprint = sha1($body);
1079
		}
1080
		else {
1081
			$fingerprint = md5($body);
1082
		}
1083
1084
		// Format 1000AB as 10:00:AB
1085
		return strtoupper(implode(':', str_split($fingerprint, 2)));
0 ignored issues
show
Bug introduced by
It seems like str_split($fingerprint, 2) can also be of type true; however, parameter $pieces of implode() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1085
		return strtoupper(implode(':', /** @scrutinizer ignore-type */ str_split($fingerprint, 2)));
Loading history...
1086
	}
1087
1088
	/**
1089
	 * Retrieve the GAB User.
1090
	 *
1091
	 * FIXME: ideally this would be a public function in grommunio Web.
1092
	 *
1093
	 * @param string $email the email address of the user
1094
	 *
1095
	 * @return mixed $user boolean if false else MAPIObject
1096
	 */
1097
	public function getGABUser($email) {
1098
		$addrbook = $GLOBALS["mapisession"]->getAddressbook();
1099
		$userArr = [[PR_DISPLAY_NAME => $email]];
1100
		$user = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $user is dead and can be removed.
Loading history...
1101
1102
		try {
1103
			$user = mapi_ab_resolvename($addrbook, $userArr, EMS_AB_ADDRESS_LOOKUP);
1104
			$user = mapi_ab_openentry($addrbook, $user[0][PR_ENTRYID]);
1105
		}
1106
		catch (MAPIException $e) {
1107
			$e->setHandled();
1108
		}
1109
1110
		return $user;
1111
	}
1112
1113
	/**
1114
	 * Retrieve the PR_EMS_AB_X509_CERT.
1115
	 *
1116
	 * @param MAPIObject $user the GAB user
0 ignored issues
show
Bug introduced by
The type MAPIObject was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
1117
	 *
1118
	 * @return string $cert the certificate, empty if not found
1119
	 */
1120
	public function getGABCert($user) {
1121
		$cert = '';
1122
		$userCertArray = mapi_getprops($user, [PR_EMS_AB_X509_CERT]);
1123
		if (isset($userCertArray[PR_EMS_AB_X509_CERT])) {
1124
			$cert = der2pem($userCertArray[PR_EMS_AB_X509_CERT][0]);
1125
		}
1126
1127
		return $cert;
1128
	}
1129
1130
	/**
1131
	 * Called when the core Settings class is initialized and ready to accept sysadmin default
1132
	 * settings. Registers the sysadmin defaults for the example plugin.
1133
	 *
1134
	 * @param mixed $data Reference to the data of the triggered hook
1135
	 */
1136
	public function onBeforeSettingsInit(&$data) {
1137
		$data['settingsObj']->addSysAdminDefaults([
1138
			'zarafa' => [
1139
				'v1' => [
1140
					'plugins' => [
1141
						'smime' => [
1142
							'enable' => defined('PLUGIN_SMIME_USER_DEFAULT_ENABLE_SMIME') && PLUGIN_SMIME_USER_DEFAULT_ENABLE_SMIME,
1143
							'passphrase_cache' => defined('PLUGIN_SMIME_PASSPHRASE_REMEMBER_BROWSER') && PLUGIN_SMIME_PASSPHRASE_REMEMBER_BROWSER,
1144
						],
1145
					],
1146
				],
1147
			],
1148
		]);
1149
	}
1150
1151
	/**
1152
	 * Get sender structure of the MAPI Message.
1153
	 *
1154
	 * @param mapimessage $mapiMessage MAPI Message resource from which we need to get the sender
0 ignored issues
show
Bug introduced by
The type mapimessage was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
1155
	 *
1156
	 * @return array with properties
1157
	 */
1158
	public function getSenderAddress($mapiMessage) {
1159
		if (method_exists($GLOBALS['operations'], 'getSenderAddress')) {
1160
			return $GLOBALS["operations"]->getSenderAddress($mapiMessage);
1161
		}
1162
1163
		$messageProps = mapi_getprops($mapiMessage, [PR_SENT_REPRESENTING_ENTRYID, PR_SENDER_ENTRYID]);
1164
		$senderEntryID = isset($messageProps[PR_SENT_REPRESENTING_ENTRYID]) ? $messageProps[PR_SENT_REPRESENTING_ENTRYID] : $messageProps[PR_SENDER_ENTRYID];
1165
1166
		try {
1167
			$senderUser = mapi_ab_openentry($GLOBALS["mapisession"]->getAddressbook(), $senderEntryID);
1168
			if ($senderUser) {
1169
				$userprops = mapi_getprops($senderUser, [PR_ADDRTYPE, PR_DISPLAY_NAME, PR_EMAIL_ADDRESS, PR_SMTP_ADDRESS, PR_OBJECT_TYPE, PR_RECIPIENT_TYPE, PR_DISPLAY_TYPE, PR_DISPLAY_TYPE_EX, PR_ENTRYID]);
1170
1171
				$senderStructure = [];
1172
				$senderStructure["props"]['entryid'] = isset($userprops[PR_ENTRYID]) ? bin2hex($userprops[PR_ENTRYID]) : '';
1173
				$senderStructure["props"]['display_name'] = $userprops[PR_DISPLAY_NAME] ?? '';
1174
				$senderStructure["props"]['email_address'] = $userprops[PR_EMAIL_ADDRESS] ?? '';
1175
				$senderStructure["props"]['smtp_address'] = $userprops[PR_SMTP_ADDRESS] ?? '';
1176
				$senderStructure["props"]['address_type'] = $userprops[PR_ADDRTYPE] ?? '';
1177
				$senderStructure["props"]['object_type'] = $userprops[PR_OBJECT_TYPE];
1178
				$senderStructure["props"]['recipient_type'] = MAPI_TO;
1179
				$senderStructure["props"]['display_type'] = $userprops[PR_DISPLAY_TYPE] ?? MAPI_MAILUSER;
1180
				$senderStructure["props"]['display_type_ex'] = $userprops[PR_DISPLAY_TYPE_EX] ?? MAPI_MAILUSER;
1181
			}
1182
		}
1183
		catch (MAPIException $e) {
1184
			error_log(sprintf("[smime] getSenderAddress(): Exception %s", $e));
1185
		}
1186
1187
		return $senderStructure;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $senderStructure does not seem to be defined for all execution paths leading up to this point.
Loading history...
1188
	}
1189
}
1190