From 53940e64ab3c89e3a98421f7c6635d8e9a1119aa Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Fri, 16 Feb 2024 01:23:23 +0100 Subject: [PATCH] Added more code for S/MIME #259 --- .../MailSo/Mime/Enumerations/ContentType.php | 27 +++ .../0.0.0/app/libraries/MailSo/Mime/Part.php | 23 +- .../libraries/RainLoop/Actions/Messages.php | 78 ++++++- .../app/libraries/RainLoop/Actions/Pgp.php | 2 +- .../libraries/snappymail/file/temporary.php | 69 ++++++ .../0.0.0/app/libraries/snappymail/smime.php | 199 ++++++++++++++++++ .../libraries/snappymail/smime/openssl.php | 104 +++++++++ 7 files changed, 491 insertions(+), 11 deletions(-) create mode 100644 snappymail/v/0.0.0/app/libraries/MailSo/Mime/Enumerations/ContentType.php create mode 100644 snappymail/v/0.0.0/app/libraries/snappymail/file/temporary.php create mode 100644 snappymail/v/0.0.0/app/libraries/snappymail/smime.php create mode 100644 snappymail/v/0.0.0/app/libraries/snappymail/smime/openssl.php diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Enumerations/ContentType.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Enumerations/ContentType.php new file mode 100644 index 000000000..b2e4b60d6 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Enumerations/ContentType.php @@ -0,0 +1,27 @@ +Headers->AddByName(Enumerations\Header::CONTENT_TYPE, 'multipart/encrypted; protocol="application/pgp-encrypted"'); + $oPart->Headers->AddByName(Enumerations\Header::CONTENT_TYPE, 'multipart/encrypted; protocol="'.$sType.'"'); $this->SubParts->append($oPart); $oSubPart = new self; - $oSubPart->Headers->AddByName(Enumerations\Header::CONTENT_TYPE, 'application/pgp-encrypted'); + $oSubPart->Headers->AddByName(Enumerations\Header::CONTENT_TYPE, $sType); $oSubPart->Headers->AddByName(Enumerations\Header::CONTENT_DISPOSITION, 'attachment'); $oSubPart->Headers->AddByName(Enumerations\Header::CONTENT_TRANSFER_ENCODING, '7Bit'); $oSubPart->Body = \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString('Version: 1'); @@ -207,12 +207,27 @@ class Part $oSubPart = new self; $oSubPart->Headers->AddByName(Enumerations\Header::CONTENT_TYPE, 'application/octet-stream'); - $oSubPart->Headers->AddByName(Enumerations\Header::CONTENT_DISPOSITION, 'inline; filename="msg.asc"'); + if ('application/pgp-encrypted' === $sType) { + $oSubPart->Headers->AddByName(Enumerations\Header::CONTENT_DISPOSITION, 'inline; filename="msg.asc"'); + } + if ('application/pkcs7-mime' === $sType) { + $oSubPart->Headers->AddByName(Enumerations\Header::CONTENT_DISPOSITION, 'inline; filename="msg.p7m"'); + } $oSubPart->Headers->AddByName(Enumerations\Header::CONTENT_TRANSFER_ENCODING, '7Bit'); $oSubPart->Body = \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString($sEncrypted); $oPart->SubParts->append($oSubPart); } + public function addPgpEncrypted(string $sEncrypted) + { + $this->addEncrypted($sEncrypted, 'application/pgp-encrypted'); + } + + public function addSMimepEncrypted(string $sEncrypted) + { + $this->addEncrypted($sEncrypted, 'application/pkcs7-mime'); + } + public function addPlain(string $sPlain) { $oPart = new self; diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php index 501c2cfd3..548346671 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php @@ -976,6 +976,9 @@ trait Messages return $oMessage; } + /** + * called by DoSaveMessage and DoSendMessage + */ private function buildMessage(Account $oAccount, bool $bWithDraftInfo = true) : \MailSo\Mime\Message { $oMessage = new \MailSo\Mime\Message(); @@ -1048,11 +1051,14 @@ trait Messages $aSigned = \explode("\r\n\r\n", $sSigned, 2); // $sBoundary = \preg_replace('/^.+boundary="([^"]+)".+$/Dsi', '$1', $aSigned[0]); $sBoundary = $this->GetActionParam('boundary', ''); +// \preg_match('/protocol="(application/[^"]+)"/', $aSigned[0], $match); +// $sProtocol = $match[1][0]; + $sProtocol = 'application/pgp-signature'; $oPart = new MimePart; $oPart->Headers->AddByName( MimeEnumHeader::CONTENT_TYPE, - 'multipart/signed; micalg="pgp-sha256"; protocol="application/pgp-signature"; boundary="'.$sBoundary.'"' + 'multipart/signed; micalg="pgp-sha256"; protocol="'.$sProtocol.'"; boundary="'.$sBoundary.'"' ); $oPart->Body = $aSigned[1]; $oMessage->SubParts->append($oPart); @@ -1176,8 +1182,9 @@ trait Messages } } - $sFingerprint = $this->GetActionParam('signFingerprint', ''); $sPassphrase = $this->GetActionParam('signPassphrase', ''); + + $sFingerprint = $this->GetActionParam('signFingerprint', ''); if ($sFingerprint) { $GPG = $this->GnuPG(); $oBody = $oMessage->GetRootPart(); @@ -1216,13 +1223,53 @@ trait Messages $oSignaturePart->Headers->AddByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING, '7Bit'); $oSignaturePart->Body = $sSignature; $oPart->SubParts->append($oSignaturePart); + } else { + $sCertificate = $this->GetActionParam('signCertificate', ''); + $sPrivateKey = $this->GetActionParam('signPrivateKey', ''); + if ($sCertificate && $sPrivateKey) { + $oBody = $oMessage->GetRootPart(); + + $resource = $oBody->ToStream(); + \MailSo\Base\StreamFilters\LineEndings::appendTo($resource); + $tmp = new \SnappyMail\File\Temporary; + $tmp->writeFromStream($resource); + + $oBody->Body = null; + $oBody->SubParts->Clear(); + $oMessage->SubParts->Clear(); + $oMessage->Attachments()->Clear(); + + $SMIME = new \SnappyMail\SMime\OpenSSL; + $SMIME->setPrivateKey($sPrivateKey, $sPassphrase); + $sSignature = $SMIME->sign($tmp, $sCertificate); + + if (!$sSignature) { + throw new \Exception('GnuPG sign() failed'); + } + + $oPart = new MimePart; + $oPart->Headers->AddByName( + MimeEnumHeader::CONTENT_TYPE, + 'multipart/signed; micalg="sha-512"; protocol="application/pkcs7-signature"' + ); + $oMessage->SubParts->append($oPart); + + $fp = $tmp->fopen(); + \rewind($fp); + $oBody->Raw = $fp; + $oPart->SubParts->append($oBody); + + $oSignaturePart = new MimePart; + $oSignaturePart->Headers->AddByName(MimeEnumHeader::CONTENT_TYPE, 'application/pkcs7-signature; name="signature.p7s"'); + $oSignaturePart->Headers->AddByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING, '7Bit'); + $oSignaturePart->Body = $sSignature; + $oPart->SubParts->append($oSignaturePart); + } } $aFingerprints = \json_decode($this->GetActionParam('encryptFingerprints', ''), true); if ($aFingerprints) { - $GPG = $this->GnuPG(); - $oBody = $oMessage->GetRootPart(); - $resource = $oBody->ToStream(); + $resource = $oMessage->GetRootPart()->ToStream(); $fp = \fopen('php://temp', 'r+b'); // \stream_copy_to_stream($resource, $fp); // Fails while (!\feof($resource)) \fwrite($fp, \fread($resource, 8192)); @@ -1230,11 +1277,30 @@ trait Messages $oMessage->SubParts->Clear(); $oMessage->Attachments()->Clear(); + $GPG = $this->GnuPG(); foreach ($aFingerprints as $sFingerprint) { $GPG->addEncryptKey($sFingerprint); } - $oMessage->addPgpEncrypted($GPG->encryptStream($fp)); + } else { + $aCertificates = \json_decode($this->GetActionParam('encryptCertificates', ''), true); + if ($aCertificates) { + $tmp = new \SnappyMail\File\Temporary; + $tmp->writeFromStream($oMessage->GetRootPart()->ToStream()); + + $oMessage->SubParts->Clear(); + $oMessage->Attachments()->Clear(); + +// $SMIME = new \SnappyMail\SMime(/*$homedir*/); + $SMIME = new \SnappyMail\SMime\OpenSSL; +/* + foreach ($aCertificates as $sCertificate) { + $SMIME->addEncryptKey($sCertificate); + } +*/ + $sEncrypted = $SMIME->encrypt($tmp, $aCertificates); + $oMessage->addSMimepEncrypted($sEncrypted); + } } $this->Plugins()->RunHook('filter.build-message', array($oMessage)); diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Pgp.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Pgp.php index 0751d1401..9cdc120f4 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Pgp.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Pgp.php @@ -77,7 +77,7 @@ trait Pgp // First try a symbolic link $tmpdir = \sys_get_temp_dir() . '/snappymail'; // if (\RainLoop\Utils::inOpenBasedir($tmpdir) && - is_dir($tmpdir) || \mkdir($tmpdir, 0700); + \is_dir($tmpdir) || \mkdir($tmpdir, 0700); if (\is_dir($tmpdir) && \is_writable($tmpdir)) { $link = $tmpdir . '/' . \md5($homedir); if (\is_link($link) || \symlink($homedir, $link)) { diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/file/temporary.php b/snappymail/v/0.0.0/app/libraries/snappymail/file/temporary.php new file mode 100644 index 000000000..9b6d37fa4 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/file/temporary.php @@ -0,0 +1,69 @@ +filename = \tempnam($tmpdir, $name); + } else { + $this->filename = $tmpdir . '/' . $name; + } + } + + function __destruct() + { + $this->filename && \unlink($this->filename); + } + + function __toString() : string + { + $this->filename; + } + + public function filename() : string + { + return $this->filename; + } + + private $fp = null; + public function fopen()/* : resource|false*/ + { + if (!$fp) { + $fp = \fopen($this->filename, 'r+b'); + } + return $fp; + } + + public function writeFromStream(/*resource*/ $from)/* : int|false*/ + { + $fp = $this->fopen(); +// return \stream_copy_to_stream($from, $fp); // Fails + $bytes = 0; + while (!\feof($from)) $bytes += \fwrite($fp, \fread($from, 8192)); + return $bytes; + } + + public function putContents($data, int $flags = 0)/* : int|false*/ + { + return \file_put_contents($this->filename, $data /*, $flags, $context*/); + } + + public function getContents()/* : string|false*/ + { + return \file_get_contents($this->filename); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/smime.php b/snappymail/v/0.0.0/app/libraries/snappymail/smime.php new file mode 100644 index 000000000..1c9ed5540 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/smime.php @@ -0,0 +1,199 @@ +homedir = $homedir; + } + + public static function isSupported() : bool + { + return SMime\OpenSSL::isSupported() || GPG\SMIME::isSupported(); + } + + private static $instance; + public static function getInstance(string $homedir) : ?self + { + if (!static::$instance) { + static::$instance = new self($homedir); + } + return static::$instance; + } + + public function handler() + { + return $this->OpenSSL ?: $this->GPGSM; + } + + public function getGPGSM(bool $throw = true) : ?GPG\SMIME + { + if (!$this->GPGSM) { + if (GPG\SMIME::isSupported()) { + $this->GPGSM = new GPG\SMIME($this->homedir); + } else if ($throw) { + throw new \Exception('GnuPG not supported'); + } + } + return $this->GPGSM; + } + + public function addDecryptKey(string $fingerprint, + #[\SensitiveParameter] + string $passphrase + ) : bool + { + } + + public function addEncryptKey(string $fingerprint) : bool + { + } + + public function addSignKey(string $fingerprint, + #[\SensitiveParameter] + ?string $passphrase + ) : bool + { + } + + public function clearDecryptKeys() : bool + { + } + + public function clearEncryptKeys() : bool + { + } + + public function clearSignKeys() : bool + { + } + + public function decrypt(string $text) /*: string|false */ + { + } + + public function decryptFile(string $filename) /*: string|false */ + { + } + + public function decryptStream(/*resource*/ $fp, /*string|resource*/ $output = null) /*: string|false */ + { + } + + public function decryptVerify(string $text, string &$plaintext) /*: array|false*/ + { + } + + public function decryptVerifyFile(string $filename, string &$plaintext) /*: array|false*/ + { + } + + public function deleteKey(string $keyId, bool $private) : bool + { + } + + public function encrypt(string $plaintext) /*: string|false*/ + { + } + + public function encryptFile(string $filename) /*: string|false*/ + { + } + + public function encryptStream(/*resource*/ $fp, /*string|resource*/ $output = null) /*: string|false*/ + { + } + + public function export(string $fingerprint, + #[\SensitiveParameter] + string $passphrase = '' + ) /*: string|false*/ + { + } + + public function getEngineInfo() : array + { + } + + public function getError() /*: string|false*/ + { + } + + public function getErrorInfo() : array + { + } + + public function getProtocol() : int + { + } + + public function generateKey(string $uid, + #[\SensitiveParameter] + string $passphrase + ) /*: string|false*/ + { + } + + public function import(string $keydata) /*: array|false*/ + { + } + + public function importFile(string $filename) /*: array|false*/ + { + } + + public function keyInfo(string $pattern) : array + { + } + + public function setArmor(bool $armor = true) : bool + { + } + + public function setErrorMode(int $errormode) : void + { + } + + public function setSignMode(int $signmode) : bool + { + } + + public function sign(string $plaintext) /*: string|false*/ + { + } + + public function signFile(string $filename) /*: string|false*/ + { + } + + public function signStream($fp, /*string|resource*/ $output = null) /*: array|false*/ + { + } + + public function verify(string $signed_text, string $signature, string &$plaintext = null) /*: array|false*/ + { + } + + public function verifyFile(string $filename, string $signature, string &$plaintext = null) /*: array|false*/ + { + } + + public function verifyStream(/*resource*/ $fp, string $signature, string &$plaintext = null) /*: string|false */ + { + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/smime/openssl.php b/snappymail/v/0.0.0/app/libraries/snappymail/smime/openssl.php new file mode 100644 index 000000000..71910e425 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/snappymail/smime/openssl.php @@ -0,0 +1,104 @@ +private_key = \openssl_pkey_get_private($private_key, $passphrase); + } + + public function decrypt(string $data, $certificate = null, $private_key = null) : ?string + { + $input = new Temporary('smimein-'); + $output = new Temporary('smimeout-'); + return ($input->putContents($data) && \openssl_pkcs7_decrypt( + $input->filename(), + $output->filename(), + $certificate ?: $this->certificate, // \openssl_pkey_get_public(); + $private_key ?: $this->private_key // \openssl_pkey_get_private($private_key, ?string $passphrase = null); + )) ? $output->getContents() : null; + } + + public function encrypt(/*string|Temporary*/$input, array $certificates) : ?string + { + if (\is_string($input)) { + $input = new Temporary('smimein-'); + if (!$input->putContents($data)) { + return null; + } + } + $output = new Temporary('smimeout-'); + $flags = \defined('PKCS7_NOOLDMIMETYPE') ? \PKCS7_NOOLDMIMETYPE : 0; + return \openssl_pkcs7_encrypt( + $input->filename(), + $output->filename(), + $certificates, + $this->headers, + $flags, + $this->cipher_algo + ) ? $output->getContents() : null; + } + + public function sign(/*string|Temporary*/$input, $certificate = null, $private_key = null) + { + if (\is_string($input)) { + $input = new Temporary('smimein-'); + if (!$input->putContents($data)) { + return null; + } + } + $output = new Temporary('smimeout-'); + if (!$input->putContents($data) || !\openssl_pkcs7_sign( + $input->filename(), + $output->filename(), + $certificate ?: $this->certificate, // \openssl_pkey_get_public(); + $private_key ?: $this->private_key, // \openssl_pkey_get_private($private_key, ?string $passphrase = null); + $this->headers, + \PKCS7_DETACHED, // | PKCS7_NOCERTS | PKCS7_NOATTR + $this->untrusted_certificates_filename + )) { + return null; + } +/* + $body = $output->getContents(); + // The message returned by openssl contains both headers and body, so need to split them up + $parts = explode("\n\n", $body, 2); + $this->MIMEHeader .= $parts[0] . static::$LE . static::$LE; + $body = $parts[1]; +*/ + return $output->getContents(); + } + + public function verify(string $data, $signers_certificates_filename = null) + { + $input = new Temporary('smimein-'); + return $input->putContents($data) && true === \openssl_pkcs7_verify( + $input->filename(), + $flags = 0, + $signers_certificates_filename ?: null, + $ca_info = [], + $this->untrusted_certificates_filename, + $content = null, + $output_filename = null + ); + } +}