Issues (752)

plugins/smime/php/class.pluginsmimemodule.php (4 issues)

1
<?php
2
3
include_once 'util.php';
4
5
define('CHANGE_PASSPHRASE_SUCCESS', 1);
6
define('CHANGE_PASSPHRASE_ERROR', 2);
7
define('CHANGE_PASSPHRASE_WRONG', 3);
8
9
class PluginSmimeModule extends Module {
10
	private $store;
11
12
	/**
13
	 * Constructor.
14
	 *
15
	 * @param int   $id   unique id
16
	 * @param array $data list of all actions
17
	 */
18
	public function __construct($id, $data) {
19
		$this->store = $GLOBALS['mapisession']->getDefaultMessageStore();
20
		parent::__construct($id, $data);
21
	}
22
23
	/**
24
	 * Executes all the actions in the $data variable.
25
	 *
26
	 * @return bool true on success or false on failure
27
	 */
28
	#[Override]
29
	public function execute() {
30
		foreach ($this->data as $actionType => $actionData) {
31
			try {
32
				if (!isset($actionType)) {
33
					continue;
34
				}
35
36
				switch ($actionType) {
37
					case 'certificate':
38
						$data = $this->verifyCertificate($actionData);
39
						$response = [
40
							'type' => 3,
41
							'status' => $data['status'],
42
							'message' => $data['message'],
43
							'data' => $data['data'],
44
						];
45
						$this->addActionData('certificate', $response);
46
						$GLOBALS['bus']->addData($this->getResponseData());
47
						break;
48
49
					case 'passphrase':
50
						$data = $this->verifyPassphrase($actionData);
51
						$response = [
52
							'type' => 3,
53
							'status' => $data['status'],
54
						];
55
						$this->addActionData('passphrase', $response);
56
						$GLOBALS['bus']->addData($this->getResponseData());
57
						break;
58
59
					case 'changepassphrase':
60
						$data = $this->changePassphrase($actionData);
61
						if ($data === CHANGE_PASSPHRASE_SUCCESS) {
62
							// Reset cached passphrase.
63
							$encryptionStore = EncryptionStore::getInstance();
64
							withPHPSession(function () use ($encryptionStore) {
65
								$encryptionStore->add('smime', '');
66
							});
67
						}
68
						$response = [
69
							'type' => 3,
70
							'code' => $data,
71
						];
72
						$this->addActionData('changepassphrase', $response);
73
						$GLOBALS['bus']->addData($this->getResponseData());
74
						break;
75
76
					case 'list':
77
						$data = $this->getPublicCertificates();
78
						$this->addActionData('list', $data);
79
						$GLOBALS['bus']->addData($this->getResponseData());
80
						break;
81
82
					case 'delete':
83
						// FIXME: handle multiple deletes? Separate function?
84
						$entryid = $actionData['entryid'];
85
						$root = mapi_msgstore_openentry($this->store);
86
						mapi_folder_deletemessages($root, [hex2bin((string) $entryid)]);
87
88
						$this->sendFeedback(true);
89
						break;
90
91
					default:
92
						$this->handleUnknownActionType($actionType);
93
				}
94
			}
95
			catch (Exception $e) {
96
				$this->sendFeedback(false, parent::errorDetailsFromException($e));
97
			}
98
		}
99
	}
100
101
	/**
102
	 * Verifies the users private certificate,
103
	 * returns array with three statuses and a message key containing a message for the user.
104
	 * 1. There is a certificate and valid
105
	 * 2. There is a certificate and not valid
106
	 * 3. No certificate
107
	 * FIXME: in the future we might support multiple private certs.
108
	 *
109
	 * @param array $data which contains the data send from JavaScript
110
	 *
111
	 * @return array $data which returns two keys containing the certificate
112
	 */
113
	public function verifyCertificate($data) {
0 ignored issues
show
The parameter $data is not used and could be removed. ( Ignorable by Annotation )

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

113
	public function verifyCertificate(/** @scrutinizer ignore-unused */ $data) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
114
		$message = '';
115
		$status = false;
116
117
		$privateCerts = getMAPICert($this->store);
118
		$certIdx = -1;
119
120
		// No certificates
121
		if (!$privateCerts || count($privateCerts) === 0) {
0 ignored issues
show
$privateCerts of type resource|true is incompatible with the type Countable|array expected by parameter $value of count(). ( Ignorable by Annotation )

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

121
		if (!$privateCerts || count(/** @scrutinizer ignore-type */ $privateCerts) === 0) {
Loading history...
122
			$message = _('No certificate available');
123
		}
124
		else {
125
			// For each certificate in MAPI store
126
			$smtpAddress = $GLOBALS['mapisession']->getSMTPAddress();
127
			for ($i = 0, $cnt = count($privateCerts); $i < $cnt; ++$i) {
128
				// Check if certificate is still valid
129
				// TODO: create a more generic function which verifies if the certificate is valid
130
				// And remove possible duplication from plugin.smime.php->onUploadCertificate
131
				if ($privateCerts[$i][PR_MESSAGE_DELIVERY_TIME] < time()) { // validTo
132
					$message = _('Private certificate has expired, unable to sign email');
133
				}
134
				elseif ($privateCerts[$i][PR_CLIENT_SUBMIT_TIME] >= time()) { // validFrom
135
					$message = _('Private certificate is not valid yet, unable to sign email');
136
				}
137
				elseif (strcasecmp((string) $privateCerts[$i][PR_SUBJECT], (string) $smtpAddress) !== 0) {
138
					$message = _('Private certificate does not match email address');
139
				}
140
				else {
141
					$status = true;
142
					$message = '';
143
					$certIdx = $i;
144
				}
145
			}
146
		}
147
148
		return [
149
			'message' => $message,
150
			'status' => $status,
151
			'data' => [
152
				'validto' => $privateCerts[$certIdx][PR_MESSAGE_DELIVERY_TIME] ?? '',
153
				'validFrom' => $privateCerts[$certIdx][PR_CLIENT_SUBMIT_TIME] ?? '',
154
				'subject' => $privateCerts[$certIdx][PR_SUBJECT] ?? 'Unknown',
155
			],
156
		];
157
	}
158
159
	/**
160
	 * Verify if the supplied passphrase unlocks the private certificate stored in the mapi
161
	 * userstore.
162
	 *
163
	 * @param array $data which contains the data send from JavaScript
164
	 *
165
	 * @return array $data which contains a key 'stats'
166
	 */
167
	public function verifyPassphrase($data) {
168
		$result = readPrivateCert($this->store, $data['passphrase']);
169
170
		if ($result) {
171
			$encryptionStore = EncryptionStore::getInstance();
172
			if (encryptionStoreExpirationSupport()) {
173
				$encryptionStore->add('smime', $data['passphrase'], time() + (5 * 60));
174
			}
175
			else {
176
				withPHPSession(function () use ($encryptionStore, $data) {
177
					$encryptionStore->add('smime', $data['passphrase']);
178
				});
179
			}
180
			$result = true;
181
		}
182
		else {
183
			$result = false;
184
		}
185
186
		return [
187
			'status' => $result,
188
		];
189
	}
190
191
	/**
192
	 * Returns data for the JavaScript CertificateStore 'list' call.
193
	 *
194
	 * @return array $data which contains a list of public certificates
195
	 */
196
	public function getPublicCertificates() {
197
		$items = [];
198
		$data['page'] = [];
0 ignored issues
show
Comprehensibility Best Practice introduced by
$data was never initialized. Although not strictly required by PHP, it is generally a good practice to add $data = array(); before regardless.
Loading history...
199
200
		$root = mapi_msgstore_openentry($this->store);
201
		$table = mapi_folder_getcontentstable($root, MAPI_ASSOCIATED);
202
203
		// restriction for public/private certificates which are stored in the root associated folder
204
		$restrict = [RES_OR, [
205
			[RES_PROPERTY,
206
				[
207
					RELOP => RELOP_EQ,
208
					ULPROPTAG => PR_MESSAGE_CLASS,
209
					VALUE => [PR_MESSAGE_CLASS => "WebApp.Security.Public"],
210
				],
211
			],
212
			[RES_PROPERTY,
213
				[
214
					RELOP => RELOP_EQ,
215
					ULPROPTAG => PR_MESSAGE_CLASS,
216
					VALUE => [PR_MESSAGE_CLASS => "WebApp.Security.Private"],
217
				],
218
			], ],
219
		];
220
		mapi_table_restrict($table, $restrict, TBL_BATCH);
221
		mapi_table_sort($table, [PR_MESSAGE_DELIVERY_TIME => TABLE_SORT_DESCEND], TBL_BATCH);
222
		$certs = mapi_table_queryallrows($table, [PR_SUBJECT, PR_ENTRYID, PR_MESSAGE_DELIVERY_TIME, PR_CLIENT_SUBMIT_TIME, PR_MESSAGE_CLASS, PR_SENDER_NAME, PR_SENDER_EMAIL_ADDRESS, PR_SUBJECT_PREFIX, PR_RECEIVED_BY_NAME, PR_INTERNET_MESSAGE_ID], $restrict);
223
		foreach ($certs as $cert) {
224
			$item = [];
225
			$item['entryid'] = bin2hex((string) $cert[PR_ENTRYID]);
226
			$item['email'] = $cert[PR_SUBJECT];
227
			$item['validto'] = $cert[PR_MESSAGE_DELIVERY_TIME];
228
			$item['validfrom'] = $cert[PR_CLIENT_SUBMIT_TIME];
229
			$item['serial'] = $cert[PR_SENDER_NAME];
230
			$item['issued_by'] = $cert[PR_SENDER_EMAIL_ADDRESS];
231
			$item['issued_to'] = $cert[PR_SUBJECT_PREFIX];
232
			$item['fingerprint_sha1'] = $cert[PR_RECEIVED_BY_NAME];
233
			$item['fingerprint_md5'] = $cert[PR_INTERNET_MESSAGE_ID];
234
			$item['type'] = strtolower((string) $cert[PR_MESSAGE_CLASS]) == 'webapp.security.public' ? 'public' : 'private';
235
			array_push($items, ['props' => $item]);
236
		}
237
		$data['page']['start'] = 0;
238
		$data['page']['rowcount'] = mapi_table_getrowcount($table);
239
		$data['page']['totalrowcount'] = $data['page']['rowcount'];
240
241
		return array_merge($data, ['item' => $items]);
242
	}
243
244
	/*
245
	 * Changes the passphrase of an already stored certificatem by generating
246
	 * a new PKCS12 container.
247
	 *
248
	 * @param Array $actionData contains the passphrase and new passphrase
249
	 * return Number error number
250
	 */
251
	public function changePassphrase($actionData) {
252
		$certs = readPrivateCert($this->store, $actionData['passphrase']);
253
254
		if (empty($certs)) {
255
			return CHANGE_PASSPHRASE_WRONG;
256
		}
257
258
		$cert = $this->pkcs12_change_passphrase($certs, $actionData['new_passphrase']);
259
260
		if ($cert === false) {
0 ignored issues
show
The condition $cert === false is always true.
Loading history...
261
			return CHANGE_PASSPHRASE_ERROR;
262
		}
263
264
		$mapiCerts = getMAPICert($this->store);
265
		$mapiCert = $mapiCerts[0] ?? [];
266
		if (!$mapiCert || empty($mapiCert)) {
267
			return CHANGE_PASSPHRASE_ERROR;
268
		}
269
		$privateCert = mapi_msgstore_openentry($this->store, $mapiCert[PR_ENTRYID]);
270
271
		$msgBody = base64_encode((string) $cert);
272
		$stream = mapi_openproperty($privateCert, PR_BODY, IID_IStream, 0, MAPI_CREATE | MAPI_MODIFY);
273
		mapi_stream_setsize($stream, strlen($msgBody));
274
		mapi_stream_write($stream, $msgBody);
275
		mapi_stream_commit($stream);
276
		mapi_message_savechanges($privateCert);
277
278
		return CHANGE_PASSPHRASE_SUCCESS;
279
	}
280
281
	/**
282
	 * Generate a new  PKCS#12 certificate store file with a new passphrase.
283
	 *
284
	 * @param array $certs          the original certificate
285
	 * @param mixed $new_passphrase
286
	 *
287
	 * @return mixed boolean or string certificate
288
	 */
289
	public function pkcs12_change_passphrase($certs, $new_passphrase) {
290
		$cert = "";
291
		$extracerts = $certs['extracerts'] ?? [];
292
		if (openssl_pkcs12_export($certs['cert'], $cert, $certs['pkey'], $new_passphrase, ['extracerts' => $extracerts])) {
293
			return $cert;
294
		}
295
296
		return false;
297
	}
298
}
299