Test Failed
Push — master ( 45b28c...46554f )
by
unknown
09:08
created

Pluginsmime::getStore()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
nc 2
nop 0
dl 0
loc 6
rs 10
c 1
b 0
f 0
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
	 * Called to initialize the plugin and register for hooks.
54
	 */
55
	public function init() {
56
		$this->registerHook('server.core.settings.init.before');
57
		$this->registerHook('server.util.parse_smime.signed');
58
		$this->registerHook('server.util.parse_smime.encrypted');
59
		$this->registerHook('server.module.itemmodule.open.after');
60
		$this->registerHook('server.core.operations.submitmessage');
61
		$this->registerHook('server.upload_attachment.upload');
62
		$this->registerHook('server.module.createmailitemmodule.beforesend');
63
		$this->registerHook('server.index.load.custom');
64
65
		if (version_compare(phpversion(), '5.4', '<')) {
66
			$this->cipher = OPENSSL_CIPHER_3DES;
0 ignored issues
show
Bug Best Practice introduced by
The property cipher does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
67
		}
68
		else {
69
			$this->cipher = PLUGIN_SMIME_CIPHER;
70
		}
71
	}
72
73
	/**
74
	 * Default message store.
75
	 *
76
	 * @return object MAPI Message store
77
	 */
78
	public function getStore() {
79
		if (!$this->store) {
80
			$this->store = $GLOBALS['mapisession']->getDefaultMessageStore();
81
		}
82
83
		return $this->store;
84
	}
85
86
	/**
87
	 * Process the incoming events that where fired by the client.
88
	 *
89
	 * @param string $eventID Identifier of the hook
90
	 * @param array  $data    Reference to the data of the triggered hook
91
	 */
92
	public function execute($eventID, &$data) {
93
		switch ($eventID) {
94
		// Register plugin
95
		case 'server.core.settings.init.before':
96
			$this->onBeforeSettingsInit($data);
97
			break;
98
		// Verify a signed or encrypted message when an email is opened
99
		case 'server.util.parse_smime.signed':
100
			$this->onSignedMessage($data);
101
			break;
102
103
		case 'server.util.parse_smime.encrypted':
104
			$this->onEncrypted($data);
105
			break;
106
		// Add S/MIME property, which is send to the client
107
		case 'server.module.itemmodule.open.after':
108
			$this->onAfterOpen($data);
109
			break;
110
		// Catch uploaded certificate
111
		case 'server.upload_attachment.upload':
112
			$this->onUploadCertificate($data);
113
			break;
114
		// Sign email before sending
115
		case 'server.core.operations.submitmessage':
116
			$this->onBeforeSend($data);
117
			break;
118
		// Verify that we have public certificates for all recipients
119
		case 'server.module.createmailitemmodule.beforesend':
120
			$this->onCertificateCheck($data);
121
			break;
122
123
		case 'server.index.load.custom':
124
			if ($data['name'] === 'smime_passphrase') {
125
				include 'templates/passphrase.tpl.php';
126
				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...
127
			}
128
			if ($data['name'] === 'smime_passphrasecheck') {
129
				// No need to do anything, this is just used to trigger
130
				// the browser's autofill save password dialog.
131
				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...
132
			}
133
			break;
134
		}
135
	}
136
137
	/**
138
	 * Function checks if public certificate exists for all recipients and creates an error
139
	 * message for the frontend which includes the email address of the missing public
140
	 * certificates.
141
	 *
142
	 * If my own certificate is missing, a different error message is shown which informs the
143
	 * user that his own public certificate is missing and required for reading encrypted emails
144
	 * in the 'Sent items' folder.
145
	 *
146
	 * @param array $data Reference to the data of the triggered hook
147
	 */
148
	public function onCertificateCheck($data) {
149
		$entryid = $data['entryid'];
150
		// FIXME: unittests, save trigger will pass $entryid is 0 (which will open the root folder and not the message we want)
151
		if ($entryid === false) {
152
			return;
153
		}
154
155
		if (!isset($data['action']['props']['smime']) || empty($data['action']['props']['smime'])) {
156
			return;
157
		}
158
159
		$message = mapi_msgstore_openentry($data['store'], $entryid);
160
		$module = $data['moduleObject'];
161
		$data['success'] = true;
162
163
		$messageClass = mapi_getprops($message, [PR_MESSAGE_CLASS]);
164
		$messageClass = $messageClass[PR_MESSAGE_CLASS];
165
		if ($messageClass !== 'IPM.Note.SMIME' &&
166
		    $messageClass !== 'IPM.Note.SMIME.SignedEncrypt' &&
167
		    $messageClass !== 'IPM.Note.deferSMIME' &&
168
		    $messageClass !== 'IPM.Note.deferSMIME.SignedEncrypt')
169
			return;
170
171
		$recipients = $data['action']['props']['smime'];
172
		$missingCerts = [];
173
174
		foreach ($recipients as $recipient) {
175
			$email = $recipient['email'];
176
177
			if (!$this->pubcertExists($email, $recipient['internal'])) {
178
				array_push($missingCerts, $email);
179
			}
180
		}
181
182
		if (empty($missingCerts)) {
183
			return;
184
		}
185
186
		function missingMyself($email) {
187
			return $GLOBALS['mapisession']->getSMTPAddress() === $email;
188
		}
189
190
		if (array_filter($missingCerts, "missingMyself") === []) {
191
			$errorMsg = _('Missing public certificates for the following recipients: ') . implode(', ', $missingCerts) . _('. Please contact your system administrator for details');
192
		}
193
		else {
194
			$errorMsg = _("Your public certificate is not installed. Without this certificate, you will not be able to read encrypted messages you have sent to others.");
195
		}
196
197
		$module->sendFeedback(false, ["type" => ERROR_GENERAL, "info" => ['display_message' => $errorMsg]]);
198
		$data['success'] = false;
199
	}
200
201
	/**
202
	 * Function which verifies a message.
203
	 *
204
	 * TODO: Clean up flow
205
	 *
206
	 * @param mixed $message
207
	 * @param mixed $eml
208
	 */
209
	public function verifyMessage($message, $eml) {
210
		$userCert = '';
211
		$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

211
		$tmpUserCert = tempnam(sys_get_temp_dir(), /** @scrutinizer ignore-type */ true);
Loading history...
212
		$importMessageCert = true;
213
		$fromGAB = false;
214
215
		// TODO: worth to split fetching public certificate in a separate function?
216
217
		// If user entry exists in GAB, try to retrieve public cert
218
		// Public certificate from GAB in combination with LDAP saved in PR_EMS_AB_X509_CERT
219
		$userProps = mapi_getprops($message, [PR_SENT_REPRESENTING_ENTRYID, PR_SENT_REPRESENTING_NAME]);
220
		if (isset($userProps[PR_SENT_REPRESENTING_ENTRYID])) {
221
			try {
222
				$user = mapi_ab_openentry($GLOBALS['mapisession']->getAddressbook(), $userProps[PR_SENT_REPRESENTING_ENTRYID]);
223
				$gabCert = $this->getGABCert($user);
224
				if (!empty($gabCert)) {
225
					$fromGAB = true;
226
					// Put empty string into file? dafuq?
227
					file_put_contents($tmpUserCert, $userCert);
228
				}
229
			}
230
			catch (MAPIException $e) {
231
				$msg = "[smime] Unable to open PR_SENT_REPRESENTING_ENTRYID. Maybe %s was does not exists or deleted from server.";
232
				Log::write(LOGLEVEL_ERROR, sprintf($msg, $userProps[PR_SENT_REPRESENTING_NAME]));
233
				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

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

342
				Log::write(LOGLEVEL_INFO, sprintf("[smime] Unable to verify message without public key, openssl error: '%s'", /** @scrutinizer ignore-type */ $this->openssl_error));
Loading history...
343
				$this->message['success'] = SMIME_STATUS_FAIL;
344
				$this->message['info'] = SMIME_CA;
345
			}
346
		}
347
		// Certificate is newer or not yet imported to the user store and not revoked
348
		// If certificate is from the GAB, then don't import it.
349
		if ($importMessageCert && !$fromGAB) {
350
			$signed_ok = openssl_pkcs7_verify($tmpfname, PKCS7_NOSIGS, $outcert, explode(';', PLUGIN_SMIME_CACERTS));
351
			$openssl_error_code = $this->extract_openssl_error();
352
			$this->validateSignedMessage($signed_ok, $openssl_error_code);
353
			$userCert = file_get_contents($outcert);
354
			$parsedImportCert = openssl_x509_parse($userCert);
355
			// FIXME: doing this in importPublicKey too...
356
			$certEmail = getCertEmail($parsedImportCert);
357
			if (!empty($certEmail)) {
358
				$this->importCertificate($userCert, $parsedImportCert, 'public', true);
359
			}
360
		}
361
362
		// Remove extracted certificate from openssl_pkcs7_verify
363
		unlink($outcert);
364
365
		// remove the temporary file
366
		unlink($tmpfname);
367
368
		// Clean up temp cert
369
		unlink($tmpUserCert);
370
	}
371
372
	/**
373
	 * Function which decrypts an encrypted message.
374
	 * The key should be unlocked and stored in the EncryptionStore for a successful decrypt
375
	 * If the key isn't in the session, we give the user a message to unlock his certificate.
376
	 *
377
	 * @param {mixed} $data array of data from hook
0 ignored issues
show
Documentation Bug introduced by
The doc comment {mixed} at position 0 could not be parsed: Unknown type name '{' at position 0 in {mixed}.
Loading history...
378
	 */
379
	public function onEncrypted($data) {
380
		// Cert unlocked, decode message
381
		$this->message['success'] = SMIME_STATUS_INFO;
382
		$this->message['info'] = SMIME_DECRYPT_FAILURE;
383
384
		$this->message['type'] = 'encrypted';
385
		$encryptionStore = EncryptionStore::getInstance();
386
		$pass = $encryptionStore->get('smime');
387
		if (isset($pass) && !empty($pass)) {
388
			$certs = readPrivateCert($this->getStore(), $pass, false);
389
			// create random file for saving the encrypted and body message
390
			$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

390
			$tmpFile = tempnam(sys_get_temp_dir(), /** @scrutinizer ignore-type */ true);
Loading history...
391
			$tmpDecrypted = tempnam(sys_get_temp_dir(), true);
392
393
			// Write mime header. Because it's not provided in the attachment, otherwise openssl won't parse it
394
			$fp = fopen($tmpFile, 'w');
395
			fwrite($fp, "Content-Type: application/pkcs7-mime; name=\"smime.p7m\"; smime-type=enveloped-data\n");
396
			fwrite($fp, "Content-Transfer-Encoding: base64\nContent-Disposition: attachment; filename=\"smime.p7m\"\n");
397
			fwrite($fp, "Content-Description: S/MIME Encrypted Message\n\n");
398
			fwrite($fp, chunk_split(base64_encode($data['data']), 72) . "\n");
399
			fclose($fp);
400
401
			$decryptStatus = false;
402
			// If multiple private certs were decrypted with supplied password
403
			if (!$certs['cert'] && count($certs) > 0) {
404
				foreach ($certs as $cert) {
405
					$decryptStatus = openssl_pkcs7_decrypt($tmpFile, $tmpDecrypted, $cert['cert'], [$cert['pkey'], $pass]);
406
					if ($decryptStatus !== false) {
407
						break;
408
					}
409
				}
410
			}
411
			else {
412
				$decryptStatus = openssl_pkcs7_decrypt($tmpFile, $tmpDecrypted, $certs['cert'], [$certs['pkey'], $pass]);
413
			}
414
415
			$content = file_get_contents($tmpDecrypted);
416
			// Handle OL empty body Outlook Signed & Encrypted mails.
417
			// The S/MIME plugin has to extract the body from the signed message.
418
			if (strpos($content, 'signed-data') !== false) {
419
				$this->message['type'] = 'encryptsigned';
420
				$olcert = tempnam(sys_get_temp_dir(), true);
421
				$olmsg = tempnam(sys_get_temp_dir(), true);
422
				openssl_pkcs7_verify($tmpDecrypted, PKCS7_NOVERIFY, $olcert);
423
				openssl_pkcs7_verify($tmpDecrypted, PKCS7_NOVERIFY, $olcert, [], $olcert, $olmsg);
424
				$content = file_get_contents($olmsg);
425
				unlink($olmsg);
426
				unlink($olcert);
427
			}
428
429
			$copyProps = mapi_getprops($data['message'], [PR_MESSAGE_DELIVERY_TIME, PR_SENDER_ENTRYID, PR_SENT_REPRESENTING_ENTRYID]);
430
			mapi_inetmapi_imtomapi($GLOBALS['mapisession']->getSession(), $data['store'], $GLOBALS['mapisession']->getAddressbook(), $data['message'], $content, ['parse_smime_signed' => true]);
431
			// Manually set time back to the received time, since mapi_inetmapi_imtomapi overwrites this
432
			mapi_setprops($data['message'], $copyProps);
433
434
			// remove temporary files
435
			unlink($tmpFile);
436
			unlink($tmpDecrypted);
437
438
			// mapi_inetmapi_imtomapi removes the PR_MESSAGE_CLASS = 'IPM.Note.SMIME.MultipartSigned'
439
			// So we need to check if the message was also signed by looking at the MIME_TAG in the eml
440
			if (strpos($content, 'multipart/signed') !== false || strpos($content, 'signed-data') !== false) {
441
				$this->message['type'] = 'encryptsigned';
442
				$this->verifyMessage($data['message'], $content);
443
			}
444
			elseif ($decryptStatus) {
445
				$this->message['info'] = SMIME_DECRYPT_SUCCESS;
446
				$this->message['success'] = SMIME_STATUS_SUCCESS;
447
			}
448
			elseif ($this->extract_openssl_error() === OPENSSL_RECIPIENT_CERTIFICATE_MISMATCH) {
449
				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

449
				error_log("[smime] Error when decrypting email, openssl error: " . /** @scrutinizer ignore-type */ print_r($this->openssl_error, true));
Loading history...
450
				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

450
				Log::Write(LOGLEVEL_ERROR, sprintf("[smime] Error when decrypting email, openssl error: '%s'", /** @scrutinizer ignore-type */ $this->openssl_error));
Loading history...
451
				$this->message['info'] = SMIME_DECRYPT_CERT_MISMATCH;
452
				$this->message['success'] = SMIME_STATUS_FAIL;
453
			}
454
		}
455
		else {
456
			$this->message['info'] = SMIME_UNLOCK_CERT;
457
		}
458
459
		if (!encryptionStoreExpirationSupport()) {
460
			withPHPSession(function () use ($encryptionStore) {
461
				$encryptionStore->add('smime', '');
462
			});
463
		}
464
	}
465
466
	/**
467
	 * Function which calls verifyMessage to verify if the message isn't malformed during transport.
468
	 *
469
	 * @param {mixed} $data array of data from hook
0 ignored issues
show
Documentation Bug introduced by
The doc comment {mixed} at position 0 could not be parsed: Unknown type name '{' at position 0 in {mixed}.
Loading history...
470
	 */
471
	public function onSignedMessage($data) {
472
		$this->message['type'] = 'signed';
473
		$this->verifyMessage($data['message'], $data['data']);
474
	}
475
476
	/**
477
	 * General function which parses the openssl_pkcs7_verify return value and the errors generated by
478
	 * openssl_error_string().
479
	 *
480
	 * @param mixed $openssl_return
481
	 * @param mixed $openssl_errors
482
	 */
483
	public function validateSignedMessage($openssl_return, $openssl_errors) {
484
		if ($openssl_return === -1) {
485
			$this->message['info'] = SMIME_ERROR;
486
			$this->message['success'] = SMIME_STATUS_FAIL;
487
			return;
488
		// Verification was successful
489
		}
490
		elseif ($openssl_return) {
491
			$this->message['info'] = SMIME_SUCCESS;
492
			$this->message['success'] = SMIME_STATUS_SUCCESS;
493
			return;
494
		// Verification was not successful, display extra information.
495
		}
496
		$this->message['success'] = SMIME_STATUS_FAIL;
497
		if ($openssl_errors === OPENSSL_CA_VERIFY_FAIL) {
498
			$this->message['info'] = SMIME_CA;
499
		}
500
		else { // Catch general errors
501
			$this->message['info'] = SMIME_ERROR;
502
		}
503
	}
504
505
	/**
506
	 * Set smime key in $data array, which is send back to client
507
	 * Since we can't create this array key in the hooks:
508
	 * 'server.util.parse_smime.signed'
509
	 * 'server.util.parse_smime.encrypted'.
510
	 *
511
	 * TODO: investigate if we can move away from this hook
512
	 *
513
	 * @param {mixed} $data
0 ignored issues
show
Documentation Bug introduced by
The doc comment {mixed} at position 0 could not be parsed: Unknown type name '{' at position 0 in {mixed}.
Loading history...
514
	 */
515
	public function onAfterOpen($data) {
516
		if (isset($this->message) && !empty($this->message)) {
517
			$data['data']['item']['props']['smime'] = $this->message;
518
		}
519
	}
520
521
	/**
522
	 * Handles the uploaded certificate in the settingsmenu in grommunio Web
523
	 * - Opens the certificate with provided passphrase
524
	 * - Checks if it can be used for signing/decrypting
525
	 * - Verifies that the email address is equal to the
526
	 * - Verifies that the certificate isn't expired and inform user.
527
	 *
528
	 * @param {mixed} $data
0 ignored issues
show
Documentation Bug introduced by
The doc comment {mixed} at position 0 could not be parsed: Unknown type name '{' at position 0 in {mixed}.
Loading history...
529
	 */
530
	public function onUploadCertificate($data) {
531
		if ($data['sourcetype'] !== 'certificate')
532
			return;
533
		$passphrase = $_POST['passphrase'];
534
		$saveCert = false;
535
		$tmpname = $data['tmpname'];
536
		$message = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $message is dead and can be removed.
Loading history...
537
538
		$certificate = file_get_contents($tmpname);
539
		$emailAddress = $GLOBALS['mapisession']->getSMTPAddress();
540
		list($message, $publickey, $publickeyData) = validateUploadedPKCS($certificate, $passphrase, $emailAddress);
541
542
		// All checks completed successful
543
		// Store private cert in users associated store (check for duplicates)
544
		if (empty($message)) {
545
			$certMessage = getMAPICert($this->getStore());
546
			// TODO: update to serialNumber check
547
			if ($certMessage && $certMessage[0][PR_MESSAGE_DELIVERY_TIME] == $publickeyData['validTo_time_t']) {
548
				$message = _('Certificate is already stored on the server');
549
			}
550
			else {
551
				$saveCert = true;
552
				$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...
553
				// Remove old certificate
554
				/*
555
				if($certMessage) {
556
					// Delete private key
557
					mapi_folder_deletemessages($root, array($certMessage[PR_ENTRYID]));
558
559
					// Delete public key
560
					$pubCert = getMAPICert($this->getStore, 'WebApp.Security.Public', getCertEmail($certMessage));
561
					if($pubCert) {
562
						mapi_folder_deletemessages($root, array($pubCert[PR_ENTRYID]));
563
					}
564
					$message = _('New certificate uploaded');
565
				} else {
566
					$message = _('Certificate uploaded');
567
				}*/
568
569
				$this->importCertificate($certificate, $publickeyData, 'private');
570
571
				// Check if the user has a public key in the GAB.
572
				$store_props = mapi_getprops($this->getStore(), [PR_USER_ENTRYID]);
573
				$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...
574
575
				$this->importCertificate($publickey, $publickeyData, 'public', true);
576
			}
577
		}
578
579
		$returnfiles = [];
580
		$returnfiles[] = [
581
			'props' => [
582
				'attach_num' => -1,
583
				'size' => $data['size'],
584
				'name' => $data['name'],
585
				'cert' => $saveCert,
586
				'cert_warning' => $message,
587
			],
588
		];
589
		$data['returnfiles'] = $returnfiles;
590
	}
591
592
	/**
593
	 * This function handles the 'beforesend' hook which is triggered before sending the email.
594
	 * If the PR_MESSAGE_CLASS is set to a signed email (IPM.Note.SMIME.Multipartsigned), this function
595
	 * will convert the mapi message to RFC822, sign the eml and attach the signed email to the mapi message.
596
	 *
597
	 * @param {mixed} $data from php hook
0 ignored issues
show
Documentation Bug introduced by
The doc comment {mixed} at position 0 could not be parsed: Unknown type name '{' at position 0 in {mixed}.
Loading history...
598
	 */
599
	public function onBeforeSend(&$data) {
600
		$store = $data['store'];
0 ignored issues
show
Unused Code introduced by
The assignment to $store is dead and can be removed.
Loading history...
601
		$message = $data['message'];
602
603
		// Retrieve message class
604
		$props = mapi_getprops($message, [PR_MESSAGE_CLASS]);
605
		$messageClass = $props[PR_MESSAGE_CLASS];
606
607
		if (!isset($messageClass))
608
			return;
609
		if (stripos($messageClass, 'IPM.Note.deferSMIME') === false &&
610
		    stripos($messageClass, 'IPM.Note.SMIME') === false)
611
			return;
612
613
		// FIXME: for now return when we are going to sign but we don't have the passphrase set
614
		// This should never happen sign
615
		$encryptionStore = \EncryptionStore::getInstance();
616
		if (($messageClass === 'IPM.Note.deferSMIME.SignedEncrypt' ||
617
		    $messageClass === 'IPM.Note.deferSMIME.MultipartSigned' ||
618
		    $messageClass === 'IPM.Note.SMIME.SignedEncrypt' ||
619
		    $messageClass === 'IPM.Note.SMIME.MultipartSigned') &&
620
		    !$encryptionStore->get('smime'))
621
			return;
622
		// NOTE: setting message class to IPM.Note, so that mapi_inetmapi_imtoinet converts the message to plain email
623
		// and doesn't fail when handling the attachments.
624
		mapi_setprops($message, [PR_MESSAGE_CLASS => 'IPM.Note']);
625
		mapi_savechanges($message);
626
627
		// Read the message as RFC822-formatted e-mail stream.
628
		$emlMessageStream = mapi_inetmapi_imtoinet($GLOBALS['mapisession']->getSession(), $GLOBALS['mapisession']->getAddressbook(), $message, []);
629
630
		// Remove all attachments, since they are stored in the attached signed message
631
		$atable = mapi_message_getattachmenttable($message);
632
		$rows = mapi_table_queryallrows($atable, [PR_ATTACH_MIME_TAG, PR_ATTACH_NUM]);
633
		foreach ($rows as $row) {
634
			$attnum = $row[PR_ATTACH_NUM];
635
			mapi_message_deleteattach($message, $attnum);
636
		}
637
638
		// create temporary files
639
		$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

639
		$tmpSendEmail = tempnam(sys_get_temp_dir(), /** @scrutinizer ignore-type */ true);
Loading history...
640
		$tmpSendSmimeEmail = tempnam(sys_get_temp_dir(), true);
641
642
		// Save message stream to a file
643
		$stat = mapi_stream_stat($emlMessageStream);
644
645
		$fhandle = fopen($tmpSendEmail, 'w');
646
		$buffer = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $buffer is dead and can be removed.
Loading history...
647
		for ($i = 0; $i < $stat["cb"]; $i += BLOCK_SIZE) {
648
			// Write stream
649
			$buffer = mapi_stream_read($emlMessageStream, BLOCK_SIZE);
650
			fwrite($fhandle, $buffer, strlen($buffer));
651
		}
652
		fclose($fhandle);
653
654
		// Create attachment for S/MIME message
655
		$signedAttach = mapi_message_createattach($message);
656
		$smimeProps = [
657
			PR_ATTACH_LONG_FILENAME => 'smime.p7m',
658
			PR_DISPLAY_NAME => 'smime.p7m',
659
			PR_ATTACH_METHOD => ATTACH_BY_VALUE,
660
			PR_ATTACH_MIME_TAG => 'multipart/signed',
661
			PR_ATTACHMENT_HIDDEN => true,
662
		];
663
664
		// Sign then Encrypt email
665
		switch ($messageClass) {
666
		case 'IPM.Note.deferSMIME.SignedEncrypt':
667
		case 'IPM.Note.SMIME.SignedEncrypt':
668
			$tmpFile = tempnam(sys_get_temp_dir(), true);
669
			$this->sign($tmpSendEmail, $tmpFile, $message, $signedAttach, $smimeProps);
0 ignored issues
show
Bug introduced by
$tmpFile of type string is incompatible with the type object expected by parameter $outfile of Pluginsmime::sign(). ( Ignorable by Annotation )

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

669
			$this->sign($tmpSendEmail, /** @scrutinizer ignore-type */ $tmpFile, $message, $signedAttach, $smimeProps);
Loading history...
Bug introduced by
$tmpSendEmail of type string is incompatible with the type object expected by parameter $infile of Pluginsmime::sign(). ( Ignorable by Annotation )

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

669
			$this->sign(/** @scrutinizer ignore-type */ $tmpSendEmail, $tmpFile, $message, $signedAttach, $smimeProps);
Loading history...
670
			$this->encrypt($tmpFile, $tmpSendSmimeEmail, $message, $signedAttach, $smimeProps);
0 ignored issues
show
Bug introduced by
$tmpSendSmimeEmail of type string is incompatible with the type object expected by parameter $outfile of Pluginsmime::encrypt(). ( Ignorable by Annotation )

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

670
			$this->encrypt($tmpFile, /** @scrutinizer ignore-type */ $tmpSendSmimeEmail, $message, $signedAttach, $smimeProps);
Loading history...
671
			unlink($tmpFile);
672
			break;
673
674
		case 'IPM.Note.deferSMIME.MultipartSigned':
675
		case 'IPM.Note.SMIME.MultipartSigned':
676
			$this->sign($tmpSendEmail, $tmpSendSmimeEmail, $message, $signedAttach, $smimeProps);
677
			break;
678
679
		case 'IPM.Note.deferSMIME':
680
		case 'IPM.Note.SMIME':
681
			$this->encrypt($tmpSendEmail, $tmpSendSmimeEmail, $message, $signedAttach, $smimeProps);
0 ignored issues
show
Bug introduced by
$tmpSendEmail of type string is incompatible with the type object expected by parameter $infile of Pluginsmime::encrypt(). ( Ignorable by Annotation )

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

681
			$this->encrypt(/** @scrutinizer ignore-type */ $tmpSendEmail, $tmpSendSmimeEmail, $message, $signedAttach, $smimeProps);
Loading history...
682
			break;
683
		}
684
685
		// Save the signed message as attachment of the send email
686
		$stream = mapi_openproperty($signedAttach, PR_ATTACH_DATA_BIN, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY);
687
		$handle = fopen($tmpSendSmimeEmail, 'r');
688
		while (!feof($handle)) {
689
			$contents = fread($handle, BLOCK_SIZE);
690
			mapi_stream_write($stream, $contents);
691
		}
692
		fclose($handle);
693
694
		mapi_stream_commit($stream);
695
696
		// remove tmp files
697
		unlink($tmpSendSmimeEmail);
698
		unlink($tmpSendEmail);
699
700
		mapi_savechanges($signedAttach);
701
		mapi_savechanges($message);
702
	}
