Test Failed
Push — master ( b103a7...4c2e75 )
by
unknown
17:01 queued 05:50
created

Pluginsmime::extractCAs()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 16
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 1
eloc 12
c 3
b 0
f 0
nc 1
nop 1
dl 0
loc 16
rs 9.8666
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
127
					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...
128
				}
129
				if ($data['name'] === 'smime_passphrasecheck') {
130
					// No need to do anything, this is just used to trigger
131
					// the browser's autofill save password dialog.
132
					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...
133
				}
134
				break;
135
		}
136
	}
137
138
	/**
139
	 * Function checks if public certificate exists for all recipients and creates an error
140
	 * message for the frontend which includes the email address of the missing public
141
	 * certificates.
142
	 *
143
	 * If my own certificate is missing, a different error message is shown which informs the
144
	 * user that his own public certificate is missing and required for reading encrypted emails
145
	 * in the 'Sent items' folder.
146
	 *
147
	 * @param array $data Reference to the data of the triggered hook
148
	 */
149
	public function onCertificateCheck($data) {
150
		$entryid = $data['entryid'];
151
		// FIXME: unittests, save trigger will pass $entryid is 0 (which will open the root folder and not the message we want)
152
		if ($entryid === false) {
153
			return;
154
		}
155
156
		if (!isset($data['action']['props']['smime']) || empty($data['action']['props']['smime'])) {
157
			return;
158
		}
159
160
		$message = mapi_msgstore_openentry($data['store'], $entryid);
161
		$module = $data['moduleObject'];
162
		$data['success'] = true;
163
164
		$messageClass = mapi_getprops($message, [PR_MESSAGE_CLASS]);
165
		$messageClass = $messageClass[PR_MESSAGE_CLASS];
166
		if ($messageClass !== 'IPM.Note.SMIME' && $messageClass !== 'IPM.Note.SMIME.SignedEncrypt') {
167
			return;
168
		}
169
170
		$recipients = $data['action']['props']['smime'];
171
		$missingCerts = [];
172
173
		foreach ($recipients as $recipient) {
174
			$email = $recipient['email'];
175
176
			if (!$this->pubcertExists($email, $recipient['internal'])) {
177
				array_push($missingCerts, $email);
178
			}
179
		}
180
181
		if (empty($missingCerts)) {
182
			return;
183
		}
184
185
		function missingMyself($email) {
186
			return $GLOBALS['mapisession']->getSMTPAddress() === $email;
187
		}
188
189
		if (array_filter($missingCerts, "missingMyself") === []) {
190
			$errorMsg = _('Missing public certificates for the following recipients: ') . implode(', ', $missingCerts) . _('. Please contact your system administrator for details');
191
		}
192
		else {
193
			$errorMsg = _("Your public certificate is not installed. Without this certificate, you will not be able to read encrypted messages you have sent to others.");
194
		}
195
196
		$module->sendFeedback(false, ["type" => ERROR_GENERAL, "info" => ['display_message' => $errorMsg]]);
197
		$data['success'] = false;
198
	}
199
200
	/**
201
	 * Function which verifies a message.
202
	 *
203
	 * TODO: Clean up flow
204
	 *
205
	 * @param mixed $message
206
	 * @param mixed $eml
207
	 */
208
	public function verifyMessage($message, $eml) {
209
		$userCert = '';
210
		$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

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

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

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

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

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

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

637
			$tmpSendEmail = tempnam(sys_get_temp_dir(), /** @scrutinizer ignore-type */ true);
Loading history...
638
			$tmpSendSmimeEmail = tempnam(sys_get_temp_dir(), true);
639
640
			// Save message stream to a file
641
			$stat = mapi_stream_stat($emlMessageStream);
642
643
			$fhandle = fopen($tmpSendEmail, 'w');
644
			$buffer = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $buffer is dead and can be removed.
Loading history...
645
			for ($i = 0; $i < $stat["cb"]; $i += BLOCK_SIZE) {
0 ignored issues
show
Bug introduced by
The constant BLOCK_SIZE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
646
				// Write stream
647
				$buffer = mapi_stream_read($emlMessageStream, BLOCK_SIZE);
648
				fwrite($fhandle, $buffer, strlen($buffer));
649
			}
650
			fclose($fhandle);
651
652
			// Create attachment for S/MIME message
653
			$signedAttach = mapi_message_createattach($message);
654
			$smimeProps = [
655
				PR_ATTACH_LONG_FILENAME => 'smime.p7m',
656
				PR_DISPLAY_NAME => 'smime.p7m',
657
				PR_ATTACH_METHOD => ATTACH_BY_VALUE,
658
				PR_ATTACH_MIME_TAG => 'multipart/signed',
659
				PR_ATTACHMENT_HIDDEN => true,
660
			];
661
662
			// Sign then Encrypt email
663
			switch ($messageClass) {
664
				case 'IPM.Note.SMIME.SignedEncrypt':
665
					$tmpFile = tempnam(sys_get_temp_dir(), true);
666
					$this->sign($tmpSendEmail, $tmpFile, $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::sign(). ( Ignorable by Annotation )

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

666
					$this->sign(/** @scrutinizer ignore-type */ $tmpSendEmail, $tmpFile, $message, $signedAttach, $smimeProps);
Loading history...
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

666
					$this->sign($tmpSendEmail, /** @scrutinizer ignore-type */ $tmpFile, $message, $signedAttach, $smimeProps);
Loading history...
667
					$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

667
					$this->encrypt($tmpFile, /** @scrutinizer ignore-type */ $tmpSendSmimeEmail, $message, $signedAttach, $smimeProps);
Loading history...
668
					unlink($tmpFile);
669
					break;
670
671
				case 'IPM.Note.SMIME.MultipartSigned':
672
					$this->sign($tmpSendEmail, $tmpSendSmimeEmail, $message, $signedAttach, $smimeProps);
673
					break;
674
675
				case 'IPM.Note.SMIME':
676
					$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

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

721
			$tmpFile = tempnam(sys_get_temp_dir(), /** @scrutinizer ignore-type */ true);
Loading history...
722
			file_put_contents($tmpFile, implode('', $certs['extracerts']));
723
			$ok = openssl_pkcs7_sign($infile, $outfile, $certs['cert'], [$certs['pkey'], ''], [], PKCS7_DETACHED, $tmpFile);
0 ignored issues
show
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

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

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

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

765
		$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

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

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

768
			Log::Write(LOGLEVEL_ERROR, sprintf("[smime] unable to encrypt message, openssl error: '%s'", /** @scrutinizer ignore-type */ @openssl_error_string()));
Loading history...
769
		}
770
		$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

770
		$tmpEml = file_get_contents(/** @scrutinizer ignore-type */ $outfile);
Loading history...
771
772
		// Grab the base64 data, since MAPI requires it saved as decoded base64 string.
773
		// FIXME: we can do better here
774
		$matches = explode("\n\n", $tmpEml);
775
		$base64 = str_replace("\n", "", $matches[1]);
776
		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

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

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

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