Issues (752)

plugins/smime/php/plugin.smime.php (34 issues)

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_AES_256_CBC;
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
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
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
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) {
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
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(':', (string) $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((string) $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
				$this->clear_openssl_error();
307
				$signed_ok = openssl_pkcs7_verify($tmpfname, PKCS7_NOINTERN, $outcert, explode(';', PLUGIN_SMIME_CACERTS), $tmpUserCert);
308
				$openssl_error_code = $this->extract_openssl_error();
309
				$this->validateSignedMessage($signed_ok, $openssl_error_code);
310
				// Check if we need to import a newer certificate
311
				$importCert = file_get_contents($outcert);
312
				$parsedImportCert = openssl_x509_parse($importCert);
313
				$parsedUserCert = openssl_x509_parse($userCert);
314
				if (!$signed_ok || $openssl_error_code === OPENSSL_CA_VERIFY_FAIL) {
315
					continue;
316
				}
317
318
				// CA Checks out
319
				$caCerts = $this->extractCAs($tmpfname);
320
				// If validTo and validFrom are more in the future, emailAddress matches and OCSP check is valid, import newer certificate
321
				if (is_array($parsedImportCert) && is_array($parsedUserCert) &&
322
					$parsedImportCert['validTo'] > $parsedUserCert['validTo'] &&
323
					$parsedImportCert['validFrom'] > $parsedUserCert['validFrom'] &&
324
					getCertEmail($parsedImportCert) === getCertEmail($parsedUserCert) &&
325
					verifyOCSP($importCert, $caCerts, $this->message) &&
326
					$importMessageCert !== false) {
327
					// Redundant
328
					$importMessageCert = true;
329
				}
330
				else {
331
					$importMessageCert = false;
332
					verifyOCSP($userCert, $caCerts, $this->message);
333
					break;
334
				}
335
			}
336
		}
337
		else {
338
			// Works. Just leave it.
339
			$this->clear_openssl_error();
340
			$signed_ok = openssl_pkcs7_verify($tmpfname, PKCS7_NOSIGS, $outcert, explode(';', PLUGIN_SMIME_CACERTS));
341
			$openssl_error_code = $this->extract_openssl_error();
342
			$this->validateSignedMessage($signed_ok, $openssl_error_code);
343
344
			// OCSP check
345
			if ($signed_ok && $openssl_error_code !== OPENSSL_CA_VERIFY_FAIL) { // CA Checks out
346
				$userCert = file_get_contents($outcert);
347
				$parsedImportCert = openssl_x509_parse($userCert);
348
349
				$caCerts = $this->extractCAs($tmpfname);
350
				if (!is_array($parsedImportCert) || !verifyOCSP($userCert, $caCerts, $this->message)) {
0 ignored issues
show
The condition is_array($parsedImportCert) is always true.
Loading history...
351
					$importMessageCert = false;
352
				}
353
			// We don't have a certificate from the MAPI UserStore or LDAP, so we will set $userCert to $importCert
354
			// so that we can verify the message according to the be imported certificate.
355
			}
356
			else { // No pubkey
357
				$importMessageCert = false;
358
				Log::write(LOGLEVEL_INFO, sprintf("[smime] Unable to verify message without public key, openssl error: '%s'", $this->openssl_error));
359
				$this->message['success'] = SMIME_STATUS_FAIL;
360
				$this->message['info'] = SMIME_CA;
361
			}
362
		}
363
		// Certificate is newer or not yet imported to the user store and not revoked
364
		// If certificate is from the GAB, then don't import it.
365
		if ($importMessageCert && !$fromGAB) {
366
			$this->clear_openssl_error();
367
			$signed_ok = openssl_pkcs7_verify($tmpfname, PKCS7_NOSIGS, $outcert, explode(';', PLUGIN_SMIME_CACERTS));
368
			$openssl_error_code = $this->extract_openssl_error();
369
			$this->validateSignedMessage($signed_ok, $openssl_error_code);
370
			$userCert = file_get_contents($outcert);
371
			$parsedImportCert = openssl_x509_parse($userCert);
372
			// FIXME: doing this in importPublicKey too...
373
			$certEmail = getCertEmail($parsedImportCert);
374
			if (!empty($certEmail)) {
375
				$this->importCertificate($userCert, $parsedImportCert, 'public', true);
376
			}
377
		}
378
379
		// Remove extracted certificate from openssl_pkcs7_verify
380
		unlink($outcert);
381
382
		// remove the temporary file
383
		unlink($tmpfname);
384
385
		// Clean up temp cert
386
		unlink($tmpUserCert);
387
	}