703
704
	/**
705
	 * Function to sign an email.
706
	 *
707
	 * @param object $infile       File eml to be encrypted
708
	 * @param object $outfile      File
709
	 * @param object $message      Mapi Message Object
710
	 * @param object $signedAttach
711
	 * @param array  $smimeProps
712
	 */
713
	public function sign(&$infile, &$outfile, &$message, &$signedAttach, $smimeProps) {
714
		// Set mesageclass back to IPM.Note.SMIME.MultipartSigned
715
		mapi_setprops($message, [PR_MESSAGE_CLASS => 'IPM.Note.SMIME.MultipartSigned']);
716
		mapi_setprops($signedAttach, $smimeProps);
717
718
		// Obtain private certificate
719
		$encryptionStore = EncryptionStore::getInstance();
720
		// Only the newest one is returned
721
		$certs = readPrivateCert($this->getStore(), $encryptionStore->get('smime'));
722
723
		// Retrieve intermediate CA's for verification, if available
724
		if (isset($certs['extracerts'])) {
725
			$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

725
			$tmpFile = tempnam(sys_get_temp_dir(), /** @scrutinizer ignore-type */ true);
Loading history...
726
			file_put_contents($tmpFile, implode('', $certs['extracerts']));
727
			$ok = openssl_pkcs7_sign($infile, $outfile, $certs['cert'], [$certs['pkey'], ''], [], PKCS7_DETACHED, $tmpFile);
0 ignored issues
show
Bug introduced by
$outfile of type object is incompatible with the type string expected by parameter $output_filename of openssl_pkcs7_sign(). ( Ignorable by Annotation )

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

727
			$ok = openssl_pkcs7_sign($infile, /** @scrutinizer ignore-type */ $outfile, $certs['cert'], [$certs['pkey'], ''], [], PKCS7_DETACHED, $tmpFile);
Loading history...
Bug introduced by
$infile of type object is incompatible with the type string expected by parameter $input_filename of openssl_pkcs7_sign(). ( Ignorable by Annotation )

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

727
			$ok = openssl_pkcs7_sign(/** @scrutinizer ignore-type */ $infile, $outfile, $certs['cert'], [$certs['pkey'], ''], [], PKCS7_DETACHED, $tmpFile);
Loading history...
728
			if (!$ok) {
729
				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

729
				Log::Write(LOGLEVEL_ERROR, sprintf("[smime] Unable to sign message with intermediate certificates, openssl error: '%s'", /** @scrutinizer ignore-type */ @openssl_error_string()));
Loading history...
730
			}
731
			unlink($tmpFile);
732
		}
733
		else {
734
			$ok = openssl_pkcs7_sign($infile, $outfile, $certs['cert'], [$certs['pkey'], ''], [], PKCS7_DETACHED);
735
			if (!$ok) {
736
				Log::Write(LOGLEVEL_ERROR, sprintf("[smime] Unable to sign message, openssl error: '%s'", @openssl_error_string()));
737
			}
738
		}
739
	}
740
741
	/**
742
	 * Function to encrypt an email.
743
	 *
744
	 * @param object $infile       File eml to be encrypted
745
	 * @param object $outfile      File
746
	 * @param object $message      Mapi Message Object
747
	 * @param object $signedAttach
748
	 * @param array  $smimeProps
749
	 */
