mirror of https://github.com/MISP/MISP
new: [internal] Support autocrypt when sending e-mails
parent
ee4de160e8
commit
46c5b824cb
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
class CryptGpgExtended extends Crypt_GPG
|
||||
{
|
||||
/**
|
||||
* Export the smallest public key possible from the keyring.
|
||||
*
|
||||
* This removes all signatures except the most recent self-signature on each user ID. This option is the same as
|
||||
* running the --edit-key command "minimize" before export except that the local copy of the key is not modified.
|
||||
*
|
||||
* The exported key remains on the keyring. To delete the public key, use
|
||||
* {@link Crypt_GPG::deletePublicKey()}.
|
||||
*
|
||||
* If more than one key fingerprint is available for the specified
|
||||
* <kbd>$keyId</kbd> (for example, if you use a non-unique uid) only the
|
||||
* first public key is exported.
|
||||
*
|
||||
* @param string $keyId either the full uid of the public key, the email
|
||||
* part of the uid of the public key or the key id of
|
||||
* the public key. For example,
|
||||
* "Test User (example) <test@example.com>",
|
||||
* "test@example.com" or a hexadecimal string.
|
||||
* @param boolean $armor optional. If true, ASCII armored data is returned;
|
||||
* otherwise, binary data is returned. Defaults to
|
||||
* true.
|
||||
*
|
||||
* @return string the public key data.
|
||||
*
|
||||
* @throws Crypt_GPG_KeyNotFoundException if a public key with the given
|
||||
* <kbd>$keyId</kbd> is not found.
|
||||
*
|
||||
* @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
|
||||
* Use the <kbd>debug</kbd> option and file a bug report if these
|
||||
* exceptions occur.
|
||||
*/
|
||||
public function exportPublicKeyMinimal($keyId, $armor = true)
|
||||
{
|
||||
$fingerprint = $this->getFingerprint($keyId);
|
||||
|
||||
if ($fingerprint === null) {
|
||||
throw new Crypt_GPG_KeyNotFoundException(
|
||||
'Key not found: ' . $keyId,
|
||||
self::ERROR_KEY_NOT_FOUND,
|
||||
$keyId
|
||||
);
|
||||
}
|
||||
|
||||
$keyData = '';
|
||||
$operation = '--export';
|
||||
$operation .= ' ' . escapeshellarg($fingerprint);
|
||||
|
||||
$arguments = array('--export-options', 'export-minimal');
|
||||
if ($armor) {
|
||||
$arguments[] = '--armor';
|
||||
}
|
||||
|
||||
$this->engine->reset();
|
||||
$this->engine->setPins($this->passphrases);
|
||||
$this->engine->setOutput($keyData);
|
||||
$this->engine->setOperation($operation, $arguments);
|
||||
$this->engine->run();
|
||||
|
||||
return $keyData;
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
class GpgTool
|
||||
{
|
||||
/**
|
||||
* @return Crypt_GPG
|
||||
* @return CryptGpgExtended
|
||||
* @throws Exception
|
||||
*/
|
||||
public function initializeGpg()
|
||||
|
@ -15,6 +15,8 @@ class GpgTool
|
|||
require_once 'Crypt/GPG.php';
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/CryptGpgExtended.php';
|
||||
|
||||
$homedir = Configure::read('GnuPG.homedir');
|
||||
if ($homedir === null) {
|
||||
throw new Exception("Configuration option 'GnuPG.homedir' is not set, Crypt_GPG cannot be initialized.");
|
||||
|
@ -26,7 +28,7 @@ class GpgTool
|
|||
'binary' => Configure::read('GnuPG.binary') ?: '/usr/bin/gpg',
|
||||
);
|
||||
|
||||
return new Crypt_GPG($options);
|
||||
return new CryptGpgExtended($options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,13 +3,25 @@ App::uses('CakeEmail', 'Network/Email');
|
|||
|
||||
class SendEmailException extends Exception {}
|
||||
|
||||
// RFC 4880 and 3156
|
||||
// https://dkg.fifthhorseman.net/notes/inline-pgp-harmful/
|
||||
// https://www.dalesandro.net/create-self-signed-smime-certificates/
|
||||
/**
|
||||
* Class CakeEmailExtended
|
||||
*
|
||||
* Extends `CakeEmail` to implement RFC 4880 and 3156.
|
||||
*
|
||||
* @see https://dkg.fifthhorseman.net/notes/inline-pgp-harmful/
|
||||
* @see https://www.dalesandro.net/create-self-signed-smime-certificates/
|
||||
*/
|
||||
class CakeEmailExtended extends CakeEmail
|
||||
{
|
||||
/**
|
||||
* @var MimeMultipart|MessagePart
|
||||
*/
|
||||
private $body;
|
||||
|
||||
/**
|
||||
* @param array $include
|
||||
* @return array
|
||||
*/
|
||||
public function getHeaders($include = array())
|
||||
{
|
||||
$headers = parent::getHeaders($include);
|
||||
|
@ -25,6 +37,9 @@ class CakeEmailExtended extends CakeEmail
|
|||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function boundary()
|
||||
{
|
||||
if ($this->body instanceof MimeMultipart) {
|
||||
|
@ -36,7 +51,7 @@ class CakeEmailExtended extends CakeEmail
|
|||
|
||||
/**
|
||||
* @param string|null|MimeMultipart|MessagePart $message
|
||||
* @return $this
|
||||
* @return string|null|MimeMultipart|MessagePart|CakeEmailExtended
|
||||
*/
|
||||
public function body($message = null)
|
||||
{
|
||||
|
@ -239,7 +254,7 @@ class MessagePart
|
|||
class SendEmail
|
||||
{
|
||||
/**
|
||||
* @var Crypt_GPG
|
||||
* @var CryptGpgExtended
|
||||
*/
|
||||
private $gpg;
|
||||
|
||||
|
@ -340,7 +355,7 @@ class SendEmail
|
|||
throw new SendEmailException('Emailing is currently disabled on this instance.');
|
||||
}
|
||||
|
||||
// check if the e-mail can be encrypted
|
||||
// Check if the e-mail can be encrypted
|
||||
$canEncryptGpg = isset($user['User']['gpgkey']) && !empty($user['User']['gpgkey']);
|
||||
$canEncryptSmime = isset($user['User']['certif_public']) && !empty($user['User']['certif_public']) && Configure::read('SMIME.enabled');
|
||||
|
||||
|
@ -348,7 +363,7 @@ class SendEmail
|
|||
throw new SendEmailException('Encrypted messages are enforced and the message could not be encrypted for this user as no valid encryption key was found.');
|
||||
}
|
||||
|
||||
// If bodyonlyencrypted is enabled and the user has no encryption key, use the alternate body (if it exists)
|
||||
// If 'bodyonlyencrypted' is enabled and the user has no encryption key, use the alternate body (if it exists)
|
||||
if (Configure::read('GnuPG.bodyonlyencrypted') && !$canEncryptSmime && !$canEncryptGpg && $bodyWithoutEncryption) {
|
||||
$body = $bodyWithoutEncryption;
|
||||
}
|
||||
|
@ -365,7 +380,10 @@ class SendEmail
|
|||
|
||||
try {
|
||||
$this->gpg->addSignKey(Configure::read('GnuPG.email'), Configure::read('GnuPG.password'));
|
||||
$this->signByGpg($email);
|
||||
$this->signByGpg($email, $replyToUser);
|
||||
$this->gpg->clearSignKeys();
|
||||
|
||||
$email->addHeaders(array('Autocrypt' => $this->generateAutocrypt(Configure::read('GnuPG.email'))));
|
||||
$signed = true;
|
||||
} catch (Exception $e) {
|
||||
throw new SendEmailException("The message could not be signed.", 0, $e);
|
||||
|
@ -391,6 +409,7 @@ class SendEmail
|
|||
try {
|
||||
$this->gpg->addEncryptKey($fingerprint);
|
||||
$this->encryptByGpg($email);
|
||||
$this->gpg->clearEncryptKeys();
|
||||
|
||||
if ($signed && Configure::read('GnuPG.obscure_subject')) {
|
||||
// If message is signed, we can remove subject from unencrypted part of email and replace with '...',
|
||||
|
@ -402,7 +421,7 @@ class SendEmail
|
|||
|
||||
$encrypted = true;
|
||||
} catch (Exception $e) {
|
||||
throw new SendEmailException("The message could not be encrypted.", 0, $e);
|
||||
throw new SendEmailException('The message could not be encrypted.', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -416,7 +435,7 @@ class SendEmail
|
|||
$email->send();
|
||||
return $encrypted;
|
||||
} catch (Exception $e) {
|
||||
throw new SendEmailException("The message could not be sent.", 0, $e);
|
||||
throw new SendEmailException('The message could not be sent.', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -431,20 +450,24 @@ class SendEmail
|
|||
// Try to encrypt empty message
|
||||
$this->encryptTextBySmime($certificate, '');
|
||||
} catch (SendEmailException $e) {
|
||||
throw new Exception("This certificate cannot be used to encrypt email", 0, $e);
|
||||
throw new Exception('This certificate cannot be used to encrypt email.', 0, $e);
|
||||
}
|
||||
|
||||
$parsed = openssl_x509_parse($certificate);
|
||||
|
||||
// 5 should be 'smimeencrypt'
|
||||
if (!($parsed['purposes'][5][0] === 1 && $parsed['purposes'][5][2] === 'smimeencrypt')) {
|
||||
throw new Exception('This certificate cannot be used to encrypt email');
|
||||
if (!$parsed) {
|
||||
throw new Exception('Could not parse certificate');
|
||||
}
|
||||
|
||||
$now = new DateTime("now");
|
||||
// Purpose '5' should be 'smimeencrypt'
|
||||
if (!($parsed['purposes'][5][0] === 1 && $parsed['purposes'][5][2] === 'smimeencrypt')) {
|
||||
throw new Exception('This certificate cannot be used to encrypt email.');
|
||||
}
|
||||
|
||||
$now = new DateTime();
|
||||
$validToTime = new DateTime("@{$parsed['validTo_time_t']}");
|
||||
if ($validToTime <= $now) {
|
||||
throw new Exception('This certificate is expired');
|
||||
throw new Exception('This certificate is expired.');
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -477,10 +500,12 @@ class SendEmail
|
|||
} elseif (!empty($replyToUser['User']['certif_public'])) {
|
||||
$attachments[$replyToUser['User']['email'] . '.pem'] = $replyToUser['User']['certif_public'];
|
||||
}
|
||||
} else if (Configure::read('MISP.email_reply_to')) {
|
||||
$email->replyTo(Configure::read('MISP.email_reply_to'));
|
||||
}
|
||||
|
||||
$email->from(Configure::read('MISP.email'));
|
||||
$email->returnPath(Configure::read('MISP.email'));
|
||||
$email->returnPath(Configure::read('MISP.email')); // TODO?
|
||||
$email->to($user['User']['email']);
|
||||
$email->subject($subject);
|
||||
$email->emailFormat('text');
|
||||
|
@ -496,8 +521,9 @@ class SendEmail
|
|||
|
||||
/**
|
||||
* @param CakeEmailExtended $email
|
||||
* @param array $replyToUser
|
||||
*/
|
||||
private function signByGpg(CakeEmailExtended $email)
|
||||
private function signByGpg(CakeEmailExtended $email, array $replyToUser = array())
|
||||
{
|
||||
$renderedEmail = $email->render();
|
||||
|
||||
|
@ -507,6 +533,7 @@ class SendEmail
|
|||
'boundary="' . $email->boundary() . '"',
|
||||
'protected-headers="v1"',
|
||||
));
|
||||
|
||||
// Protect User-Facing Headers according to https://tools.ietf.org/id/draft-autocrypt-lamps-protected-headers-01.html
|
||||
$originalHeaders = $email->getHeaders(array('subject', 'from', 'to'));
|
||||
$protectedHeaders = array('From', 'To', 'Date', 'Message-ID', 'Subject', 'Reply-To');
|
||||
|
@ -515,6 +542,21 @@ class SendEmail
|
|||
$messagePart->addHeader($header, $originalHeaders[$header]);
|
||||
}
|
||||
}
|
||||
|
||||
// If the e-mail is sent on behalf of a user and that user has assigned GPG key, we will send his public key
|
||||
// in signed autocrypt header.
|
||||
if ($replyToUser) {
|
||||
if (!empty($replyToUser['User']['gpgkey'])) {
|
||||
$autocrypt = $this->generateAutocrypt($replyToUser['User']['email'], $replyToUser['User']['gpgkey'], false);
|
||||
$messagePart->addHeader('Autocrypt-Gossip', $autocrypt);
|
||||
}
|
||||
} else if (Configure::read('MISP.email_reply_to')) {
|
||||
$autocrypt = $this->generateAutocrypt(Configure::read('MISP.email_reply_to'), null, false);
|
||||
if ($autocrypt) {
|
||||
$messagePart->addHeader('Autocrypt-Gossip', $autocrypt);
|
||||
}
|
||||
}
|
||||
|
||||
$messagePart->setPayload($renderedEmail);
|
||||
|
||||
// GPG message to sign must be delimited by <CR><LF>
|
||||
|
@ -760,4 +802,37 @@ class SendEmail
|
|||
$second = base_convert(mt_rand(), 10, 36) . base_convert(mt_rand(), 10, 36);
|
||||
return "<$first.$second@{$email->domain()}>";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates Autocrypt header.
|
||||
*
|
||||
* If $gpgKey is not provided, GPG will try to find correct key by given e-mail address. If no key found, `null` is
|
||||
* returned.
|
||||
*
|
||||
* @see https://autocrypt.org/level1.html
|
||||
* @param string $address
|
||||
* @param string|null $gpgKey
|
||||
* @param bool $preferEncrypt
|
||||
* @return string|null
|
||||
*/
|
||||
private function generateAutocrypt($address, $gpgKey = null, $preferEncrypt = true)
|
||||
{
|
||||
if ($gpgKey) {
|
||||
$keyImportOutput = $this->gpg->importKey($gpgKey);
|
||||
$keyData = $this->gpg->exportPublicKeyMinimal($keyImportOutput['fingerprint'], false);
|
||||
} else {
|
||||
try {
|
||||
$keyData = $this->gpg->exportPublicKeyMinimal($address, false);
|
||||
} catch (Crypt_GPG_KeyNotFoundException $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$parts = array("addr=$address");
|
||||
if ($preferEncrypt) {
|
||||
$parts[] = 'prefer-encrypt=mutual';
|
||||
}
|
||||
$parts[] = 'keydata=' . base64_encode($keyData);
|
||||
return implode('; ', $parts);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue