Issues (752)

plugins/smime/php/util.php (4 issues)

1
<?php
2
3
/**
4
 * This file contains functions which are used in plugin.smime.php and class.pluginsmimemodule.php and therefore
5
 * exists here to avoid code-duplication.
6
 *
7
 * @param mixed $certificate
8
 */
9
10
/**
11
 * Function which extracts the email address from a certificate, and tries to get the subjectAltName if
12
 * subject/emailAddress is not set.
13
 *
14
 * @param mixed $certificate certificate data
15
 */
16
function getCertEmail($certificate) {
17
	$certEmailAddress = "";
18
	// If subject/emailAddress is not set, try subjectAltName
19
	if (isset($certificate['subject']['emailAddress'])) {
20
		$certEmailAddress = $certificate['subject']['emailAddress'];
21
	}
22
	elseif (isset($certificate['extensions'], $certificate['extensions']['subjectAltName'])) {
23
		// Example [subjectAltName] => email:[email protected]
24
		$tmp = explode('email:', $certificate['extensions']['subjectAltName']);
25
		// Only get the first match
26
		if (isset($tmp[1]) && !empty($tmp[1])) {
27
			$certEmailAddress = $tmp[1];
28
		}
29
	}
30
31
	return $certEmailAddress;
32
}
33
34
/**
35
 * Function that will return the private certificate of the user from the user store where it is stored in pkcs#12 format.
36
 *
37
 * @param resource $store        user's store
38
 * @param string   $type         of message_class
39
 * @param string   $emailAddress emailaddress to specify
40
 *
41
 * @return bool|resource the mapi message containing the private certificate, returns false if no certificate is found
42
 */
43
function getMAPICert($store, $type = 'WebApp.Security.Private', $emailAddress = '') {
44
	$root = mapi_msgstore_openentry($store);
45
	$table = mapi_folder_getcontentstable($root, MAPI_ASSOCIATED);
46
47
	$restrict = [RES_PROPERTY,
48
		[
49
			RELOP => RELOP_EQ,
50
			ULPROPTAG => PR_MESSAGE_CLASS,
51
			VALUE => [PR_MESSAGE_CLASS => $type],
52
		],
53
	];
54
	if ($type == 'WebApp.Security.Public' && !empty($emailAddress)) {
55
		$restrict = [RES_AND, [
56
			$restrict,
57
			[RES_CONTENT,
58
				[
59
					FUZZYLEVEL => FL_FULLSTRING | FL_IGNORECASE,
60
					ULPROPTAG => PR_SUBJECT,
61
					VALUE => [PR_SUBJECT => $emailAddress],
62
				],
63
			],
64
		]];
65
	}
66
67
	// PR_MESSAGE_DELIVERY_TIME validTo / PR_CLIENT_SUBMIT_TIME validFrom
68
	mapi_table_restrict($table, $restrict, TBL_BATCH);
69
	mapi_table_sort($table, [PR_MESSAGE_DELIVERY_TIME => TABLE_SORT_DESCEND], TBL_BATCH);
70
71
	$privateCerts = mapi_table_queryallrows($table, [PR_ENTRYID, PR_SUBJECT, PR_MESSAGE_DELIVERY_TIME, PR_CLIENT_SUBMIT_TIME], $restrict);
72
73
	if ($privateCerts && count($privateCerts) > 0) {
74
		return $privateCerts;
75
	}
76
77
	return false;
78
}
79
80
/**
81
 * Function that will decrypt the private certificate using a supplied password
82
 * If multiple private certificates can be decrypted with the supplied password,
83
 * all of them will be returned, if $singleCert == false, otherwise only the first one.
84
 *
85
 * @param resource $store      user's store
86
 * @param string   $passphrase passphrase for private certificate
87
 * @param bool     $singleCert if true, returns the first certificate, which was successfully decrypted with $passphrase
88
 *
89
 * @return mixed collection of certificates, empty if none if decrypting fails or stored private certificate isn't found
90
 */
