grommunio /
grommunio-web
| 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
|
|||||
| 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
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
|
|||||
| 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
|
|||||
| 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 |
This check looks for parameters that have been defined for a function or method, but which are not used in the method body.