388
389
	public function join_xph(&$prop, $msg) {
390
		$a = mapi_getprops($msg, [PR_TRANSPORT_MESSAGE_HEADERS]);
391
		$a = $a === false ? "" : ($a[PR_TRANSPORT_MESSAGE_HEADERS] ?? "");
392
		$prop[PR_TRANSPORT_MESSAGE_HEADERS] =
393
			"# Outer headers:\n" . ($prop[PR_TRANSPORT_MESSAGE_HEADERS] ?? "") .
394
			"# Inner headers:\n" . $a;
395
	}
396
397
	/**
398
	 * Function which decrypts an encrypted message.
399
	 * The key should be unlocked and stored in the EncryptionStore for a successful decrypt
400
	 * If the key isn't in the session, we give the user a message to unlock his certificate.
401
	 *
402
	 * @param mixed $data array of data from hook
403
	 */
404
	public function onEncrypted($data) {
405
		// Cert unlocked, decode message
406
		$this->message['success'] = SMIME_STATUS_INFO;
407
		$this->message['info'] = SMIME_DECRYPT_FAILURE;
408
409
		$this->message['type'] = 'encrypted';
410
		$encryptionStore = EncryptionStore::getInstance();
411
		$pass = $encryptionStore->get('smime');
412
413
		$tmpFile = tempnam(sys_get_temp_dir(), true);
0 ignored issues
show
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

413
		$tmpFile = tempnam(sys_get_temp_dir(), /** @scrutinizer ignore-type */ true);
Loading history...
414
		// Write mime header. Because it's not provided in the attachment, otherwise openssl won't parse it
415
		$fp = fopen($tmpFile, 'w');
416
		fwrite($fp, "Content-Type: application/pkcs7-mime; name=\"smime.p7m\"; smime-type=enveloped-data\n");
417
		fwrite($fp, "Content-Transfer-Encoding: base64\nContent-Disposition: attachment; filename=\"smime.p7m\"\n");
418
		fwrite($fp, "Content-Description: S/MIME Encrypted Message\n\n");
419
		fwrite($fp, chunk_split(base64_encode((string) $data['data']), 72) . "\n");
420
		fclose($fp);
421
		if (isset($pass) && !empty($pass)) {
422
			$certs = readPrivateCert($this->getStore(), $pass, false);
0 ignored issues
show
$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

422
			$certs = readPrivateCert(/** @scrutinizer ignore-type */ $this->getStore(), $pass, false);
Loading history...
423
			// create random file for saving the encrypted and body message
424
			$tmpDecrypted = tempnam(sys_get_temp_dir(), true);
425
426
			$decryptStatus = false;
427
			// If multiple private certs were decrypted with supplied password
428
			if (!$certs['cert'] && count($certs) > 0) {
429
				foreach ($certs as $cert) {
430
					$this->clear_openssl_error();
431
					$decryptStatus = openssl_pkcs7_decrypt($tmpFile, $tmpDecrypted, $cert['cert'], [$cert['pkey'], $pass]);
432
					if ($decryptStatus !== false) {
433
						break;
434
					}
435
				}
436
			}
437
			else {
438
				$this->clear_openssl_error();
439
				$decryptStatus = openssl_pkcs7_decrypt($tmpFile, $tmpDecrypted, $certs['cert'], [$certs['pkey'], $pass]);
440
			}
441
442
			$ossl_error = $this->extract_openssl_error();
443
			$content = file_get_contents($tmpDecrypted);
444
			// Handle OL empty body Outlook Signed & Encrypted mails.
445
			// The S/MIME plugin has to extract the body from the signed message.
446
			if (str_contains($content, 'signed-data')) {
447
				$this->message['type'] = 'encryptsigned';
448
				$olcert = tempnam(sys_get_temp_dir(), true);
449
				$olmsg = tempnam(sys_get_temp_dir(), true);
450
				openssl_pkcs7_verify($tmpDecrypted, PKCS7_NOVERIFY, $olcert);
451
				openssl_pkcs7_verify($tmpDecrypted, PKCS7_NOVERIFY, $olcert, [], $olcert, $olmsg);
452
				$content = file_get_contents($olmsg);
453
				unlink($olmsg);
454
				unlink($olcert);
455
			}
456
457
			$copyProps = mapi_getprops($data['message'], [PR_MESSAGE_DELIVERY_TIME, PR_SENDER_ENTRYID, PR_SENT_REPRESENTING_ENTRYID, PR_TRANSPORT_MESSAGE_HEADERS]);
458
			mapi_inetmapi_imtomapi($GLOBALS['mapisession']->getSession(), $data['store'], $GLOBALS['mapisession']->getAddressbook(), $data['message'], $content, ['parse_smime_signed' => true]);
459
			$this->join_xph($copyProps, $data['message']);
460
			// Manually set time back to the received time, since mapi_inetmapi_imtomapi overwrites this
461
			mapi_setprops($data['message'], $copyProps);
462
463
			// remove temporary files
464
			unlink($tmpFile);
465
			unlink($tmpDecrypted);
466
467
			// mapi_inetmapi_imtomapi removes the PR_MESSAGE_CLASS = 'IPM.Note.SMIME.MultipartSigned'
468
			// So we need to check if the message was also signed by looking at the MIME_TAG in the eml
469
			if (str_contains($content, 'multipart/signed') || str_contains($content, 'signed-data')) {
470
				$this->message['type'] = 'encryptsigned';
471
				$this->verifyMessage($data['message'], $content);
472
			}
473
			elseif ($decryptStatus) {
474
				$this->message['info'] = SMIME_DECRYPT_SUCCESS;
475
				$this->message['success'] = SMIME_STATUS_SUCCESS;
476
			}
477
			elseif ($ossl_error === OPENSSL_RECIPIENT_CERTIFICATE_MISMATCH) {
478
				error_log("[smime] Error when decrypting email, openssl error: " . print_r($this->openssl_error, true));
0 ignored issues
show
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

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

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

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

782
		$certs = readPrivateCert(/** @scrutinizer ignore-type */ $this->getStore(), $encryptionStore->get('smime'));
Loading history...
783
784
		// Retrieve intermediate CA's for verification, if available
785
		if (isset($certs['extracerts'])) {
786
			$tmpFile = tempnam(sys_get_temp_dir(), true);
0 ignored issues
show
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

786
			$tmpFile = tempnam(sys_get_temp_dir(), /** @scrutinizer ignore-type */ true);
Loading history...
787
			file_put_contents($tmpFile, implode('', $certs['extracerts']));
788
			$ok = openssl_pkcs7_sign($infile, $outfile, $certs['cert'], [$certs['pkey'], ''], [], PKCS7_DETACHED, $tmpFile);
789
			if (!$ok) {
790
				Log::Write(LOGLEVEL_ERROR, sprintf("[smime] Unable to sign message with intermediate certificates, openssl error: '%s'", @openssl_error_string()));
0 ignored issues
show
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

790
				Log::Write(LOGLEVEL_ERROR, sprintf("[smime] Unable to sign message with intermediate certificates, openssl error: '%s'", /** @scrutinizer ignore-type */ @openssl_error_string()));
Loading history...
791
			}
792
			unlink($tmpFile);
793
		}
794
		else {
795
			$ok = openssl_pkcs7_sign($infile, $outfile, $certs['cert'], [$certs['pkey'], ''], [], PKCS7_DETACHED);
796
			if (!$ok) {
797
				Log::Write(LOGLEVEL_ERROR, sprintf("[smime] Unable to sign message, openssl error: '%s'", @openssl_error_string()));
798
			}
799
		}
800
	}
801
802
	/**
803
	 * Function to encrypt an email.
804
	 *
805
	 * @param string $infile       File eml to be encrypted
806
	 * @param string $outfile      File
807
	 * @param object $message      Mapi Message Object
808
	 * @param object $signedAttach
809
	 * @param array  $smimeProps
810
	 */
811
	public function encrypt(&$infile, &$outfile, &$message, &$signedAttach, $smimeProps) {
812
		mapi_setprops($message, [PR_MESSAGE_CLASS => 'IPM.Note.SMIME']);
813
		$smimeProps[PR_ATTACH_MIME_TAG] = "application/pkcs7-mime";
814
		mapi_setprops($signedAttach, $smimeProps);
815
816
		$publicCerts = $this->getPublicKeyForMessage($message);
817
		// Always append our own certificate, so that the mail can be decrypted in 'Sent items'
818
		// Prefer GAB public certificate above MAPI Store certificate.
819
		$email = $GLOBALS['mapisession']->getSMTPAddress();
820
		$user = $this->getGABUser($email);
821
		$cert = $this->getGABCert($user);
822
		if (empty($cert)) {
823
			$cert = base64_decode($this->getPublicKey($email));
824
		}
825
826
		if (!empty($cert)) {
827
			array_push($publicCerts, $cert);
828
		}
829
830
		$ok = openssl_pkcs7_encrypt($infile, $outfile, $publicCerts, [], 0, $this->cipher);
831
		if (!$ok) {
832
			error_log("[smime] unable to encrypt message, openssl error: " . print_r(@openssl_error_string(), true));
0 ignored issues
show
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

832
			error_log("[smime] unable to encrypt message, openssl error: " . /** @scrutinizer ignore-type */ print_r(@openssl_error_string(), true));
Loading history...
833
			Log::Write(LOGLEVEL_ERROR, sprintf("[smime] unable to encrypt message, openssl error: '%s'", @openssl_error_string()));
0 ignored issues
show
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

833
			Log::Write(LOGLEVEL_ERROR, sprintf("[smime] unable to encrypt message, openssl error: '%s'", /** @scrutinizer ignore-type */ @openssl_error_string()));
Loading history...
834
		}
835
		$tmpEml = file_get_contents($outfile);
836
837
		// Grab the base64 data, since MAPI requires it saved as decoded base64 string.
838
		// FIXME: we can do better here
839
		$matches = explode("\n\n", $tmpEml);
840
		$base64 = str_replace("\n", "", $matches[1]);
841
		file_put_contents($outfile, base64_decode($base64));
842
843
		// Empty the body
844
		mapi_setprops($message, [PR_BODY => ""]);
845
	}
846
847
	/**
848
	 * Function which fetches the public certificates for all recipients (TO/CC/BCC) of a message
849
	 * Always get the certificate of an address which expires last.
850
	 *
851
	 * @param object $message Mapi Message Object
852
	 *
853
	 * @return array of public certificates
854
	 */
855
	public function getPublicKeyForMessage($message) {
856
		$recipientTable = mapi_message_getrecipienttable($message);
857
		$recips = mapi_table_queryallrows($recipientTable, [PR_SMTP_ADDRESS, PR_RECIPIENT_TYPE, PR_ADDRTYPE], [RES_OR, [
858
			[RES_PROPERTY,
859
				[
860
					RELOP => RELOP_EQ,
861
					ULPROPTAG => PR_RECIPIENT_TYPE,
862
					VALUE => MAPI_BCC,
863
				],
864
			],
865
			[RES_PROPERTY,
866
				[
867
					RELOP => RELOP_EQ,
868
					ULPROPTAG => PR_RECIPIENT_TYPE,
869
					VALUE => MAPI_CC,
870
				],
871
			],
872
			[RES_PROPERTY,
873
				[
874
					RELOP => RELOP_EQ,
875
					ULPROPTAG => PR_RECIPIENT_TYPE,
876
					VALUE => MAPI_TO,
877
				],
878
			],
879
		]]);
880
881
		$publicCerts = [];
882
		$storeCert = '';
0 ignored issues
show
The assignment to $storeCert is dead and can be removed.
Loading history...
883
		$gabCert = '';
884
885
		foreach ($recips as $recip) {
886
			$emailAddr = $recip[PR_SMTP_ADDRESS];
887
			$addrType = $recip[PR_ADDRTYPE];
888
889
			if ($addrType === "ZARAFA" || $addrType === "EX") {
890
				$user = $this->getGABUser($emailAddr);
891
				$gabCert = $this->getGABCert($user);
892
			}
893
894
			$storeCert = $this->getPublicKey($emailAddr);
895
896
			if (!empty($gabCert)) {
897
				array_push($publicCerts, $gabCert);
898
			}
899
			elseif (!empty($storeCert)) {
900
				array_push($publicCerts, base64_decode($storeCert));
901
			}
902
		}
903
904
		return $publicCerts;
905
	}
906
907
	/**
908
	 * Retrieves the public certificates stored in the MAPI UserStore and belonging to the
909
	 * emailAdddress, returns "" if there is no certificate for that user.
910
	 *
911
	 * @param string emailAddress
0 ignored issues
show
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...
912
	 * @param mixed $emailAddress
913
	 * @param mixed $multiple
914
	 *
915
	 * @return string $certificate
916
	 */
917
	public function getPublicKey($emailAddress, $multiple = false) {
918
		$certificates = [];
919
920
		$certs = getMAPICert($this->getStore(), 'WebApp.Security.Public', $emailAddress);
0 ignored issues
show
$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

920
		$certs = getMAPICert(/** @scrutinizer ignore-type */ $this->getStore(), 'WebApp.Security.Public', $emailAddress);
Loading history...
921
922
		if ($certs && count($certs) > 0) {
0 ignored issues
show
$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

922
		if ($certs && count(/** @scrutinizer ignore-type */ $certs) > 0) {
Loading history...
923
			foreach ($certs as $cert) {
0 ignored issues
show
The expression $certs of type resource|true is not traversable.
Loading history...
924
				$pubkey = mapi_msgstore_openentry($this->getStore(), $cert[PR_ENTRYID]);
925
				$certificate = "";
926
				if ($pubkey == false) {
927
					continue;
928
				}
929
				// retrieve pkcs#11 certificate from body
930
				$stream = mapi_openproperty($pubkey, PR_BODY, IID_IStream, 0, 0);
931
				$stat = mapi_stream_stat($stream);
932
				mapi_stream_seek($stream, 0, STREAM_SEEK_SET);
933
				for ($i = 0; $i < $stat['cb']; $i += 1024) {
934
					$certificate .= mapi_stream_read($stream, 1024);
935
				}
936
				array_push($certificates, $certificate);
937
			}
938
		}
939
940
		return $multiple ? $certificates : ($certificates[0] ?? '');
941
	}
942
943
	/**
944
	 * Function which is used to check if there is a public certificate for the provided emailAddress.
945
	 *
946
	 * @param string emailAddress emailAddres of recipient
947
	 * @param bool gabUser is the user of PR_ADDRTYPE == ZARAFA
0 ignored issues
show
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...
948
	 * @param mixed $emailAddress
949
	 * @param mixed $gabUser
950
	 *
951
	 * @return bool true if public certificate exists
952
	 */
953
	public function pubcertExists($emailAddress, $gabUser = false) {
954
		if ($gabUser) {
955
			$user = $this->getGABUser($emailAddress);
956
			$gabCert = $this->getGABCert($user);
957
			if ($user && !empty($gabCert)) {
958
				return true;
959
			}
960
		}
961
962
		$root = mapi_msgstore_openentry($this->getStore());
963
		$table = mapi_folder_getcontentstable($root, MAPI_ASSOCIATED);
964
965
		// Restriction for public certificates which are from the recipient of the email, are active and have the correct message_class
966
		$restrict = [RES_AND, [
967
			[RES_PROPERTY,
968
				[
969
					RELOP => RELOP_EQ,
970
					ULPROPTAG => PR_MESSAGE_CLASS,
971
					VALUE => [PR_MESSAGE_CLASS => "WebApp.Security.Public"],
972
				],
973
			],
974
			[RES_PROPERTY,
975
				[
976
					RELOP => RELOP_EQ,
977
					ULPROPTAG => PR_SUBJECT,
978
					VALUE => [PR_SUBJECT => $emailAddress],
979
				],
980
			],
981
		]];
982
		mapi_table_restrict($table, $restrict, TBL_BATCH);
983
		mapi_table_sort($table, [PR_MESSAGE_DELIVERY_TIME => TABLE_SORT_DESCEND], TBL_BATCH);
984
985
		$rows = mapi_table_queryallrows($table, [PR_SUBJECT, PR_ENTRYID, PR_MESSAGE_DELIVERY_TIME, PR_CLIENT_SUBMIT_TIME], $restrict);
986
987
		return !empty($rows);
988
	}
989
990
	public function clear_openssl_error() {
991
		while (@openssl_error_string() !== false)
992
		/* nothing */;
993
	}
994
995
	/**
996
	 * Helper functions which extracts the errors from openssl_error_string()
997
	 * Example error from openssl_error_string(): error:21075075:PKCS7 routines:PKCS7_verify:certificate verify error
998
	 * 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.
999
	 *
1000
	 * @return string
1001
	 */
1002
	public function extract_openssl_error() {
1003
		$this->openssl_error = "";
1004
		while (($s = @openssl_error_string()) !== false) {
1005
			if (strlen($this->openssl_error) == 0) {
1006
				$this->openssl_error = $s;
1007
			}
1008
			else {
1009
				$this->openssl_error .= "\n" . $s;
1010
			}
1011
		}
1012
		$openssl_error_code = 0;
1013
		if ($this->openssl_error) {
1014
			$openssl_error_list = explode(":", $this->openssl_error);
1015
			$openssl_error_code = $openssl_error_list[1];
1016
		}
1017
1018
		return $openssl_error_code;
1019
	}
1020
1021
	/**
1022
	 * Extract the intermediate certificates from the signed email.
1023
	 * Uses openssl_pkcs7_verify to extract the PKCS#7 blob and then converts the PKCS#7 blob to
1024
	 * X509 certificates using openssl_pkcs7_read.
1025
	 *
1026
	 * @param string $emlfile - the s/mime message
1027
	 *
1028
	 * @return array a list of extracted intermediate certificates
1029
	 */
1030
	public function extractCAs($emlfile) {
1031
		$cas = [];
1032
		$certfile = tempnam(sys_get_temp_dir(), true);
0 ignored issues
show
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

1032
		$certfile = tempnam(sys_get_temp_dir(), /** @scrutinizer ignore-type */ true);
Loading history...
1033
		$outfile = tempnam(sys_get_temp_dir(), true);
1034
		$p7bfile = tempnam(sys_get_temp_dir(), true);
1035
		openssl_pkcs7_verify($emlfile, PKCS7_NOVERIFY, $certfile);
1036
		openssl_pkcs7_verify($emlfile, PKCS7_NOVERIFY, $certfile, [], $certfile, $outfile, $p7bfile);
1037
1038
		$p7b = file_get_contents($p7bfile);
1039
1040
		openssl_pkcs7_read($p7b, $cas);
1041
		unlink($certfile);
1042
		unlink($outfile);
1043
		unlink($p7bfile);
1044
1045
		return $cas;
1046
	}
1047
1048
	/**
1049
	 * Imports certificate in the MAPI Root Associated Folder.
1050
	 *
1051
	 * Private key, always insert certificate
1052
	 * Public key, check if we already have one stored
1053
	 *
1054
	 * @param string $cert     certificate body as a string
1055
	 * @param mixed  $certData an array with the parsed certificate data
1056
	 * @param string $type     certificate type, default 'public'
1057
	 * @param bool   $force    force import the certificate even though we have one already stored in the MAPI Store.
1058
	 *                         FIXME: remove $force in the future and move the check for newer certificate in this function.
1059
	 */
1060
	public function importCertificate($cert, $certData, $type = 'public', $force = false) {
1061
		$certEmail = getCertEmail($certData);
1062
		if ($this->pubcertExists($certEmail) && !$force && $type !== 'private') {
1063
			return;
1064
		}
1065
		$issued_by = "";
1066
		foreach (array_keys($certData['issuer']) as $key) {
1067
			$issued_by .= $key . '=' . $certData['issuer'][$key] . "\n";
1068
		}
1069
1070
		$root = mapi_msgstore_openentry($this->getStore());
1071
		$assocMessage = mapi_folder_createmessage($root, MAPI_ASSOCIATED);
1072
		// TODO: write these properties down.
1073
		mapi_setprops($assocMessage, [
1074
			PR_SUBJECT => $certEmail,
1075
			PR_MESSAGE_CLASS => $type == 'public' ? 'WebApp.Security.Public' : 'WebApp.Security.Private',
1076
			PR_MESSAGE_DELIVERY_TIME => $certData['validTo_time_t'],
1077
			PR_CLIENT_SUBMIT_TIME => $certData['validFrom_time_t'],
1078
			PR_SENDER_NAME => $certData['serialNumber'], // serial
1079
			PR_SENDER_EMAIL_ADDRESS => $issued_by, // Issuer To
1080
			PR_SUBJECT_PREFIX => '',
1081
			PR_RECEIVED_BY_NAME => $this->fingerprint_cert($cert, 'sha1'), // SHA1 Fingerprint
1082
			PR_INTERNET_MESSAGE_ID => $this->fingerprint_cert($cert), // MD5 FingerPrint
1083
		]);
1084
		// Save attachment
1085
		$msgBody = base64_encode($cert);
1086
		$stream = mapi_openproperty($assocMessage, PR_BODY, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY);
1087
		mapi_stream_setsize($stream, strlen($msgBody));
1088
		mapi_stream_write($stream, $msgBody);
1089
		mapi_stream_commit($stream);
1090
		mapi_message_savechanges($assocMessage);
1091
	}
1092
1093
	/**
1094
	 * Function which returns the fingerprint (hash) of the certificate.
1095
	 *
1096
	 * @param string $hash optional hash algorithm
1097
	 * @param mixed  $body
1098
	 */
1099
	public function fingerprint_cert($body, $hash = 'md5') {
1100
		// TODO: Note for PHP > 5.6 we can use openssl_x509_fingerprint
1101
		$body = str_replace('-----BEGIN CERTIFICATE-----', '', $body);
1102
		$body = str_replace('-----END CERTIFICATE-----', '', $body);
1103
		$body = base64_decode($body);
1104
1105
		if ($hash === 'sha1') {
1106
			$fingerprint = sha1($body);
1107
		}
1108
		else {
1109
			$fingerprint = md5($body);
1110
		}
1111
1112
		// Format 1000AB as 10:00:AB
1113
		return strtoupper(implode(':', str_split($fingerprint, 2)));
0 ignored issues
show
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

1113
		return strtoupper(implode(':', /** @scrutinizer ignore-type */ str_split($fingerprint, 2)));
Loading history...
1114
	}
1115
1116
	/**
1117
	 * Retrieve the GAB User.
1118
	 *
1119
	 * FIXME: ideally this would be a public function in grommunio Web.
1120
	 *
1121
	 * @param string $email the email address of the user
1122
	 *
1123
	 * @return mixed $user boolean if false else MAPIObject
1124
	 */
1125
	public function getGABUser($email) {
1126
		$addrbook = $GLOBALS["mapisession"]->getAddressbook();
1127
		$userArr = [[PR_DISPLAY_NAME => $email]];
1128
		$user = false;
0 ignored issues
show
The assignment to $user is dead and can be removed.
Loading history...
1129
1130
		try {
1131
			$user = mapi_ab_resolvename($addrbook, $userArr, EMS_AB_ADDRESS_LOOKUP);
1132
			$user = mapi_ab_openentry($addrbook, $user[0][PR_ENTRYID]);
1133
		}
1134
		catch (MAPIException $e) {
1135
			$e->setHandled();
1136
		}
1137
1138
		return $user;
1139
	}
1140
1141
	/**
1142
	 * Retrieve the PR_EMS_AB_X509_CERT.
1143
	 *
1144
	 * @param MAPIObject $user the GAB user
0 ignored issues
show
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...
1145
	 *
1146
	 * @return string $cert the certificate, empty if not found
1147
	 */
1148
	public function getGABCert($user) {
1149
		$cert = '';
1150
		$userCertArray = mapi_getprops($user, [PR_EMS_AB_X509_CERT]);
1151
		if (isset($userCertArray[PR_EMS_AB_X509_CERT])) {
1152
			$cert = der2pem($userCertArray[PR_EMS_AB_X509_CERT][0]);
1153
		}
1154
1155
		return $cert;
1156
	}
1157
1158
	/**
1159
	 * Called when the core Settings class is initialized and ready to accept sysadmin default
1160
	 * settings. Registers the sysadmin defaults for the example plugin.
1161
	 *
1162
	 * @param mixed $data Reference to the data of the triggered hook
1163
	 */
1164
	public function onBeforeSettingsInit(&$data) {
1165
		$data['settingsObj']->addSysAdminDefaults([
1166
			'zarafa' => [
1167
				'v1' => [
1168
					'plugins' => [
1169
						'smime' => [
1170
							'enable' => defined('PLUGIN_SMIME_USER_DEFAULT_ENABLE_SMIME') && PLUGIN_SMIME_USER_DEFAULT_ENABLE_SMIME,
1171
							'passphrase_cache' => defined('PLUGIN_SMIME_PASSPHRASE_REMEMBER_BROWSER') && PLUGIN_SMIME_PASSPHRASE_REMEMBER_BROWSER,
1172
						],
1173
					],
1174
				],
1175
			],
1176
		]);
1177
	}
1178
1179
	/**
1180
	 * Get sender structure of the MAPI Message.
1181
	 *
1182
	 * @param mapimessage $mapiMessage MAPI Message resource from which we need to get the sender
0 ignored issues
show
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...
1183
	 *
1184
	 * @return array with properties
1185
	 */
1186
	public function getSenderAddress($mapiMessage) {
1187
		if (method_exists($GLOBALS['operations'], 'getSenderAddress')) {
1188
			return $GLOBALS["operations"]->getSenderAddress($mapiMessage);
1189
		}
1190
1191
		$messageProps = mapi_getprops($mapiMessage, [PR_SENT_REPRESENTING_ENTRYID, PR_SENDER_ENTRYID]);
1192
		$senderEntryID = $messageProps[PR_SENT_REPRESENTING_ENTRYID] ?? $messageProps[PR_SENDER_ENTRYID];
1193
1194
		try {
1195
			$senderUser = mapi_ab_openentry($GLOBALS["mapisession"]->getAddressbook(), $senderEntryID);
1196
			if ($senderUser) {
1197
				$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]);
1198
1199
				$senderStructure = [];
1200
				$senderStructure["props"]['entryid'] = isset($userprops[PR_ENTRYID]) ? bin2hex((string) $userprops[PR_ENTRYID]) : '';
1201
				$senderStructure["props"]['display_name'] = $userprops[PR_DISPLAY_NAME] ?? '';
1202
				$senderStructure["props"]['email_address'] = $userprops[PR_EMAIL_ADDRESS] ?? '';
1203
				$senderStructure["props"]['smtp_address'] = $userprops[PR_SMTP_ADDRESS] ?? '';
1204
				$senderStructure["props"]['address_type'] = $userprops[PR_ADDRTYPE] ?? '';
1205
				$senderStructure["props"]['object_type'] = $userprops[PR_OBJECT_TYPE];
1206
				$senderStructure["props"]['recipient_type'] = MAPI_TO;
1207
				$senderStructure["props"]['display_type'] = $userprops[PR_DISPLAY_TYPE] ?? MAPI_MAILUSER;
1208
				$senderStructure["props"]['display_type_ex'] = $userprops[PR_DISPLAY_TYPE_EX] ?? MAPI_MAILUSER;
1209
			}
1210
		}
1211
		catch (MAPIException $e) {
1212
			error_log(sprintf("[smime] getSenderAddress(): Exception %s", $e));
1213
		}
1214
1215
		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...
1216
	}
1217
}
1218