91
function readPrivateCert($store, $passphrase, $singleCert = true) {
92
	$unlockedCerts = [];
93
	// Get all private certificates saved in the store
94
	$privateCerts = getMAPICert($store);
95
	if ($singleCert) {
96
		$privateCerts = [$privateCerts[0]];
97
	}
98
99
	// Get messages from certificates
100
	foreach ($privateCerts as $privateCert) {
101
		$privateCertMessage = mapi_msgstore_openentry($store, $privateCert[PR_ENTRYID]);
102
		if ($privateCertMessage === false) {
103
			continue;
104
		}
105
		$pkcs12 = "";
106
		$certs = [];
107
		// Read pkcs12 cert from message
108
		$stream = mapi_openproperty($privateCertMessage, PR_BODY, IID_IStream, 0, 0);
109
		$stat = mapi_stream_stat($stream);
110
		mapi_stream_seek($stream, 0, STREAM_SEEK_SET);
111
		for ($i = 0; $i < $stat['cb']; $i += 1024) {
112
			$pkcs12 .= mapi_stream_read($stream, 1024);
113
		}
114
		$ok = openssl_pkcs12_read(base64_decode($pkcs12), $certs, $passphrase);
115
		if ($ok !== false) {
116
			array_push($unlockedCerts, $certs);
117
		}
118
	}
119
120
	return ($singleCert !== false && count($unlockedCerts) > 0) ? $unlockedCerts[0] : $unlockedCerts;
121
}
122
123
/**
124
 * Converts X509 DER format string to PEM format.
125
 *
126
 * @param string X509 Certificate in DER format
0 ignored issues
show
The type X509 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...
127
 * @param mixed $certificate
128
 *
129
 * @return string X509 Certificate in PEM format
130
 */
131
function der2pem($certificate) {
132
	return "-----BEGIN CERTIFICATE-----\n" . chunk_split(base64_encode((string) $certificate), 64, "\n") . "-----END CERTIFICATE-----\n";
133
}
134
135
/**
136
 * Function which does an OCSP/CRL check on the certificate to find out if it has been
137
 * revoked.
138
 *
139
 * For an OCSP request we need the following items:
140
 * - Client certificate which we need to verify
141
 * - Issuer certificate (Authority Information Access: Ca Issuers) openssl x509 -in certificate.crt -text
142
 * - OCSP URL (Authority Information Access: OCSP Url)
143
 *
144
 * The issuer certificate is fetched once and stored in /var/lib/grommunio-web/tmp/smime
145
 * We create the directory if it does not exists, check if the certificate is already stored. If it is already
146
 * stored we, use stat() to determine if it is not very old (> 1 Month) and otherwise fetch the certificate and store it.
147
 *
148
 * @param string $certificate
149
 * @param array  $extracerts  an array of intermediate certificates
150
 * @param mixed  $message
151
 *
152
 * @return bool true is OCSP verification has succeeded or when there is no OCSP support, false if it hasn't
153
 */
154
function verifyOCSP($certificate, $extracerts, &$message) {
155
	if (!PLUGIN_SMIME_ENABLE_OCSP) {
156
		$message['success'] = SMIME_STATUS_SUCCESS;
157
		$message['info'] = SMIME_OCSP_DISABLED;
158
159
		return true;
160
	}
161
162
	$pubcert = new Certificate($certificate);
163
164
	/*
165
	 * Walk over the provided extra intermediate certificates and setup the issuer
166
	 * chain.
167
	 */
168
	$parent = $pubcert;
169
	if (!isset($extracerts) || !is_array($extracerts)) {
170
		$extracerts = [];
171
	}
172
	while ($cert = array_shift($extracerts)) {
173
		$cert = new Certificate($cert);
174
175
		if ($cert->getName() === $pubcert->getName()) {
176
			continue;
177
		}
178
179
		if ($cert->getName() === $parent->getIssuerName()) {
180
			$parent->setIssuer($cert);
181
			$parent = $cert;
182
		}
183
	}
184
185
	try {
186
		$pubcert->verify();
187
		$issuer = $pubcert->issuer();
188
		if ($issuer->issuer()) {
189
			$issuer->verify();
190
		}
191
	}
192
	catch (OCSPException $e) {
193
		if ($e->getCode() === OCSP_CERT_STATUS && $e->getCertStatus() == OCSP_CERT_STATUS_REVOKED) {
0 ignored issues
show
Are you sure the usage of $e->getCertStatus() targeting OCSPException::getCertStatus() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
194
			$message['info'] = SMIME_REVOKED;
195
			$message['success'] = SMIME_STATUS_PARTIAL;
196
197
			return false;
198
		}
199
		error_log(sprintf("[SMIME] OCSP verification warning: '%s'", $e->getMessage()));
200
	}
201
202
	// Certificate does not support OCSP
203
	$message['info'] = SMIME_SUCCESS;
204
	$message['success'] = SMIME_STATUS_SUCCESS;
205
206
	return true;
207
}
208
209
/* Validate the certificate of a user, set an error message.
210
 *
211
 * @param string $certificate the pkcs#12 cert
212
 * @param string $passphrase the pkcs#12 passphrase
213
 * @param string $emailAddres the users email address (must match certificate email)
214
 */
