Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement #20938: GPG signing and encryption #9

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
152 changes: 150 additions & 2 deletions Mail/mime.php
Expand Up @@ -103,6 +103,13 @@ class Mail_mime
*/
protected $calbody;

/**
* Crypt_GPG instance to encrypt or sign email with
*
* @var Crypt_GPG
*/
protected $gpg;

/**
* list of the attached images
*
Expand Down Expand Up @@ -158,6 +165,8 @@ class Mail_mime
'calendar_method' => 'request',
// multipart part preamble (RFC2046 5.1.1)
'preamble' => '',
//MIME boundary for GPG parts
'boundary_gpg' => null,
);


Expand Down Expand Up @@ -309,6 +318,21 @@ public function getCalendarBody()
return $this->calbody;
}

/**
* Set the Crypt_GPG object used for encrypting and signing the mail.
*
* To activate encryption, at least one encryption key has to be added.
* To activate signing, at least one signing key has to be added.
*
* @param Crypt_GPG $gpg Configured GPG object
*
* @return void
*/
public function setGPG(Crypt_GPG $gpg)
{
$this->gpg = $gpg;
}

/**
* Adds an image to the list of embedded images.
* Images added this way will be added as related parts of the HTML message.
Expand Down Expand Up @@ -1050,13 +1074,21 @@ public function get($params = null, $filename = null, $skip_head = false)
if (self::isError($headers)) {
return $headers;
}
//FIXME: GPG support for $filename
$this->headers = array_merge($this->headers, $headers);
return null;
} else {
$output = $message->encode($boundary, $skip_head);
if (self::isError($output)) {
return $output;
}
if ($this->gpg !== null) {
$output = $this->applyGPG($output);
if (self::isError($output)) {
return $output;
}
}

$this->headers = array_merge($this->headers, $output['headers']);
return $output['body'];
}
Expand Down Expand Up @@ -1136,9 +1168,21 @@ public function txtHeaders($xtra_headers = null, $overwrite = false, $skip_conte
$headers = array('Received' => $received) + $headers;
}

$ret = '';
$eol = $this->build_params['eol'];
return self::flattenHeaders($headers, $this->build_params['eol']);
}

/**
* Convert an array of headers to a string
*
* @param array $headers Key-value pairs; header name as key.
* Value may be a string or an array.
* @param string $eol End-of-line character, e.g. "\n"
*
* @return string A string with all headers
*/
protected static function flattenHeaders($headers, $eol = "\n")
{
$ret = '';
foreach ($headers as $key => $val) {
if (is_array($val)) {
foreach ($val as $value) {
Expand Down Expand Up @@ -1594,6 +1638,110 @@ protected function addBodyPart($obj, $body, $ctype, $type)
return $ret;
}

/**
* Encrypt and/or sign the e-mail contents.
* Uses the Crypt_GPG object set via setGPG().
*
* This method implements
* - RFC 1847 - Security Multiparts for MIME:
* Multipart/Signed and Multipart/Encrypted
* - RFC 2015 - MIME Security with Pretty Good Privacy (PGP)
* - RFC 3156 - MIME Security with OpenPGP
*
* @param array $output Result of Mail_mimePart::encode() with
* "headers" and "body" keys.
*
* @return array Result of Mail_mimePart::encode() with "headers" and
* "body" keys containing the encrypted/signed data.
* A PEAR_Error object may also be raised.
*/
protected function applyGPG($output)
{
$headers = $output['headers'];
$body = $output['body'];
$eol = $this->build_params['eol'];
$content = self::flattenHeaders($headers, $eol) . $eol . $body;

if ($this->gpg->hasEncryptKeys()) {
if ($this->gpg->hasSignKeys()) {
//encrypt + sign
$data = $this->gpg->encryptAndSign($content);
} else {
//encrypt only
$data = $this->gpg->encrypt($content);
}

$message = new Mail_mimePart(
'',
array(
'content_type' => 'multipart/encrypted'
. '; protocol="application/pgp-encrypted"',
'eol' => $eol,
)
);
$message->addSubpart(
'Version: 1' . $eol,
array(
'content_type' => 'application/pgp-encrypted',
'eol' => $eol,
)
);
$message->addSubpart(
$data,
array(
'content_type' => 'application/octet-stream',
'eol' => $eol,
)
);
$output = $message->encode($this->build_params['boundary_gpg'], false);
} else if ($this->gpg->hasSignKeys()) {
//signing only, no encryption
$headersNoContentType = $headers;
unset($headersNoContentType['Content-Type']);

$signature = $this->gpg->sign(
$content,
Crypt_GPG::SIGN_MODE_DETACHED,
Crypt_GPG::ARMOR_ASCII,
Crypt_GPG::TEXT_NORMALIZED
);

$sigInfo = $this->gpg->getLastSignatureInfo();
if ($sigInfo === null) {
return self::raiseError('Failed to fetch signature information');
}
$micalg = 'pgp-' . $sigInfo->getHashAlgorithmName();

$message = new Mail_mimePart(
'',
array(
'content_type' => 'multipart/signed'
. ';protocol="application/pgp-signature"'
. ';micalg=' . $micalg,
'eol' => $eol,
)
);
$message->addSubpart(
$body,
array(
'content_type' => $headers['Content-Type'],
'headers' => $headersNoContentType,
'eol' => $eol,
)
);
$message->addSubpart(
$signature,
array(
'content_type' => 'application/pgp-signature',
'eol' => $eol,
)
);
$output = $message->encode($this->build_params['boundary_gpg'], false);
}

return $output;
}

/**
* PEAR::isError implementation
*
Expand Down
25 changes: 25 additions & 0 deletions examples/gpg-encrypt.php
@@ -0,0 +1,25 @@
<?php
/**
* Encrypt an email with PGP/gnupg
*/
require_once 'Mail.php';
require_once 'Mail/mime.php' ;
require_once 'Crypt/GPG.php';

$mime = new Mail_mime(array('eol' => "\n"));
$hdrs = array(
'From' => 'foo@example.org',
'Subject' => 'An encrypted mail example'
);
$mime->setTXTBody('This text will be only readable by bar@example.org');

$gpg = new Crypt_GPG();
$gpg->addEncryptKey('bar@example.org');
$mime->setGPG($gpg);

$body = $mime->get();
$hdrs = $mime->headers($hdrs);

$mail = Mail::factory('mail');
//$mail->send('bar@example.org', $hdrs, $body);
?>
30 changes: 30 additions & 0 deletions examples/gpg-sign-encrypt.php
@@ -0,0 +1,30 @@
<?php
/**
* Sign AND encrypt an email with PGP/gnupg
*/
require_once 'Mail.php';
require_once 'Mail/mime.php' ;
require_once 'Crypt/GPG.php';

$mime = new Mail_mime(array('eol' => "\n"));
$hdrs = array(
'From' => 'foo@example.org',
'Subject' => 'An encrypted mail example'
);
$mime->setTXTBody(
'This text and the attachment will be signed by foo@example.org'
. ' and only readable by bar@example.org'
);
$mime->addAttachment('/path/to/file', 'text/plain');

$gpg = new Crypt_GPG();
$gpg->addEncryptKey('bar@example.org');
$gpg->addSignKey('foo@example.org', 'gpgpassword');
$mime->setGPG($gpg);

$body = $mime->get();
$hdrs = $mime->headers($hdrs);

$mail = Mail::factory('mail');
//$mail->send('bar@example.org', $hdrs, $body);
?>
25 changes: 25 additions & 0 deletions examples/gpg-sign.php
@@ -0,0 +1,25 @@
<?php
/**
* Sign an email with PGP/gnupg
*/
require_once 'Mail.php';
require_once 'Mail/mime.php' ;
require_once 'Crypt/GPG.php';

$mime = new Mail_mime(array('eol' => "\n"));
$hdrs = array(
'From' => 'foo@example.org',
'Subject' => 'An encrypted mail example'
);
$mime->setTXTBody('This text will be signed by foo@example.org');

$gpg = new Crypt_GPG();
$gpg->addSignKey('foo@example.org', 'gpgpassword');
$mime->setGPG($gpg);

$body = $mime->get();
$hdrs = $mime->headers($hdrs);

$mail = Mail::factory('mail');
//$mail->send('bar@example.org', $hdrs, $body);
?>
Binary file added tests/gpg-keychain/pubring.gpg
Binary file not shown.
Binary file added tests/gpg-keychain/random_seed
Binary file not shown.
Binary file added tests/gpg-keychain/secring.gpg
Binary file not shown.
Binary file added tests/gpg-keychain/trustdb.gpg
Binary file not shown.
91 changes: 91 additions & 0 deletions tests/gpg_encrypt.phpt
@@ -0,0 +1,91 @@
--TEST--
Request #20938: Encrypting mail with GPG
--SKIPIF--
<?php
include 'Crypt/GPG.php';
if (!class_exists('Crypt_GPG')) {
echo "skip Crypt_GPG not available\n";
}
?>
--FILE--
<?php
require_once 'Mail/mime.php';
require_once 'Crypt/GPG.php';

$mime = new Mail_mime(
array(
'eol' => "\n",
'boundary_gpg' => '=_unittest-gpg',
'boundary' => '=_unittest-main'
)
);
$hdrs = array(
'From' => 'foo@example.org',
'Subject' => 'PGP Test'
);
$mime->setTXTBody("txtbody");
$mime->setHTMLBody('<h1>foo</h1>');

$gpg = new Crypt_GPG(
array('homedir' => __DIR__ . '/gpg-keychain')
);
$gpg->addEncryptKey('first-keypair@example.com');
$mime->setGPG($gpg);

$message = $mime->getMessage();

echo "E-mail:\n----\n";
echo $message . "\n";

echo "----\nDecrypted e-mail content:\n";

preg_match(
'#-----BEGIN PGP MESSAGE-----.*-----END PGP MESSAGE-----#s',
$message,
$matches
);
$encryptedData = $matches[0];

$gpg->addDecryptKey('first-keypair@example.com', 'test1');
echo str_replace("\r\n", "\n", $gpg->decrypt($encryptedData)) . "\n";
echo "done\n";
?>
--EXPECTF--
E-mail:
----
MIME-Version: 1.0
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
boundary="=_unittest-gpg"

--=_unittest-gpg
Content-Type: application/pgp-encrypted

Version: 1

--=_unittest-gpg
Content-Type: application/octet-stream

-----BEGIN PGP MESSAGE-----
%s
-----END PGP MESSAGE-----

--=_unittest-gpg--

----
Decrypted e-mail content:
Content-Type: multipart/alternative;
boundary="=_unittest-main"

--=_unittest-main
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain; charset=ISO-8859-1

txtbody
--=_unittest-main
Content-Transfer-Encoding: quoted-printable
Content-Type: text/html; charset=ISO-8859-1

<h1>foo</h1>
--=_unittest-main--

done