mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-08-29 20:19:22 +03:00
Fix signing messages with S/MIME #259
This commit is contained in:
parent
128e2f6254
commit
4df3766231
7 changed files with 112 additions and 39 deletions
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue