mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-09-07 08:27:03 +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,
|
doSign: false,
|
||||||
doEncrypt: false,
|
doEncrypt: false,
|
||||||
|
|
||||||
canPgpSign: false,
|
pgpSignKey: false,
|
||||||
canPgpEncrypt: false,
|
canPgpEncrypt: false,
|
||||||
canMailvelope: false,
|
canMailvelope: false,
|
||||||
|
|
||||||
|
|
@ -334,7 +334,10 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
attachmentsInProcessCount: () => this.attachmentsInProcess.length,
|
attachmentsInProcessCount: () => this.attachmentsInProcess.length,
|
||||||
isDraft: () => this.draftsFolder() && this.draftUid(),
|
isDraft: () => this.draftsFolder() && this.draftUid(),
|
||||||
|
|
||||||
canSign: () => this.canPgpSign() | this.canSMimeSign(),
|
canSign: () => {
|
||||||
|
let s = this.canSMimeSign();
|
||||||
|
return this.pgpSignKey() || s;
|
||||||
|
},
|
||||||
canEncrypt: () => this.canPgpEncrypt() | this.canSMimeEncrypt(),
|
canEncrypt: () => this.canPgpEncrypt() | this.canSMimeEncrypt(),
|
||||||
|
|
||||||
identitiesOptions: () =>
|
identitiesOptions: () =>
|
||||||
|
|
@ -365,14 +368,14 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
},
|
},
|
||||||
|
|
||||||
from: value => {
|
from: value => {
|
||||||
this.canPgpSign(false);
|
this.pgpSignKey(false);
|
||||||
value = getEmail(value);
|
value = getEmail(value);
|
||||||
value && PgpUserStore.getKeyForSigning(value).then(result => {
|
value && PgpUserStore.getKeyForSigning(value).then(result => {
|
||||||
console.log({
|
console.log({
|
||||||
email: value,
|
email: value,
|
||||||
canPgpSign:result
|
pgpSignKey:result
|
||||||
});
|
});
|
||||||
this.canPgpSign(result)
|
this.pgpSignKey(result)
|
||||||
});
|
});
|
||||||
this.initPgpEncrypt();
|
this.initPgpEncrypt();
|
||||||
},
|
},
|
||||||
|
|
@ -1458,7 +1461,7 @@ export class ComposePopupView extends AbstractViewPopup {
|
||||||
linkedData: []
|
linkedData: []
|
||||||
},
|
},
|
||||||
recipients = draft ? [identity.email()] : this.allRecipients(),
|
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()),
|
encrypt = this.doEncrypt() && (this.canPgpEncrypt() || this.canSMimeEncrypt()),
|
||||||
isHtml = this.oEditor.isHtml();
|
isHtml = this.oEditor.isHtml();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,7 @@ class BodyStructure implements \JsonSerializable
|
||||||
public function isSMimeEncrypted() : bool
|
public function isSMimeEncrypted() : bool
|
||||||
{
|
{
|
||||||
$type = \strtolower(\trim($this->aContentTypeParams['smime-type'] ?? ''));
|
$type = \strtolower(\trim($this->aContentTypeParams['smime-type'] ?? ''));
|
||||||
return 'application/pkcs7-mime' === $this->sContentType
|
return ContentType::isPkcs7Mime($this->sContentType)
|
||||||
&& !empty($this->aContentTypeParams['smime-type'])
|
&& !empty($this->aContentTypeParams['smime-type'])
|
||||||
&& ('enveloped-data' === $type || 'authenveloped-data' === $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.
|
// The multipart/signed body MUST consist of exactly two parts.
|
||||||
&& 2 === \count($this->aSubParts)
|
&& 2 === \count($this->aSubParts)
|
||||||
&& ContentType::isPkcs7Signature($this->aSubParts[1]->ContentType())
|
&& ContentType::isPkcs7Signature($this->aSubParts[1]->ContentType())
|
||||||
) || ('application/pkcs7-mime' === $this->sContentType
|
) || (ContentType::isPkcs7Mime($this->sContentType)
|
||||||
&& !empty($this->aContentTypeParams['smime-type'])
|
&& !empty($this->aContentTypeParams['smime-type'])
|
||||||
&& 'signed-data' === \strtolower(\trim($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
|
public function GetPartByMimeIndex(string $sMimeIndex) : self
|
||||||
{
|
{
|
||||||
$oPart = null;
|
$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) {
|
foreach ($gEncryptedParts as $oPart) {
|
||||||
if ($oPart->isSMimeEncrypted()) {
|
if ($oPart->isSMimeEncrypted()) {
|
||||||
$oMessage->smimeEncrypted = [
|
$oMessage->smimeEncrypted = [
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,12 @@ abstract class ContentType
|
||||||
const PKCS7_SIGNATURE = 'application/pkcs7-signature';
|
const PKCS7_SIGNATURE = 'application/pkcs7-signature';
|
||||||
const PKCS7_MIME = 'application/pkcs7-mime';
|
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
|
public static function isPkcs7Signature(string $data) : bool
|
||||||
{
|
{
|
||||||
return 'application/pkcs7-signature' === $data
|
return 'application/pkcs7-signature' === $data
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ class Part
|
||||||
&& 2 === \count($this->SubParts)
|
&& 2 === \count($this->SubParts)
|
||||||
&& ContentType::isPkcs7Signature($this->SubParts[1]->ContentType())
|
&& ContentType::isPkcs7Signature($this->SubParts[1]->ContentType())
|
||||||
) || ($header
|
) || ($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->SubParts->Clear();
|
||||||
$oMessage->Attachments()->Clear();
|
$oMessage->Attachments()->Clear();
|
||||||
|
|
||||||
|
$detached = true;
|
||||||
|
|
||||||
$SMIME = new \SnappyMail\SMime\OpenSSL;
|
$SMIME = new \SnappyMail\SMime\OpenSSL;
|
||||||
|
$SMIME->setCertificate($sCertificate);
|
||||||
$SMIME->setPrivateKey($sPrivateKey, $sPassphrase);
|
$SMIME->setPrivateKey($sPrivateKey, $sPassphrase);
|
||||||
$sSignature = $SMIME->sign($tmp, $sCertificate);
|
$sSignature = $SMIME->sign($tmp, $detached);
|
||||||
|
|
||||||
if (!$sSignature) {
|
if (!$sSignature) {
|
||||||
throw new \Exception('S/MIME sign() failed');
|
throw new \RuntimeException('S/MIME sign() failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
$oPart = new MimePart;
|
$oPart = new MimePart;
|
||||||
$oPart->Headers->AddByName(
|
|
||||||
MimeEnumHeader::CONTENT_TYPE,
|
|
||||||
'multipart/signed; micalg="sha-512"; protocol="application/pkcs7-signature"'
|
|
||||||
);
|
|
||||||
$oMessage->SubParts->append($oPart);
|
$oMessage->SubParts->append($oPart);
|
||||||
|
if ($detached) {
|
||||||
|
$oPart->Headers->AddByName(
|
||||||
|
MimeEnumHeader::CONTENT_TYPE,
|
||||||
|
'multipart/signed; micalg="sha-256"; protocol="application/pkcs7-signature"'
|
||||||
|
);
|
||||||
|
|
||||||
$fp = $tmp->fopen();
|
$fp = $tmp->fopen();
|
||||||
\rewind($fp);
|
\rewind($fp);
|
||||||
$oBody->Raw = $fp;
|
$oBody->Raw = $fp;
|
||||||
$oPart->SubParts->append($oBody);
|
$oPart->SubParts->append($oBody);
|
||||||
|
|
||||||
$oSignaturePart = new MimePart;
|
$oSignaturePart = new MimePart;
|
||||||
$oSignaturePart->Headers->AddByName(MimeEnumHeader::CONTENT_TYPE, 'application/pkcs7-signature; name="signature.p7s"');
|
$oSignaturePart->Headers->AddByName(MimeEnumHeader::CONTENT_TYPE, 'application/pkcs7-signature; name="signature.p7s"');
|
||||||
$oSignaturePart->Headers->AddByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING, 'base64');
|
$oSignaturePart->Headers->AddByName(MimeEnumHeader::CONTENT_TRANSFER_ENCODING, 'base64');
|
||||||
$oSignaturePart->Body = $sSignature;
|
$oSignaturePart->Body = $sSignature;
|
||||||
$oPart->SubParts->append($oSignaturePart);
|
$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 $flags = 0;
|
||||||
private int $cipher_algo = \OPENSSL_CIPHER_AES_128_CBC;
|
private int $cipher_algo = \OPENSSL_CIPHER_AES_128_CBC;
|
||||||
private ?string $untrusted_certificates_filename = null;
|
private ?string $untrusted_certificates_filename = null;
|
||||||
|
|
||||||
|
// Used for sign and decrypt
|
||||||
private $certificate; // OpenSSLCertificate|array|string
|
private $certificate; // OpenSSLCertificate|array|string
|
||||||
private $private_key; // OpenSSLAsymmetricKey|OpenSSLCertificate|array|string
|
private $privateKey; // OpenSSLAsymmetricKey|OpenSSLCertificate|array|string
|
||||||
|
|
||||||
public static function isSupported() : bool
|
public static function isSupported() : bool
|
||||||
{
|
{
|
||||||
return \defined('PKCS7_DETACHED');
|
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]
|
#[\SensitiveParameter]
|
||||||
?string $passphrase = null
|
?string $passphrase = null
|
||||||
) : void
|
) : void
|
||||||
{
|
{
|
||||||
$this->private_key = \openssl_pkey_get_private($private_key, $passphrase);
|
$this->privateKey = \openssl_pkey_get_private($privateKey, $passphrase);
|
||||||
if (!$this->private_key) {
|
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());
|
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-');
|
$input = new Temporary('smimein-');
|
||||||
$output = new Temporary('smimeout-');
|
$output = new Temporary('smimeout-');
|
||||||
return ($input->putContents($data) && \openssl_pkcs7_decrypt(
|
return ($input->putContents($data) && \openssl_pkcs7_decrypt(
|
||||||
$input->filename(),
|
$input->filename(),
|
||||||
$output->filename(),
|
$output->filename(),
|
||||||
$certificate ?: $this->certificate, // \openssl_pkey_get_public();
|
$this->certificate,
|
||||||
$private_key ?: $this->private_key // \openssl_pkey_get_private($private_key, ?string $passphrase = null);
|
$this->privateKey
|
||||||
)) ? $output->getContents() : null;
|
)) ? $output->getContents() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,7 +77,7 @@ class OpenSSL
|
||||||
) ? $output->getContents() : null;
|
) ? $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)) {
|
if (\is_string($input)) {
|
||||||
$input = new Temporary('smimein-');
|
$input = new Temporary('smimein-');
|
||||||
|
|
@ -73,18 +89,47 @@ class OpenSSL
|
||||||
if (!\openssl_pkcs7_sign(
|
if (!\openssl_pkcs7_sign(
|
||||||
$input->filename(),
|
$input->filename(),
|
||||||
$output->filename(),
|
$output->filename(),
|
||||||
$certificate ?: $this->certificate, // \openssl_pkey_get_public();
|
$this->certificate,
|
||||||
$private_key ?: $this->private_key, // \openssl_pkey_get_private($private_key, ?string $passphrase = null);
|
$this->privateKey,
|
||||||
$this->headers,
|
$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
|
$this->untrusted_certificates_filename
|
||||||
)) {
|
)) {
|
||||||
throw new \RuntimeException('OpenSSL sign: ' . \openssl_error_string());
|
throw new \RuntimeException('OpenSSL sign: ' . \openssl_error_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
$body = $output->getContents();
|
/**
|
||||||
if (\preg_match('/\\.p7s"\R\R(.+?)------/s', $body, $match)) {
|
* Only fetch the signed part
|
||||||
return \trim($match[1]);
|
*/
|
||||||
|
$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');
|
throw new \RuntimeException('OpenSSL sign: failed to find p7s');
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue