Files
anonaddy/app/CustomMailDriver/Mime/Crypto/OpenPGPEncrypter.php
2022-07-07 08:44:47 +01:00

303 lines
9.6 KiB
PHP

<?php
namespace App\CustomMailDriver\Mime\Crypto;
use App\CustomMailDriver\Mime\Part\EncryptedPart;
use Symfony\Component\Mailer\Exception\RuntimeException;
use Symfony\Component\Mime\Email;
class OpenPGPEncrypter
{
protected $gnupg = null;
/**
* The signing hash algorithm. 'MD5', SHA1, or SHA256. SHA256 (the default) is highly recommended
* unless you need to deal with an old client that doesn't support it. SHA1 and MD5 are
* currently considered cryptographically weak.
*
* This is apparently not supported by the PHP GnuPG module.
*
* @type string
*/
protected $micalg = 'SHA256';
protected $recipientKey = null;
/**
* The fingerprint of the key that will be used to sign the email. Populated either with
* autoAddSignature or addSignature.
*
* @type string
*/
protected $signingKey;
/**
* An associative array of keyFingerprint=>passwords to decrypt secret keys (if needed).
* Populated by calling addKeyPassphrase. Pointless at the moment because the GnuPG module in
* PHP doesn't support decrypting keys with passwords. The command line client does, so this
* method stays for now.
*
* @type array
*/
protected $keyPassphrases = [];
/**
* Specifies the home directory for the GnuPG keyrings. By default this is the user's home
* directory + /.gnupg, however when running on a web server (eg: Apache) the home directory
* will likely not exist and/or not be writable. Set this by calling setGPGHome before calling
* any other encryption/signing methods.
*
* @var string
*/
protected $gnupgHome = null;
public function __construct($signingKey = null, $recipientKey = null, $gnupgHome = null)
{
$this->initGNUPG();
$this->signingKey = $signingKey;
$this->recipientKey = $recipientKey;
$this->gnupgHome = $gnupgHome;
}
/**
* @param string $micalg
*/
public function setMicalg($micalg)
{
$this->micalg = $micalg;
}
/**
* @param $identifier
* @param null $passPhrase
*
* @throws RuntimeException
*/
public function addSignature($identifier, $keyFingerprint = null, $passPhrase = null)
{
if (!$keyFingerprint) {
$keyFingerprint = $this->getKey($identifier, 'sign');
}
$this->signingKey = $keyFingerprint;
if ($passPhrase) {
$this->addKeyPassphrase($keyFingerprint, $passPhrase);
}
}
/**
* @param $identifier
* @param $passPhrase
*
* @throws RuntimeException
*/
public function addKeyPassphrase($identifier, $passPhrase)
{
$keyFingerprint = $this->getKey($identifier, 'sign');
$this->keyPassphrases[$keyFingerprint] = $passPhrase;
}
/**
* @param Email $email
*
* @return $this
*
* @throws RuntimeException
*/
public function encrypt(Email $message): Email
{
$originalMessage = $message->toString();
$headers = $message->getPreparedHeaders();
$boundary = strtr(base64_encode(random_bytes(6)), '+/', '-_');
$headers->setHeaderBody('Parameterized', 'Content-Type', 'multipart/signed');
$headers->setHeaderParameter('Content-Type', 'micalg', sprintf("pgp-%s", strtolower($this->micalg)));
$headers->setHeaderParameter('Content-Type', 'protocol', 'application/pgp-signature');
$headers->setHeaderParameter('Content-Type', 'boundary', $boundary);
$message->setHeaders($headers);
if (!$this->signingKey) {
foreach ($message->getFrom() as $key => $value) {
$this->addSignature($this->getKey($key, 'sign'));
}
}
if (!$this->signingKey) {
throw new RuntimeException('Signing has been enabled, but no signature has been added. Use autoAddSignature() or addSignature()');
}
$lines = preg_split('/(\r\n|\r|\n)/', rtrim($originalMessage));
for ($i=0; $i<count($lines); $i++) {
$lines[$i] = rtrim($lines[$i])."\r\n";
}
// Remove excess trailing newlines (RFC3156 section 5.4)
$signedBody = rtrim(implode('', $lines))."\r\n";
$signature = $this->pgpSignString($signedBody, $this->signingKey);
// Fixes DKIM signature incorrect body hash for custom domains
$body = "This is an OpenPGP/MIME signed message (RFC 4880 and 3156)\r\n\r\n";
$body .= "--{$boundary}\r\n";
$body .= $signedBody."\r\n";
$body .= "--{$boundary}\r\n";
$body .= "Content-Type: application/pgp-signature; name=\"signature.asc\"\r\n";
$body .= "Content-Description: OpenPGP digital signature\r\n";
$body .= "Content-Disposition: attachment; filename=\"signature.asc\"\r\n\r\n";
$body .= $signature."\r\n\r\n";
$body .= "--{$boundary}--";
$signed = sprintf("%s\r\n%s", $message->getHeaders()->get('content-type')->toString(), $body);
if (!$this->recipientKey) {
throw new RuntimeException('Encryption has been enabled, but no recipients have been added. Use autoAddRecipients() or addRecipient()');
}
//Create body from signed message
$encryptedBody = $this->pgpEncryptString($signed, $this->recipientKey);
$headers->setHeaderBody('Parameterized', 'Content-Type', 'multipart/encrypted');
$headers->setHeaderParameter('Content-Type', 'protocol', 'application/pgp-encrypted');
$headers->setHeaderParameter('Content-Type', 'boundary', $boundary);
// Fixes DKIM signature incorrect body hash for custom domains
$body = "This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)\r\n\r\n";
$body .= "--{$boundary}\r\n";
$body .= "Content-Type: application/pgp-encrypted\r\n";
$body .= "Content-Description: PGP/MIME version identification\r\n\r\n";
$body .= "Version: 1\r\n\r\n";
$body .= "--{$boundary}\r\n";
$body .= "Content-Type: application/octet-stream; name=\"encrypted.asc\"\r\n";
$body .= "Content-Description: OpenPGP encrypted message\r\n";
$body .= "Content-Disposition: inline; filename=\"encrypted.asc\"\r\n\r\n";
$body .= $encryptedBody."\r\n\r\n";
$body .= "--{$boundary}--";
return $message->setBody(new EncryptedPart($body));
}
/**
* @throws RuntimeException
*/
protected function initGNUPG()
{
if (!class_exists('gnupg')) {
throw new RuntimeException('PHPMailerPGP requires the GnuPG class');
}
if (!$this->gnupgHome && isset($_SERVER['HOME'])) {
$this->gnupgHome = $_SERVER['HOME'] . '/.gnupg';
}
if (!$this->gnupgHome && getenv('HOME')) {
$this->gnupgHome = getenv('HOME') . '/.gnupg';
}
if (!$this->gnupg) {
$this->gnupg = new \gnupg();
}
$this->gnupg->seterrormode(\gnupg::ERROR_EXCEPTION);
}
/**
* @param $plaintext
* @param $keyFingerprint
*
* @return string
*
* @throws RuntimeException
*/
protected function pgpSignString($plaintext, $keyFingerprint)
{
if (isset($this->keyPassphrases[$keyFingerprint]) && !$this->keyPassphrases[$keyFingerprint]) {
$passPhrase = $this->keyPassphrases[$keyFingerprint];
} else {
$passPhrase = null;
}
$this->gnupg->clearsignkeys();
$this->gnupg->addsignkey($keyFingerprint, $passPhrase);
$this->gnupg->setsignmode(\gnupg::SIG_MODE_DETACH);
$this->gnupg->setarmor(1);
$signed = $this->gnupg->sign($plaintext);
if ($signed) {
return $signed;
}
throw new RuntimeException('Unable to sign message (perhaps the secret key is encrypted with a passphrase?)');
}
/**
* @param $plaintext
* @param $keyFingerprints
*
* @return string
*
* @throws RuntimeException
*/
protected function pgpEncryptString($plaintext, $keyFingerprint)
{
$this->gnupg->clearencryptkeys();
$this->gnupg->addencryptkey($keyFingerprint);
$this->gnupg->setarmor(1);
$encrypted = $this->gnupg->encrypt($plaintext);
if ($encrypted) {
return $encrypted;
}
throw new RuntimeException('Unable to encrypt message');
}
/**
* @param $identifier
* @param $purpose
*
* @return string
*
* @throws RuntimeException
*/
protected function getKey($identifier, $purpose)
{
$keys = $this->gnupg->keyinfo($identifier);
$fingerprints = [];
foreach ($keys as $key) {
if ($this->isValidKey($key, $purpose)) {
foreach ($key['subkeys'] as $subKey) {
if ($this->isValidKey($subKey, $purpose)) {
$fingerprints[] = $subKey['fingerprint'];
}
}
}
}
// Return first available to encrypt
if (count($fingerprints) >= 1) {
return $fingerprints[0];
}
/* if (count($fingerprints) > 1) {
throw new Swift_SwiftException(sprintf('Found more than one active key for %s use addRecipient() or addSignature()', $identifier));
} */
throw new RuntimeException(sprintf('Unable to find an active key to %s for %s,try importing keys first', $purpose, $identifier));
}
protected function isValidKey($key, $purpose)
{
return !($key['disabled'] || $key['expired'] || $key['revoked'] || ($purpose == 'sign' && !$key['can_sign']) || ($purpose == 'encrypt' && !$key['can_encrypt']));
}
}