diff --git a/dev/View/Popup/Compose.js b/dev/View/Popup/Compose.js index de5f8f93d..2cdc85ef2 100644 --- a/dev/View/Popup/Compose.js +++ b/dev/View/Popup/Compose.js @@ -253,7 +253,7 @@ export class ComposePopupView extends AbstractViewPopup { doSign: false, doEncrypt: false, - canPgpSign: false, + pgpSignKey: false, canPgpEncrypt: false, canMailvelope: false, @@ -334,7 +334,10 @@ export class ComposePopupView extends AbstractViewPopup { attachmentsInProcessCount: () => this.attachmentsInProcess.length, isDraft: () => this.draftsFolder() && this.draftUid(), - canSign: () => this.canPgpSign() | this.canSMimeSign(), + canSign: () => { + let s = this.canSMimeSign(); + return this.pgpSignKey() || s; + }, canEncrypt: () => this.canPgpEncrypt() | this.canSMimeEncrypt(), identitiesOptions: () => @@ -365,14 +368,14 @@ export class ComposePopupView extends AbstractViewPopup { }, from: value => { - this.canPgpSign(false); + this.pgpSignKey(false); value = getEmail(value); value && PgpUserStore.getKeyForSigning(value).then(result => { console.log({ email: value, - canPgpSign:result + pgpSignKey:result }); - this.canPgpSign(result) + this.pgpSignKey(result) }); this.initPgpEncrypt(); }, @@ -1458,7 +1461,7 @@ export class ComposePopupView extends AbstractViewPopup { linkedData: [] }, recipients = draft ? [identity.email()] : this.allRecipients(), - sign = !draft && this.doSign() && (this.canPgpSign() || this.canSMimeSign()), + sign = !draft && this.doSign() && (this.pgpSignKey() || this.canSMimeSign()), encrypt = this.doEncrypt() && (this.canPgpEncrypt() || this.canSMimeEncrypt()), isHtml = this.oEditor.isHtml(); diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/BodyStructure.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/BodyStructure.php index 1370900bf..d619b13cb 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/BodyStructure.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/BodyStructure.php @@ -157,7 +157,7 @@ class BodyStructure implements \JsonSerializable public function isSMimeEncrypted() : bool { $type = \strtolower(\trim($this->aContentTypeParams['smime-type'] ?? '')); - return 'application/pkcs7-mime' === $this->sContentType + return ContentType::isPkcs7Mime($this->sContentType) && !empty($this->aContentTypeParams['smime-type']) && ('enveloped-data' === $type || 'authenveloped-data' === $type); } @@ -171,7 +171,7 @@ class BodyStructure implements \JsonSerializable // The multipart/signed body MUST consist of exactly two parts. && 2 === \count($this->aSubParts) && ContentType::isPkcs7Signature($this->aSubParts[1]->ContentType()) - ) || ('application/pkcs7-mime' === $this->sContentType + ) || (ContentType::isPkcs7Mime($this->sContentType) && !empty($this->aContentTypeParams['smime-type']) && 'signed-data' === \strtolower(\trim($this->aContentTypeParams['smime-type'])) ); @@ -275,6 +275,13 @@ class BodyStructure implements \JsonSerializable }); } + public function SearchByContentTypes(array $aContentTypes) : iterable + { + return $this->SearchByCallback(function ($oItem) use ($aContentTypes) { + return \in_array($oItem->sContentType, $aContentTypes); + }); + } + public function GetPartByMimeIndex(string $sMimeIndex) : self { $oPart = null; diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php index 25046ed40..a4cc1b5b7 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php @@ -320,7 +320,7 @@ class Message implements \JsonSerializable } } - $gEncryptedParts = $oBodyStructure->SearchByContentType('application/pkcs7-mime'); + $gEncryptedParts = $oBodyStructure->SearchByContentTypes(['application/pkcs7-mime','application/x-pkcs7-mime']); foreach ($gEncryptedParts as $oPart) { if ($oPart->isSMimeEncrypted()) { $oMessage->smimeEncrypted = [ 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 index bb0fb7320..dacc39637 100644 --- 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 @@ -25,6 +25,12 @@ abstract class ContentType const PKCS7_SIGNATURE = 'application/pkcs7-signature'; const PKCS7_MIME = 'application/pkcs7-mime'; + public static function isPkcs7Mime(string $data) : bool + { + return 'application/pkcs7-mime' === $data + || 'application/x-pkcs7-mime' === $data; + } + public static function isPkcs7Signature(string $data) : bool { return 'application/pkcs7-signature' === $data diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Part.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Part.php index 54d737e19..190a146e1 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Part.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Part.php @@ -103,7 +103,7 @@ class Part && 2 === \count($this->SubParts) && ContentType::isPkcs7Signature($this->SubParts[1]->ContentType()) ) || ($header - && \preg_match('#application/pkcs7-mime.+smime-type=["\']?signed-data#si', $header->FullValue()) + && \preg_match('#application/(x-)?pkcs7-mime.+smime-type=["\']?signed-data#si', $header->FullValue()) ); } 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 9e8541077..d06d0aead 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 @@ -1239,31 +1239,43 @@ trait Messages $oMessage->SubParts->Clear(); $oMessage->Attachments()->Clear(); + $detached = true; + $SMIME = new \SnappyMail\SMime\OpenSSL; + $SMIME->setCertificate($sCertificate); $SMIME->setPrivateKey($sPrivateKey, $sPassphrase); - $sSignature = $SMIME->sign($tmp, $sCertificate); + $sSignature = $SMIME->sign($tmp, $detached); if (!$sSignature) { - throw new \Exception('S/MIME sign() failed'); + throw new \RuntimeException('S/MIME sign() failed'); } $oPart = new MimePart; - $oPart->Headers->AddByName( - MimeEnumHeader::CONTENT_TYPE, - 'multipart/signed; micalg="sha-512"; protocol="application/pkcs7-signature"' - ); $oMessage->SubParts->append($oPart); + if ($detached) { + $oPart->Headers->AddByName( + MimeEnumHeader::CONTENT_TYPE, + 'multipart/signed; micalg="sha-256"; protocol="application/pkcs7-signature"' + ); - $fp = $tmp->fopen(); - \rewind($fp); - $oBody->Raw = $fp; - $oPart->SubParts->append($oBody); + $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, 'base64'); - $oSignaturePart->Body = $sSignature; - $oPart->SubParts->append($oSignaturePart); + $oSignaturePart = new MimePart; + $oSignaturePart->Headers->AddByName(MimeEnumHeader::CONTENT_TYPE, 'application/pkcs7-signature; name="signature.p7s"'); + $oSignaturePart->Headers->AddByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING, 'base64'); + $oSignaturePart->Body = $sSignature; + $oPart->SubParts->append($oSignaturePart); + } else { + $oPart->Headers->AddByName( + MimeEnumHeader::CONTENT_TYPE, + 'application/pkcs7-mime; smime-type=signed-data; name="smime.p7m"' + ); + $oPart->Headers->AddByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING, 'base64'); + $oPart->Body = $sSignature; + } } } 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 index f8770614b..14f1cf4b8 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/smime/openssl.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/smime/openssl.php @@ -10,34 +10,50 @@ class OpenSSL private int $flags = 0; private int $cipher_algo = \OPENSSL_CIPHER_AES_128_CBC; private ?string $untrusted_certificates_filename = null; + + // Used for sign and decrypt private $certificate; // OpenSSLCertificate|array|string - private $private_key; // OpenSSLAsymmetricKey|OpenSSLCertificate|array|string + private $privateKey; // OpenSSLAsymmetricKey|OpenSSLCertificate|array|string public static function isSupported() : bool { return \defined('PKCS7_DETACHED'); } - public function setPrivateKey($private_key = null, + public function setCertificate(/*OpenSSLCertificate|string*/$certificate) + { + $this->certificate = \openssl_x509_read($certificate); + if (!$this->certificate) { + throw new \RuntimeException('OpenSSL x509: ' . \openssl_error_string()); + } + if ($this->privateKey && !\openssl_x509_check_private_key($this->certificate, $this->privateKey)) { + throw new \RuntimeException('OpenSSL x509: ' . \openssl_error_string()); + } + } + + public function setPrivateKey(/*OpenSSLAsymmetricKey|string*/$privateKey, #[\SensitiveParameter] ?string $passphrase = null ) : void { - $this->private_key = \openssl_pkey_get_private($private_key, $passphrase); - if (!$this->private_key) { + $this->privateKey = \openssl_pkey_get_private($privateKey, $passphrase); + if (!$this->privateKey) { + throw new \RuntimeException('OpenSSL setPrivateKey: ' . \openssl_error_string()); + } + if ($this->certificate && !\openssl_x509_check_private_key($this->certificate, $this->privateKey)) { throw new \RuntimeException('OpenSSL setPrivateKey: ' . \openssl_error_string()); } } - public function decrypt(string $data, $certificate = null, $private_key = null) : ?string + public function decrypt(string $data) : ?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); + $this->certificate, + $this->privateKey )) ? $output->getContents() : null; } @@ -61,7 +77,7 @@ class OpenSSL ) ? $output->getContents() : null; } - public function sign(/*string|Temporary*/$input, $certificate = null, $private_key = null) + public function sign(/*string|Temporary*/$input, bool $detached = true) { if (\is_string($input)) { $input = new Temporary('smimein-'); @@ -73,18 +89,47 @@ class OpenSSL if (!\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->certificate, + $this->privateKey, $this->headers, - \PKCS7_DETACHED | \PKCS7_BINARY, // | PKCS7_NOCERTS | PKCS7_NOATTR + $detached ? \PKCS7_DETACHED | \PKCS7_BINARY : \PKCS7_BINARY, // | PKCS7_NOCERTS | PKCS7_NOATTR $this->untrusted_certificates_filename )) { throw new \RuntimeException('OpenSSL sign: ' . \openssl_error_string()); } - $body = $output->getContents(); - if (\preg_match('/\\.p7s"\R\R(.+?)------/s', $body, $match)) { - return \trim($match[1]); + /** + * Only fetch the signed part + */ + $fp = $output->fopen(); + $micalg = ''; + while (!\feof($fp)) { + $line = \fgets($fp); + $fp = $output->fopen(); + while (!\feof($fp)) { + $line = \fgets($fp); +/* + if (!$micalg && \str_contains($line, 'Content-Type: multipart/signed')) { + \preg_match('/micalg="([^"+])"/', $line, $match); + $micalg = $match[1]; + } +*/ + if (($detached && \str_contains($line, 'Content-Type: application/x-pkcs7-signature')) + || (!$detached && \str_contains($line, 'Content-Type: application/x-pkcs7-mime')) + ) { + // Skip headers + while (\trim(\fgets($fp))); + // Fetch the body + $data = ''; + do { + $line = \fgets($fp); + if (!\trim($line)) { + return $data; + } + $data .= $line; + } while (true); + } + } } throw new \RuntimeException('OpenSSL sign: failed to find p7s');