750
	public function encrypt(&$infile, &$outfile, &$message, &$signedAttach, $smimeProps) {
751
		mapi_setprops($message, [PR_MESSAGE_CLASS => 'IPM.Note.SMIME']);
752
		$smimeProps[PR_ATTACH_MIME_TAG] = "application/pkcs7-mime";
753
		mapi_setprops($signedAttach, $smimeProps);
754
755
		$publicCerts = $this->getPublicKeyForMessage($message);
756
		// Always append our own certificate, so that the mail can be decrypted in 'Sent items'
757
		// Prefer GAB public certificate above MAPI Store certificate.
758
		$email = $GLOBALS['mapisession']->getSMTPAddress();
759
		$user = $this->getGABUser($email);
760
		$cert = $this->getGABCert($user);
761
		if (empty($cert)) {
762
			$cert = base64_decode($this->getPublicKey($email));
763
		}
764
765
		if (!empty($cert)) {
766
			array_push($publicCerts, $cert);
767
		}
768
769
		$ok = openssl_pkcs7_encrypt($infile, $outfile, $publicCerts, [], 0, $this->cipher);
0 ignored issues
show
Bug introduced by
$outfile of type object is incompatible with the type string expected by parameter $output_filename of openssl_pkcs7_encrypt(). ( Ignorable by Annotation )

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

769
		$ok = openssl_pkcs7_encrypt($infile, /** @scrutinizer ignore-type */ $outfile, $publicCerts, [], 0, $this->cipher);
Loading history...
Bug introduced by
$infile of type object is incompatible with the type string expected by parameter $input_filename of openssl_pkcs7_encrypt(). ( Ignorable by Annotation )

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

769
		$ok = openssl_pkcs7_encrypt(/** @scrutinizer ignore-type */ $infile, $outfile, $publicCerts, [], 0, $this->cipher);
Loading history...
770
		if (!$ok) {
771
			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

771
			error_log("[smime] unable to encrypt message, openssl error: " . /** @scrutinizer ignore-type */ print_r(@openssl_error_string(), true));
Loading history...
772
			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

772
			Log::Write(LOGLEVEL_ERROR, sprintf("[smime] unable to encrypt message, openssl error: '%s'", /** @scrutinizer ignore-type */ @openssl_error_string()));
Loading history...
773
		}
774
		$tmpEml = file_get_contents($outfile);
0 ignored issues
show
Bug introduced by
$outfile of type object is incompatible with the type string expected by parameter $filename of file_get_contents(). ( Ignorable by Annotation )

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

774
		$tmpEml = file_get_contents(/** @scrutinizer ignore-type */ $outfile);
Loading history...
775
776
		// Grab the base64 data, since MAPI requires it saved as decoded base64 string.
777
		// FIXME: we can do better here
778
		$matches = explode("\n\n", $tmpEml);
779
		$base64 = str_replace("\n", "", $matches[1]);
780
		file_put_contents($outfile, base64_decode($base64));
0 ignored issues
show
Bug introduced by
$outfile of type object is incompatible with the type string expected by parameter $filename of file_put_contents(). ( Ignorable by Annotation )

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

780
		file_put_contents(/** @scrutinizer ignore-type */ $outfile, base64_decode($base64));
Loading history...
781
782
		// Empty the body
783
		mapi_setprops($message, [PR_BODY => ""]);
784
	}
785
786
	/**
787
	 * Function which fetches the public certificates for all recipients (TO/CC/BCC) of a message
788
	 * Always get the certificate of an address which expires last.
789
	 *
790
	 * @param object $message Mapi Message Object
791
	 *
792
	 * @return array of public certificates
793
	 */
794
	public function getPublicKeyForMessage($message) {
795
		$recipientTable = mapi_message_getrecipienttable($message);
796
		$recips = mapi_table_queryallrows($recipientTable, [PR_SMTP_ADDRESS, PR_RECIPIENT_TYPE, PR_ADDRTYPE], [RES_OR, [
797
			[RES_PROPERTY,
798
				[
799
					RELOP => RELOP_EQ,
800
					ULPROPTAG => PR_RECIPIENT_TYPE,
801
					VALUE => MAPI_BCC,
802
				],
803
			],
804
			[RES_PROPERTY,
805
				[
806
					RELOP => RELOP_EQ,
807
					ULPROPTAG => PR_RECIPIENT_TYPE,
808
					VALUE => MAPI_CC,
809
				],
810
			],
811
			[RES_PROPERTY,
812
				[
813
					RELOP => RELOP_EQ,
814
					ULPROPTAG => PR_RECIPIENT_TYPE,
815
					VALUE => MAPI_TO,
816
				],
817
			],
818
		]]);
819
820
		$publicCerts = [];
821
		$storeCert = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $storeCert is dead and can be removed.
Loading history...
822
		$gabCert = '';
823
824
		foreach ($recips as $recip) {
825
			$emailAddr = $recip[PR_SMTP_ADDRESS];
826
			$addrType = $recip[PR_ADDRTYPE];
827
828
			if ($addrType === "ZARAFA" || $addrType === "EX") {
829
				$user = $this->getGABUser($emailAddr);
830
				$gabCert = $this->getGABCert($user);
831
			}
832
833
			$storeCert = $this->getPublicKey($emailAddr);
834
835
			if (!empty($gabCert)) {
836
				array_push($publicCerts, $gabCert);
837
			}
838
			elseif (!empty($storeCert)) {
839
				array_push($publicCerts, base64_decode($storeCert));
840
			}
841
		}
842
843
		return $publicCerts;
844
	}
845
846
	/**
847
	 * Retrieves the public certificates stored in the MAPI UserStore and belonging to the
848
	 * emailAdddress, returns "" if there is no certificate for that user.
849
	 *
850
	 * @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...
851
	 * @param mixed $emailAddress
852
	 * @param mixed $multiple
853
	 *
854
	 * @return {String} $certificate
0 ignored issues
show
Documentation Bug introduced by
The doc comment {String} at position 0 could not be parsed: Unknown type name '{' at position 0 in {String}.
Loading history...
855
	 */
856
	public function getPublicKey($emailAddress, $multiple = false) {
857
		$certificates = [];
858
859
		$certs = getMAPICert($this->getStore(), 'WebApp.Security.Public', $emailAddress);
860
861
		if ($certs && count($certs) > 0) {
862
			foreach ($certs as $cert) {
863
				$pubkey = mapi_msgstore_openentry($this->getStore(), $cert[PR_ENTRYID]);
864
				$certificate = "";
865
				if ($pubkey == false)
866
					continue;
867
				// retrieve pkcs#11 certificate from body
868
				$stream = mapi_openproperty($pubkey, PR_BODY, IID_IStream, 0, 0);
869
				$stat = mapi_stream_stat($stream);
870
				mapi_stream_seek($stream, 0, STREAM_SEEK_SET);
871
				for ($i = 0; $i < $stat['cb']; $i += 1024) {
872
					$certificate .= mapi_stream_read($stream, 1024);
873
				}
874
				array_push($certificates, $certificate);
875
			}
876
		}
877
878
		return $multiple ? $certificates : ($certificates[0] ?? '');
879
	}
880
881
	/**
882
	 * Function which is used to check if there is a public certificate for the provided emailAddress.
883
	 *
884
	 * @param {String} emailAddress emailAddres of recipient
885
	 * @param {Boolean} 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...
886
	 * @param mixed $emailAddress
887
	 * @param mixed $gabUser
888
	 *
889
	 * @return {Boolean} true if public certificate exists
0 ignored issues
show
Documentation Bug introduced by
The doc comment {Boolean} at position 0 could not be parsed: Unknown type name '{' at position 0 in {Boolean}.
Loading history...
890
	 */
891
	public function pubcertExists($emailAddress, $gabUser = false) {
892
		if ($gabUser) {
893
			$user = $this->getGABUser($emailAddress);
894
			$gabCert = $this->getGABCert($user);
895
			if ($user && !empty($gabCert)) {
896
				return true;
897
			}
898
		}
899
900
		$root = mapi_msgstore_openentry($this->getStore(), null);
901
		$table = mapi_folder_getcontentstable($root, MAPI_ASSOCIATED);
902
903
		// Restriction for public certificates which are from the recipient of the email, are active and have the correct message_class
904
		$restrict = [RES_AND, [
905
			[RES_PROPERTY,
906
				[
907
					RELOP => RELOP_EQ,
908
					ULPROPTAG => PR_MESSAGE_CLASS,
909
					VALUE => [PR_MESSAGE_CLASS => "WebApp.Security.Public"],
910
				],
911
			],
912
			[RES_PROPERTY,
913
				[
914
					RELOP => RELOP_EQ,
915
					ULPROPTAG => PR_SUBJECT,
916
					VALUE => [PR_SUBJECT => $emailAddress],
917
				],
918
			],
919
		]];
920
		mapi_table_restrict($table, $restrict, TBL_BATCH);
921
		mapi_table_sort($table, [PR_MESSAGE_DELIVERY_TIME => TABLE_SORT_DESCEND], TBL_BATCH);
922
923
		$rows = mapi_table_queryallrows($table, [PR_SUBJECT, PR_ENTRYID, PR_MESSAGE_DELIVERY_TIME, PR_CLIENT_SUBMIT_TIME], $restrict);
924
925
		return !empty($rows);
926
	}
927
928
	/**
929
	 * Helper functions which extracts the errors from openssl_error_string()
930
	 * Example error from openssl_error_string(): error:21075075:PKCS7 routines:PKCS7_verify:certificate verify error
931
	 * 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.
932
	 *
933
	 * @return {String}
0 ignored issues
show
Documentation Bug introduced by
The doc comment {String} at position 0 could not be parsed: Unknown type name '{' at position 0 in {String}.
Loading history...
934
	 */
935
	public function extract_openssl_error() {
936
		// TODO: should catch more errors by using while($error = @openssl_error_string())
937
		$this->openssl_error = @openssl_error_string();
938
		$openssl_error_code = 0;
939
		if ($this->openssl_error) {
940
			$openssl_error_list = explode(":", $this->openssl_error);
941
			$openssl_error_code = $openssl_error_list[1];
942
		}
943
944
		return $openssl_error_code;
945
	}
946
947
	/**
948
	 * Extract the intermediate certificates from the signed email.
949
	 * Uses openssl_pkcs7_verify to extract the PKCS#7 blob and then converts the PKCS#7 blob to
950
	 * X509 certificates using openssl_pkcs7_read.
951
	 *
952
	 * @param string $emlfile - the s/mime message
953
	 *
954
	 * @return array a list of extracted intermediate certificates
955
	 */
956
	public function extractCAs($emlfile) {
957
		$cas = [];
958
		$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

958
		$certfile = tempnam(sys_get_temp_dir(), /** @scrutinizer ignore-type */ true);
Loading history...
959
		$outfile = tempnam(sys_get_temp_dir(), true);
960
		$p7bfile = tempnam(sys_get_temp_dir(), true);
961
		openssl_pkcs7_verify($emlfile, PKCS7_NOVERIFY, $certfile);
962
		openssl_pkcs7_verify($emlfile, PKCS7_NOVERIFY, $certfile, [], $certfile, $outfile, $p7bfile);
963
964
		$p7b = file_get_contents($p7bfile);
965
966
		openssl_pkcs7_read($p7b, $cas);
967
		unlink($certfile);
968
		unlink($outfile);
969
		unlink($p7bfile);
970
971
		return $cas;
972
	}
973
974
	/**
975
	 * Imports certificate in the MAPI Root Associated Folder.
976
	 *
977
	 * Private key, always insert certificate
978
	 * Public key, check if we already have one stored
979
	 *
980
	 * @param string $cert     certificate body as a string
981
	 * @param mixed  $certData an array with the parsed certificate data
982
	 * @param string $type     certificate type, default 'public'
983
	 * @param bool   $force    force import the certificate even though we have one already stored in the MAPI Store.
984
	 *                         FIXME: remove $force in the future and move the check for newer certificate in this function.
985
	 */
986
	public function importCertificate($cert, $certData, $type = 'public', $force = false) {
987
		$certEmail = getCertEmail($certData);
988
		if ($this->pubcertExists($certEmail) && !$force && $type !== 'private')
989
			return;
990
		$issued_by = "";
991
		foreach (array_keys($certData['issuer']) as $key) {
992
			$issued_by .= $key . '=' . $certData['issuer'][$key] . "\n";
993
		}
994
995
		$root = mapi_msgstore_openentry($this->getStore(), null);
996
		$assocMessage = mapi_folder_createmessage($root, MAPI_ASSOCIATED);
997
		// TODO: write these properties down.
998
		mapi_setprops($assocMessage, [
999
			PR_SUBJECT => $certEmail,
1000
			PR_MESSAGE_CLASS => $type == 'public' ? 'WebApp.Security.Public' : 'WebApp.Security.Private',
1001
			PR_MESSAGE_DELIVERY_TIME => $certData['validTo_time_t'],
1002
			PR_CLIENT_SUBMIT_TIME => $certData['validFrom_time_t'],
1003
			PR_SENDER_NAME => $certData['serialNumber'], // serial
1004
			PR_SENDER_EMAIL_ADDRESS => $issued_by, // Issuer To
1005
			PR_SUBJECT_PREFIX => '',
1006
			PR_RECEIVED_BY_NAME => $this->fingerprint_cert($cert, 'sha1'), // SHA1 Fingerprint
1007
			PR_INTERNET_MESSAGE_ID => $this->fingerprint_cert($cert), // MD5 FingerPrint
1008
		]);
1009
		// Save attachment
1010
		$msgBody = base64_encode($cert);
1011
		$stream = mapi_openproperty($assocMessage, PR_BODY, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY);
1012
		mapi_stream_setsize($stream, strlen($msgBody));
1013
		mapi_stream_write($stream, $msgBody);
1014
		mapi_stream_commit($stream);
1015
		mapi_message_savechanges($assocMessage);
1016
	}
1017
1018
	/**
1019
	 * Function which returns the fingerprint (hash) of the certificate.
1020
	 *
1021
	 * @param {string} $cert certificate body as a string
0 ignored issues
show
Documentation Bug introduced by
The doc comment {string} at position 0 could not be parsed: Unknown type name '{' at position 0 in {string}.
Loading history...
1022
	 * @param {string} $hash optional hash algorithm
1023
	 * @param mixed $body
1024
	 */