215
function validateUploadedPKCS($certificate, $passphrase, $emailAddress) {
216
	if (!openssl_pkcs12_read($certificate, $certs, $passphrase)) {
217
		return [_('Unable to decrypt certificate'), '', ''];
218
	}
219
220
	$message = '';
221
	$data = [];
222
	$privatekey = $certs['pkey'];
223
	$publickey = $certs['cert'];
224
	$extracerts = $certs['extracerts'] ?? [];
225
	$publickeyData = openssl_x509_parse($publickey);
226
	$imported = false;
227
228
	if ($publickeyData) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $publickeyData of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
229
		$certEmailAddress = getCertEmail($publickeyData);
230
		$validFrom = $publickeyData['validFrom_time_t'];
231
		$validTo = $publickeyData['validTo_time_t'];
232
233
		// Check priv key for signing capabilities
234
		if (!openssl_x509_checkpurpose($privatekey, X509_PURPOSE_SMIME_SIGN)) {
235
			$message = _('Private key can\'t be used to sign email');
236
		}
237
		// Check if the certificate owner matches the grommunio Web users email address
238
		elseif (strcasecmp((string) $certEmailAddress, (string) $emailAddress) !== 0) {
239
			$message = _('Certificate email address doesn\'t match grommunio Web account ') . $certEmailAddress;
240
		}
241
		// Check if certificate is not expired, still import the certificate since a user wants to decrypt his old email
242
		elseif ($validTo < time()) {
243
			$message = _('Certificate was expired on ') . date('Y-m-d', $validTo) . '. ' . _('Certificate was imported.');
244
			$imported = true;
245
		}
246
		// Check if the certificate is validFrom date is not in the future
247
		elseif ($validFrom > time()) {
248
			$message = _('Certificate is not yet valid ') . date('Y-m-d', $validFrom) . '. ' . _('Certificate has not been imported');
249
		}
250
		// We allow users to import private certificate which have no OCSP support
251
		elseif (!verifyOCSP($certs['cert'], $extracerts, $data)) {
252
			$message = _('Certificate is revoked, but was imported.');
253
			$imported = true;
254
		}
255
		else {
256
			$imported = true;
257
			$message = _('Certificate was imported.');
258
		}
259
	}
260
	else { // Can't parse public certificate pkcs#12 file might be corrupt
261
		$message = _('Unable to read public certificate');
262
	}
263
264
	return [$message, $publickey, $publickeyData, $imported];
265
}
266
267
/**
268
 * Detect if the encryptionstore has a third parameter which sets the expiration.
269
 *
270
 * @return {boolean} true is expiration is supported
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...
271
 */
272
function encryptionStoreExpirationSupport() {
273
	$refClass = new ReflectionClass('EncryptionStore');
274
275
	return count($refClass->getMethod('add')->getParameters()) === 3;
276
}
277
278
/**
279
 * Open PHP session if it not open closed. Returns if the session was opened.
280
 *
281
 * @param mixed $func
282
 * @param mixed $sessionOpened
283
 */
284
function withPHPSession($func, $sessionOpened = false) {
285
	if (session_status() === PHP_SESSION_NONE) {
286
		session_start();
287
		$sessionOpened = true;
288
	}
289
290
	$func();
291
292
	if ($sessionOpened) {
293
		session_write_close();
294
	}
295
}
296