623 lines
20 KiB
PHP
623 lines
20 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* SPDX-FileCopyrightText: 2022 F7cloud GmbH and F7cloud contributors
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
|
|
namespace OCA\Mail\Service;
|
|
|
|
use Exception;
|
|
use Horde_Imap_Client;
|
|
use Horde_Imap_Client_Data_Fetch;
|
|
use Horde_Imap_Client_Fetch_Query;
|
|
use Horde_Mail_Rfc822_Address;
|
|
use Horde_Mime_Exception;
|
|
use Horde_Mime_Headers;
|
|
use Horde_Mime_Headers_ContentParam_ContentType;
|
|
use Horde_Mime_Part;
|
|
use OCA\Mail\AddressList;
|
|
use OCA\Mail\Db\SmimeCertificate;
|
|
use OCA\Mail\Db\SmimeCertificateMapper;
|
|
use OCA\Mail\Exception\ServiceException;
|
|
use OCA\Mail\Exception\SmimeCertificateParserException;
|
|
use OCA\Mail\Exception\SmimeDecryptException;
|
|
use OCA\Mail\Exception\SmimeEncryptException;
|
|
use OCA\Mail\Exception\SmimeSignException;
|
|
use OCA\Mail\Model\EnrichedSmimeCertificate;
|
|
use OCA\Mail\Model\SmimeCertificateInfo;
|
|
use OCA\Mail\Model\SmimeCertificatePurposes;
|
|
use OCA\Mail\Model\SmimeDecryptionResult;
|
|
use OCP\AppFramework\Db\DoesNotExistException;
|
|
use OCP\ICertificateManager;
|
|
use OCP\ITempManager;
|
|
use OCP\Security\ICrypto;
|
|
|
|
class SmimeService {
|
|
private ITempManager $tempManager;
|
|
private ICertificateManager $certificateManager;
|
|
private ICrypto $crypto;
|
|
private SmimeCertificateMapper $certificateMapper;
|
|
|
|
|
|
public function __construct(ITempManager $tempManager,
|
|
ICertificateManager $certificateManager,
|
|
ICrypto $crypto,
|
|
SmimeCertificateMapper $certificateMapper) {
|
|
$this->tempManager = $tempManager;
|
|
$this->certificateManager = $certificateManager;
|
|
$this->crypto = $crypto;
|
|
$this->certificateMapper = $certificateMapper;
|
|
}
|
|
|
|
/**
|
|
* Attempt to verify a message signed with S/MIME.
|
|
*
|
|
* Requires the openssl extension.
|
|
*
|
|
* @param string $message Whole message including all headers and parts as stored on IMAP
|
|
* @return bool
|
|
*/
|
|
public function verifyMessage(string $message): bool {
|
|
// Ideally, we should use the more modern openssl cms module as it is a superset of the
|
|
// smime/pkcs7 module. Unfortunately, it is only supported since php 8.
|
|
// Ref https://www.php.net/manual/en/function.openssl-cms-verify.php
|
|
|
|
$messageTemp = $this->getTemporaryFileOrThrow();
|
|
$messageTempHandle = fopen($messageTemp, 'wb');
|
|
fwrite($messageTempHandle, $message);
|
|
fclose($messageTempHandle);
|
|
/** @psalm-suppress NullArgument */
|
|
$valid = openssl_pkcs7_verify(
|
|
$messageTemp,
|
|
0,
|
|
null,
|
|
[$this->certificateManager->getAbsoluteBundlePath()],
|
|
);
|
|
|
|
if (is_int($valid)) {
|
|
// OpenSSL error
|
|
return false;
|
|
}
|
|
|
|
return $valid;
|
|
}
|
|
|
|
/**
|
|
* Attempt to extract the signed content from a signed S/MIME message.
|
|
* Can be used to extract opaque signed content even if the signature itself can't be verified.
|
|
*
|
|
* Warning: This method does not attempt to verify the signature.
|
|
*
|
|
* Requires the openssl extension.
|
|
*
|
|
* @param string $message Whole message including all headers and parts as stored on IMAP
|
|
* @return string Signed content
|
|
*
|
|
* @throws ServiceException If no signed content can be extracted
|
|
*/
|
|
public function extractSignedContent(string $message): string {
|
|
// Ideally, we should use the more modern openssl cms module as it is a superset of the
|
|
// smime/pkcs7 module. Unfortunately, it is only supported since php 8.
|
|
// Ref https://www.php.net/manual/en/function.openssl-cms-verify.php
|
|
|
|
$verifiedContentTemp = $this->getTemporaryFileOrThrow();
|
|
$messageTemp = $this->getTemporaryFileOrThrow();
|
|
$messageTempHandle = fopen($messageTemp, 'wb');
|
|
fwrite($messageTempHandle, $message);
|
|
fclose($messageTempHandle);
|
|
/** @psalm-suppress NullArgument */
|
|
$valid = openssl_pkcs7_verify(
|
|
$messageTemp,
|
|
PKCS7_NOSIGS | PKCS7_NOVERIFY,
|
|
null,
|
|
[$this->certificateManager->getAbsoluteBundlePath()],
|
|
null,
|
|
$verifiedContentTemp,
|
|
);
|
|
|
|
if (is_int($valid)) {
|
|
// OpenSSL error
|
|
throw new ServiceException('Failed to extract signed content');
|
|
}
|
|
|
|
$verifiedContent = file_get_contents($verifiedContentTemp);
|
|
if ($verifiedContent === false) {
|
|
throw new ServiceException('Could not read back verified content');
|
|
}
|
|
|
|
return $verifiedContent;
|
|
}
|
|
|
|
/**
|
|
* Parse a X509 certificate.
|
|
*
|
|
* @param string $certificate X509 certificate encoded as PEM
|
|
* @return SmimeCertificateInfo Metadata of the certificate
|
|
*
|
|
* @throws SmimeCertificateParserException If the certificate can't be parsed
|
|
*/
|
|
public function parseCertificate(string $certificate): SmimeCertificateInfo {
|
|
// TODO: support parsing email addresses from SANs
|
|
// TODO: support multiple email addresses per certificate
|
|
|
|
$certificateData = openssl_x509_parse($certificate);
|
|
if ($certificateData === false) {
|
|
throw new SmimeCertificateParserException('Could not parse certificate');
|
|
}
|
|
|
|
if (!isset($certificateData['subject']['emailAddress'])
|
|
&& !isset($certificateData['subject']['CN'])) {
|
|
throw new SmimeCertificateParserException('Certificate does not contain an email address');
|
|
}
|
|
|
|
$purposes = new SmimeCertificatePurposes(false, false);
|
|
foreach ($certificateData['purposes'] as $purpose) {
|
|
[$state, $_, $name] = $purpose;
|
|
if ($name === 'smimesign') {
|
|
$purposes->setSign((bool)$state);
|
|
} elseif ($name === 'smimeencrypt') {
|
|
$purposes->setEncrypt((bool)$state);
|
|
}
|
|
}
|
|
|
|
$decryptedCertificateFile = $this->getTemporaryFileOrThrow();
|
|
file_put_contents($decryptedCertificateFile, $certificate);
|
|
|
|
$caBundle = [$this->certificateManager->getAbsoluteBundlePath()];
|
|
return new SmimeCertificateInfo(
|
|
$certificateData['subject']['CN'] ?? null,
|
|
$certificateData['subject']['emailAddress'] ?? $certificateData['subject']['CN'],
|
|
$certificateData['validTo_time_t'],
|
|
$purposes,
|
|
openssl_x509_checkpurpose($certificate, X509_PURPOSE_ANY, $caBundle, $decryptedCertificateFile) === true,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Enrich S/MIME certificate from the database with additional information.
|
|
*
|
|
* @param SmimeCertificate $certificate
|
|
* @return EnrichedSmimeCertificate
|
|
*
|
|
* @throws ServiceException If decrypting the certificate fails
|
|
* @throws SmimeCertificateParserException If parsing the certificate fails
|
|
*/
|
|
public function enrichCertificate(SmimeCertificate $certificate): EnrichedSmimeCertificate {
|
|
try {
|
|
$decryptedCertificate = $this->crypto->decrypt($certificate->getCertificate());
|
|
} catch (Exception $e) {
|
|
throw new ServiceException(
|
|
'Failed to decrypt certificate: ' . $e->getMessage(),
|
|
0,
|
|
$e,
|
|
);
|
|
}
|
|
|
|
return new EnrichedSmimeCertificate(
|
|
$certificate,
|
|
$this->parseCertificate($decryptedCertificate),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if the given private key corresponds to the given certificate.
|
|
*
|
|
* @param string $certificate X509 certificate encoded as PEM
|
|
* @param string $privateKey Private key encoded as PEM
|
|
* @return bool True if the private key matches the certificate, false otherwise or if the private key is protected by a passphrase
|
|
*/
|
|
public function checkPrivateKey(string $certificate, string $privateKey): bool {
|
|
return openssl_x509_check_private_key($certificate, $privateKey);
|
|
}
|
|
|
|
/**
|
|
* Get a single S/MIME certificate by id.
|
|
*
|
|
* @param int $certificateId
|
|
* @param string $userId
|
|
* @return SmimeCertificate
|
|
*
|
|
* @throws DoesNotExistException
|
|
*/
|
|
public function findCertificate(int $certificateId, string $userId): SmimeCertificate {
|
|
return $this->certificateMapper->find($certificateId, $userId);
|
|
}
|
|
|
|
/**
|
|
* Get all S/MIME certificates belonging to an email address.
|
|
*
|
|
* @param string $emailAddress
|
|
* @param string $userId
|
|
* @return SmimeCertificate[]
|
|
*
|
|
* @throws ServiceException If the database query fails
|
|
*/
|
|
public function findCertificatesByEmailAddress(string $emailAddress,
|
|
string $userId): array {
|
|
try {
|
|
return $this->certificateMapper->findAllByEmailAddress($userId, $emailAddress);
|
|
} catch (\OCP\DB\Exception $e) {
|
|
throw new ServiceException(
|
|
'Failed to fetch certificates by email address: ' . $e->getMessage(),
|
|
0,
|
|
$e,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all S/MIME certificates belonging to an address list
|
|
*
|
|
* @param AddressList $addressList
|
|
* @param string $userId
|
|
* @return SmimeCertificate[]
|
|
*
|
|
* @throws ServiceException If the database query fails or converting an email address failed
|
|
*/
|
|
public function findCertificatesByAddressList(AddressList $addressList, string $userId): array {
|
|
$emailAddresses = [];
|
|
|
|
foreach ($addressList->iterate() as $address) {
|
|
try {
|
|
$emailAddress = $address->getEmail();
|
|
} catch (\Exception $e) {
|
|
throw new ServiceException($e->getMessage(), 0, $e);
|
|
}
|
|
|
|
if (!empty($emailAddress)) {
|
|
$emailAddresses[] = $emailAddress;
|
|
}
|
|
}
|
|
|
|
try {
|
|
return $this->certificateMapper->findAllByEmailAddresses($userId, $emailAddresses);
|
|
} catch (\OCP\DB\Exception $e) {
|
|
throw new ServiceException('Failed to fetch certificates by email addresses: ' . $e->getMessage(), 0, $e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find all S/MIME certificates of the given user.
|
|
*
|
|
* @param string $userId
|
|
* @return SmimeCertificate[]
|
|
*
|
|
* @throws ServiceException
|
|
*/
|
|
public function findAllCertificates(string $userId): array {
|
|
return $this->certificateMapper->findAll($userId);
|
|
}
|
|
|
|
/**
|
|
* Delete an S/MIME certificate by its id.
|
|
*
|
|
* @param int $id
|
|
* @param string $userId
|
|
* @return void
|
|
*
|
|
* @throws DoesNotExistException
|
|
*/
|
|
public function deleteCertificate(int $id, string $userId): void {
|
|
$certificate = $this->certificateMapper->find($id, $userId);
|
|
$this->certificateMapper->delete($certificate);
|
|
}
|
|
|
|
/**
|
|
* Store an S/MIME certificate in the database.
|
|
*
|
|
* @param string $userId
|
|
* @param string $certificateData
|
|
* @param ?string $privateKeyData
|
|
* @return SmimeCertificate
|
|
*
|
|
* @throws ServiceException
|
|
*/
|
|
public function createCertificate(string $userId,
|
|
string $certificateData,
|
|
?string $privateKeyData): SmimeCertificate {
|
|
$emailAddress = $this->parseCertificate($certificateData)->getEmailAddress();
|
|
|
|
$certificate = new SmimeCertificate();
|
|
$certificate->setUserId($userId);
|
|
$certificate->setEmailAddress($emailAddress);
|
|
$certificate->setCertificate($this->crypto->encrypt($certificateData));
|
|
if ($privateKeyData !== null) {
|
|
if (!$this->checkPrivateKey($certificateData, $privateKeyData)) {
|
|
throw new ServiceException('Private key does not match certificate or is protected by a passphrase');
|
|
}
|
|
$certificate->setPrivateKey($this->crypto->encrypt($privateKeyData));
|
|
}
|
|
return $this->certificateMapper->insert($certificate);
|
|
}
|
|
|
|
/**
|
|
* Sign a MIME part using the given certificate and private key.
|
|
*
|
|
* @param Horde_Mime_Part $part
|
|
* @param SmimeCertificate $certificate
|
|
* @return Horde_Mime_Part New MIME part containing the signed message and the signature
|
|
*
|
|
* @throws SmimeSignException If signing the message fails
|
|
* @throws ServiceException If decrypting the certificate or private key fails or the private key is missing
|
|
*/
|
|
public function signMimePart(Horde_Mime_Part $part,
|
|
SmimeCertificate $certificate): Horde_Mime_Part {
|
|
if ($certificate->getPrivateKey() === null) {
|
|
throw new ServiceException('Certificate does not have a private key');
|
|
}
|
|
|
|
try {
|
|
$decryptedCertificate = $this->crypto->decrypt($certificate->getCertificate());
|
|
$decryptedKey = $this->crypto->decrypt($certificate->getPrivateKey());
|
|
} catch (Exception $e) {
|
|
throw new ServiceException(
|
|
'Failed to decrypt certificate or private key: ' . $e->getMessage(),
|
|
0,
|
|
$e,
|
|
);
|
|
}
|
|
|
|
$decryptedCertificateFile = $this->getTemporaryFileOrThrow();
|
|
file_put_contents($decryptedCertificateFile, $decryptedCertificate);
|
|
|
|
$inPath = $this->getTemporaryFileOrThrow();
|
|
$outPath = $this->getTemporaryFileOrThrow();
|
|
file_put_contents($inPath, $part->toString([
|
|
'canonical' => true,
|
|
'headers' => true,
|
|
'encode' => Horde_Mime_Part::ENCODE_8BIT,
|
|
]));
|
|
if (!openssl_pkcs7_sign($inPath, $outPath, $decryptedCertificate, $decryptedKey, null, PKCS7_DETACHED | PKCS7_BINARY, $decryptedCertificateFile)) {
|
|
throw new SmimeSignException('Failed to sign MIME part');
|
|
}
|
|
|
|
try {
|
|
$parsedPart = Horde_Mime_Part::parseMessage(file_get_contents($outPath));
|
|
} catch (Horde_Mime_Exception $e) {
|
|
throw new SmimeSignException(
|
|
'Failed to parse signed MIME part: ' . $e->getMessage(),
|
|
0,
|
|
$e,
|
|
);
|
|
}
|
|
|
|
// Not required but makes sense. Otherwise, it's just a generic MIME format message.
|
|
$parsedPart->setContents("This is a cryptographically signed message in MIME format.\n");
|
|
|
|
// Retain signature but replace signed part content with original content
|
|
$parsedPart[1] = $part;
|
|
|
|
return $parsedPart;
|
|
}
|
|
|
|
/**
|
|
* Decrypt full text of a MIME message and verify the signed message within.
|
|
* This method assumes the given mime part text to be encrypted without checking.
|
|
*
|
|
* @param string $mimePartText
|
|
* @param SmimeCertificate $certificate The certificate needs to contain a private key.
|
|
* @return SmimeDecryptionResult Full text of decrypted MIME message and the signature verification status.
|
|
*
|
|
* @throws ServiceException If the given certificate does not have a private key or can't be decrypted
|
|
* @throws SmimeDecryptException If openssl reports an error during decryption
|
|
*/
|
|
public function decryptMimePartText(string $mimePartText,
|
|
SmimeCertificate $certificate): SmimeDecryptionResult {
|
|
if ($certificate->getPrivateKey() === null) {
|
|
throw new ServiceException('Certificate does not have a private key');
|
|
}
|
|
|
|
try {
|
|
$decryptedCertificate = $this->crypto->decrypt($certificate->getCertificate());
|
|
$decryptedKey = $this->crypto->decrypt($certificate->getPrivateKey());
|
|
} catch (Exception $e) {
|
|
throw new ServiceException(
|
|
'Failed to decrypt certificate or private key: ' . $e->getMessage(),
|
|
0,
|
|
$e,
|
|
);
|
|
}
|
|
|
|
$inPath = $this->getTemporaryFileOrThrow();
|
|
$outPath = $this->getTemporaryFileOrThrow();
|
|
file_put_contents($inPath, $mimePartText);
|
|
if (!openssl_pkcs7_decrypt($inPath, $outPath, $decryptedCertificate, $decryptedKey)) {
|
|
throw new SmimeDecryptException('Failed to decrypt MIME part text');
|
|
}
|
|
|
|
$decryptedMessage = file_get_contents($outPath);
|
|
|
|
// Handle smime-type="signed-data" as the content is opaque until verified
|
|
$headers = Horde_Mime_Headers::parseHeaders($decryptedMessage);
|
|
if (!isset($headers['content-type'])) {
|
|
// Has no content-typ header -> can't be a signed message
|
|
return new SmimeDecryptionResult($decryptedMessage, true, false, false);
|
|
}
|
|
/** @var Horde_Mime_Headers_ContentParam_ContentType $contentType */
|
|
$contentType = $headers['content-type'];
|
|
|
|
$contentTypeString = "$contentType->ptype/$contentType->stype";
|
|
|
|
$isSigned = false;
|
|
$signatureIsValid = false;
|
|
if (($contentTypeString === 'application/pkcs7-mime'
|
|
|| $contentTypeString === 'application/x-pkcs7-mime')
|
|
&& isset($contentType['smime-type'])
|
|
&& $contentType['smime-type'] === 'signed-data') {
|
|
// Opaque signed data needs to be extracted and verified
|
|
$isSigned = true;
|
|
try {
|
|
$signatureIsValid = $this->verifyMessage($decryptedMessage);
|
|
$decryptedMessage = $this->extractSignedContent($decryptedMessage);
|
|
} catch (ServiceException $e) {
|
|
throw new ServiceException(
|
|
'Failed to extract nested opaque signed data: ' . $e->getMessage(),
|
|
0,
|
|
$e,
|
|
);
|
|
}
|
|
} elseif ($contentTypeString === 'multipart/signed') {
|
|
// Clear signed data just needs to be verified
|
|
$isSigned = true;
|
|
$signatureIsValid = $this->verifyMessage($decryptedMessage);
|
|
}
|
|
|
|
return new SmimeDecryptionResult(
|
|
$decryptedMessage,
|
|
true,
|
|
$isSigned,
|
|
$signatureIsValid,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Try to decrypt a raw data fetch from horde and verify the signed message within.
|
|
* The fetch needs to contain at least envelope, headerText and fullText.
|
|
* See the addDecryptQueries() method.
|
|
*
|
|
* This method will do nothing to the full text if the message is not encrypted.
|
|
* The verification will also be skipped in that case.
|
|
*
|
|
* @param Horde_Imap_Client_Data_Fetch $message
|
|
* @param string $userId
|
|
* @return SmimeDecryptionResult
|
|
*
|
|
* @throws ServiceException
|
|
*/
|
|
public function decryptDataFetch(Horde_Imap_Client_Data_Fetch $message,
|
|
string $userId): SmimeDecryptionResult {
|
|
$encryptedText = $message->getFullMsg();
|
|
if (!$this->isEncrypted($message)) {
|
|
// Verification of a potential signature is up to the caller because it is trivial
|
|
return SmimeDecryptionResult::fromPlain($encryptedText);
|
|
}
|
|
|
|
$decryptionResult = null;
|
|
$envelope = $message->getEnvelope();
|
|
foreach ($envelope->to as $recipient) {
|
|
/** @var Horde_Mail_Rfc822_Address $recipient */
|
|
$recipientAddress = $recipient->bare_address;
|
|
$certs = $this->findCertificatesByEmailAddress(
|
|
$recipientAddress,
|
|
$userId,
|
|
);
|
|
|
|
foreach ($certs as $cert) {
|
|
try {
|
|
$decryptionResult = $this->decryptMimePartText($encryptedText, $cert);
|
|
} catch (ServiceException|SmimeDecryptException $e) {
|
|
// Certificate probably didn't match -> continue
|
|
// TODO: filter a real decryption error
|
|
// (is hard because openssl doesn't return a proper error code)
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($decryptionResult === null) {
|
|
throw new SmimeDecryptException('Failed to find a suitable S/MIME certificate for decryption');
|
|
}
|
|
|
|
return $decryptionResult;
|
|
}
|
|
|
|
public function addEncryptionCheckQueries(Horde_Imap_Client_Fetch_Query $query,
|
|
bool $peek = true): void {
|
|
if (!$query->contains(Horde_Imap_Client::FETCH_HEADERTEXT)) {
|
|
$query->headerText([
|
|
'peek' => $peek,
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function addDecryptQueries(Horde_Imap_Client_Fetch_Query $query,
|
|
bool $peek = true): void {
|
|
$this->addEncryptionCheckQueries($query, $peek);
|
|
$query->envelope();
|
|
if (!$query->contains(Horde_Imap_Client::FETCH_FULLMSG)) {
|
|
$query->fullText([
|
|
'peek' => $peek,
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function isEncrypted(Horde_Imap_Client_Data_Fetch $message): bool {
|
|
$headers = $message->getHeaderText('0', Horde_Imap_Client_Data_Fetch::HEADER_PARSE);
|
|
if (!isset($headers['content-type'])) {
|
|
return false;
|
|
}
|
|
|
|
/** @var Horde_Mime_Headers_ContentParam_ContentType $contentType */
|
|
$contentType = $headers['content-type'];
|
|
return $contentType->ptype === 'application'
|
|
&& str_ends_with($contentType->stype, 'pkcs7-mime')
|
|
&& isset($contentType['smime-type'])
|
|
&& $contentType['smime-type'] === 'enveloped-data';
|
|
}
|
|
|
|
/**
|
|
* Encrypt a MIME part using the given certificates.
|
|
*
|
|
* @param Horde_Mime_Part $part
|
|
* @param SmimeCertificate[] $certificates
|
|
* @return Horde_Mime_Part New MIME part containing the encrypted message and the signature
|
|
*
|
|
* @throws ServiceException If decrypting the certificates fails
|
|
* @throws SmimeEncryptException If encrypting the message fails
|
|
*/
|
|
public function encryptMimePart(Horde_Mime_Part $part, array $certificates): Horde_Mime_Part {
|
|
try {
|
|
$decryptedCertificates = array_map(fn (SmimeCertificate $certificate) => $this->crypto->decrypt($certificate->getCertificate()), $certificates);
|
|
} catch (Exception $e) {
|
|
throw new ServiceException('Failed to decrypt certificate: ' . $e->getMessage(), 0, $e);
|
|
}
|
|
|
|
$inPath = $this->getTemporaryFileOrThrow();
|
|
$outPath = $this->getTemporaryFileOrThrow();
|
|
file_put_contents($inPath, $part->toString([
|
|
'canonical' => true,
|
|
'headers' => true,
|
|
'encode' => Horde_Mime_Part::ENCODE_8BIT,
|
|
]));
|
|
|
|
/**
|
|
* Content-Type is application/x-pkcs7-mime by default.
|
|
* The flag PKCS7_NOOLDMIMETYPE / 0x400 let openssl use application/pkcs7-mime as Content-Type.
|
|
* PKCS7_NOOLDMIMETYPE is not available as constant in PHP.
|
|
*
|
|
* https://github.com/openssl/openssl/blob/9a2f78e14a67eeaadefc77d05f0778fc9684d26c/include/openssl/pkcs7.h.in#L211
|
|
* https://github.com/php/php-src/blob/51b70e4414b43a571ebd743d752cf4cbd1556eb5/ext/openssl/openssl_arginfo.h#L572-L580
|
|
*/
|
|
if (!openssl_pkcs7_encrypt($inPath, $outPath, $decryptedCertificates, [], 0x400, OPENSSL_CIPHER_AES_128_CBC)) {
|
|
throw new SmimeEncryptException('Failed to encrypt MIME part');
|
|
}
|
|
|
|
try {
|
|
$parsedPart = Horde_Mime_Part::parseMessage(file_get_contents($outPath), [
|
|
'forcemime' => true,
|
|
]);
|
|
} catch (Horde_Mime_Exception $e) {
|
|
throw new SmimeEncryptException('Failed to parse signed MIME part: ' . $e->getMessage(), 0, $e);
|
|
}
|
|
|
|
return $parsedPart;
|
|
}
|
|
|
|
/**
|
|
* Create a temporary file and return the path or throw if it could not be created.
|
|
*
|
|
* @throws ServiceException If the temporary file could not be created
|
|
*/
|
|
private function getTemporaryFileOrThrow(): string {
|
|
$file = $this->tempManager->getTemporaryFile();
|
|
if ($file === false) {
|
|
throw new ServiceException('Failed to create temporary file');
|
|
}
|
|
|
|
return $file;
|
|
}
|
|
}
|