1025
	public function fingerprint_cert($body, $hash = 'md5') {
1026
		// TODO: Note for PHP > 5.6 we can use openssl_x509_fingerprint
1027
		$body = str_replace('-----BEGIN CERTIFICATE-----', '', $body);
1028
		$body = str_replace('-----END CERTIFICATE-----', '', $body);
1029
		$body = base64_decode($body);
1030
1031
		if ($hash === 'sha1') {
1032
			$fingerprint = sha1($body);
1033
		}
1034
		else {
1035
			$fingerprint = md5($body);
1036
		}
1037
1038
		// Format 1000AB as 10:00:AB
1039
		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

1039
		return strtoupper(implode(':', /** @scrutinizer ignore-type */ str_split($fingerprint, 2)));
Loading history...
1040
	}
1041
1042
	/**
1043
	 * Retrieve the GAB User.
1044
	 *
1045
	 * FIXME: ideally this would be a public function in grommunio Web.
1046
	 *
1047
	 * @param string $email the email address of the user
1048
	 *
1049
	 * @return mixed $user boolean if false else MAPIObject
1050
	 */
1051
	public function getGABUser($email) {
1052
		$addrbook = $GLOBALS["mapisession"]->getAddressbook();
1053
		$userArr = [[PR_DISPLAY_NAME => $email]];
1054
		$user = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $user is dead and can be removed.
Loading history...
1055
1056
		try {
1057
			$user = mapi_ab_resolvename($addrbook, $userArr, EMS_AB_ADDRESS_LOOKUP);
1058
			$user = mapi_ab_openentry($addrbook, $user[0][PR_ENTRYID]);
1059
		}
1060
		catch (MAPIException $e) {
1061
			$e->setHandled();
1062
		}
1063
1064
		return $user;
1065
	}
1066
1067
	/**
1068
	 * Retrieve the PR_EMS_AB_X509_CERT.
1069
	 *
1070
	 * @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...
1071
	 *
1072
	 * @return string $cert the certificate, empty if not found
1073
	 */
1074
	public function getGABCert($user) {
1075
		$cert = '';
1076
		$userCertArray = mapi_getprops($user, [PR_EMS_AB_X509_CERT]);
1077
		if (isset($userCertArray[PR_EMS_AB_X509_CERT])) {
1078
			$cert = der2pem($userCertArray[PR_EMS_AB_X509_CERT][0]);
1079
		}
1080
1081
		return $cert;
1082
	}
1083
1084
	/**
1085
	 * Called when the core Settings class is initialized and ready to accept sysadmin default
1086
	 * settings. Registers the sysadmin defaults for the example plugin.
1087
	 *
1088
	 * @param {mixed} $data Reference to the data of the triggered hook
0 ignored issues
show
Documentation Bug introduced by
The doc comment {mixed} at position 0 could not be parsed: Unknown type name '{' at position 0 in {mixed}.
Loading history...
1089
	 */
1090
	public function onBeforeSettingsInit(&$data) {
1091
		$data['settingsObj']->addSysAdminDefaults([
1092
			'zarafa' => [
1093
				'v1' => [
1094
					'plugins' => [
1095
						'smime' => [
1096
							'enable' => defined('PLUGIN_SMIME_USER_DEFAULT_ENABLE_SMIME') && PLUGIN_SMIME_USER_DEFAULT_ENABLE_SMIME,
1097
							'passphrase_cache' => defined('PLUGIN_SMIME_PASSPHRASE_REMEMBER_BROWSER') && PLUGIN_SMIME_PASSPHRASE_REMEMBER_BROWSER,
1098
						],
1099
					],
1100
				],
1101
			],
1102
		]);
1103
	}
1104
1105
	/**
1106
	 * Get sender structure of the MAPI Message.
1107
	 *
1108
	 * @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...
1109
	 *
1110
	 * @return array with properties
1111
	 */
1112
	public function getSenderAddress($mapiMessage) {
1113
		if (method_exists($GLOBALS['operations'], 'getSenderAddress'))
1114
			return $GLOBALS["operations"]->getSenderAddress($mapiMessage);
1115
1116
		$messageProps = mapi_getprops($mapiMessage, [PR_SENT_REPRESENTING_ENTRYID, PR_SENDER_ENTRYID]);
1117
		$senderEntryID = isset($messageProps[PR_SENT_REPRESENTING_ENTRYID]) ? $messageProps[PR_SENT_REPRESENTING_ENTRYID] : $messageProps[PR_SENDER_ENTRYID];
1118
1119
		try {
1120
			$senderUser = mapi_ab_openentry($GLOBALS["mapisession"]->getAddressbook(), $senderEntryID);
1121
			if ($senderUser) {
1122
				$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]);
1123
1124
				$senderStructure = [];
1125
				$senderStructure["props"]['entryid'] = bin2hex($userprops[PR_ENTRYID]);
1126
				$senderStructure["props"]['display_name'] = isset($userprops[PR_DISPLAY_NAME]) ? $userprops[PR_DISPLAY_NAME] : '';
1127
				$senderStructure["props"]['email_address'] = isset($userprops[PR_EMAIL_ADDRESS]) ? $userprops[PR_EMAIL_ADDRESS] : '';
1128
				$senderStructure["props"]['smtp_address'] = isset($userprops[PR_SMTP_ADDRESS]) ? $userprops[PR_SMTP_ADDRESS] : '';
1129
				$senderStructure["props"]['address_type'] = isset($userprops[PR_ADDRTYPE]) ? $userprops[PR_ADDRTYPE] : '';
1130
				$senderStructure["props"]['object_type'] = $userprops[PR_OBJECT_TYPE];
1131
				$senderStructure["props"]['recipient_type'] = MAPI_TO;
1132
				$senderStructure["props"]['display_type'] = isset($userprops[PR_DISPLAY_TYPE]) ? $userprops[PR_DISPLAY_TYPE] : MAPI_MAILUSER;
1133
				$senderStructure["props"]['display_type_ex'] = isset($userprops[PR_DISPLAY_TYPE_EX]) ? $userprops[PR_DISPLAY_TYPE_EX] : MAPI_MAILUSER;
1134
			}
1135
		}
1136
		catch (MAPIException $e) {
1137
			error_log(sprintf("[smime] getSenderAddress(): Exception %s", $e));
1138
		}
1139
1140
		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...
1141
	}
1142
}
1143