*/ class OpenPGPSigner implements Swift_Signers_BodySigner { 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'; /** * An associative array of identifier=>keyFingerprint for the recipients we'll encrypt the email * to, where identifier is usually the email address, but could be anything used to look up a * key (including the fingerprint itself). This is populated either by autoAddRecipients or by * calling addRecipient. * * @type array */ protected $recipientKeys = []; /** * 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; /** * @var bool */ protected $encrypt = true; public function __construct($signingKey = null, $recipientKeys = [], $gnupgHome = null) { $this->initGNUPG(); $this->signingKey = $signingKey; $this->recipientKeys = $recipientKeys; $this->gnupgHome = $gnupgHome; } public static function newInstance($signingKey = null, $recipientKeys = [], $gnupgHome = null) { return new self($signingKey, $recipientKeys, $gnupgHome); } /** * @param boolean $encrypt */ public function setEncrypt($encrypt) { $this->encrypt = $encrypt; } /** * @param string $gnupgHome */ public function setGnupgHome($gnupgHome) { $this->gnupgHome = $gnupgHome; } /** * @param string $micalg */ public function setMicalg($micalg) { $this->micalg = $micalg; } /** * @param $identifier * @param null $passPhrase * * @throws Swift_SwiftException */ 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 Swift_SwiftException */ public function addKeyPassphrase($identifier, $passPhrase) { $keyFingerprint = $this->getKey($identifier, 'sign'); $this->keyPassphrases[$keyFingerprint] = $passPhrase; } /** * Adds a recipient to encrypt a copy of the email for. If you exclude a key fingerprint, we * will try to find a matching key based on the identifier. However if no match is found, or * if multiple valid keys are found, this will fail. Specifying a key fingerprint avoids these * issues. * * @param string $identifier * an email address, but could be a key fingerprint, key ID, name, etc. * * @param string $keyFingerprint */ public function addRecipient($identifier, $keyFingerprint = null) { if (!$keyFingerprint) { $keyFingerprint = $this->getKey($identifier, 'encrypt'); } $this->recipientKeys[$identifier] = $keyFingerprint; } /** * @param Swift_Message $message * * @return $this * * @throws Swift_DependencyException * @throws Swift_SwiftException */ public function signMessage(Swift_Message $message) { $originalMessage = $this->createMessage($message); $message->setChildren([]); $message->setEncoder(Swift_DependencyContainer::getInstance()->lookup('mime.rawcontentencoder')); $type = $message->getHeaders()->get('Content-Type'); $type->setValue('multipart/signed'); $type->setParameters([ 'micalg' => sprintf("pgp-%s", strtolower($this->micalg)), 'protocol' => 'application/pgp-signature', 'boundary' => $message->getBoundary() ]); if (!$this->signingKey) { foreach ($message->getFrom() as $key => $value) { $this->addSignature($this->getKey($key, 'sign')); } } if (!$this->signingKey) { throw new Swift_SwiftException('Signing has been enabled, but no signature has been added. Use autoAddSignature() or addSignature()'); } $signedBody = $originalMessage->toString(); $lines = preg_split('/(\r\n|\r|\n)/', rtrim($signedBody)); for ($i=0; $ipgpSignString($signedBody, $this->signingKey); //Swiftmailer is automatically changing content type and this is the hack to prevent it $body = <<getBoundary()} $signedBody --{$message->getBoundary()} Content-Type: application/pgp-signature; name="signature.asc" Content-Description: OpenPGP digital signature Content-Disposition: attachment; filename="signature.asc" $signature --{$message->getBoundary()}-- EOT; $message->setBody($body); if ($this->encrypt) { $signed = sprintf("%s\r\n%s", $message->getHeaders()->get('Content-Type')->toString(), $body); if (!$this->recipientKeys) { foreach ($message->getTo() as $key => $value) { if (!isset($this->recipientKeys[$key])) { $this->addRecipient($key); } } } if (!$this->recipientKeys) { throw new Swift_SwiftException('Encryption has been enabled, but no recipients have been added. Use autoAddRecipients() or addRecipient()'); } //Create body from signed message $encryptedBody = $this->pgpEncryptString($signed, array_keys($this->recipientKeys)); $type = $message->getHeaders()->get('Content-Type'); $type->setValue('multipart/encrypted'); $type->setParameters([ 'protocol' => 'application/pgp-encrypted', 'boundary' => $message->getBoundary() ]); $body = <<getBoundary()} Content-Type: application/pgp-encrypted Content-Description: PGP/MIME version identification Version: 1 --{$message->getBoundary()} Content-Type: application/octet-stream; name="encrypted.asc" Content-Description: OpenPGP encrypted message Content-Disposition: inline; filename="encrypted.asc" $encryptedBody --{$message->getBoundary()}-- EOT; $message->setBody($body); } $messageHeaders = $message->getHeaders(); $messageHeaders->removeAll('Content-Transfer-Encoding'); return $this; } /** * @return array */ public function getAlteredHeaders() { return ['Content-Type', 'Content-Transfer-Encoding', 'Content-Disposition', 'Content-Description']; } /** * @return $this */ public function reset() { return $this; } protected function createMessage(Swift_Message $message) { $mimeEntity = new Swift_Message('', $message->getBody(), $message->getContentType(), $message->getCharset()); $mimeEntity->setChildren($message->getChildren()); $messageHeaders = $mimeEntity->getHeaders(); $messageHeaders->remove('Message-ID'); $messageHeaders->remove('Date'); $messageHeaders->remove('Subject'); $messageHeaders->remove('MIME-Version'); $messageHeaders->remove('To'); $messageHeaders->remove('From'); return $mimeEntity; } /** * @throws Swift_SwiftException */ protected function initGNUPG() { if (!class_exists('gnupg')) { throw new Swift_SwiftException('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 Swift_SwiftException */ 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 Swift_SwiftException('Unable to sign message (perhaps the secret key is encrypted with a passphrase?)'); } /** * @param $plaintext * @param $keyFingerprints * * @return string * * @throws Swift_SwiftException */ protected function pgpEncryptString($plaintext, $keyFingerprints) { $this->gnupg->clearencryptkeys(); foreach ($keyFingerprints as $keyFingerprint) { $this->gnupg->addencryptkey($keyFingerprint); } $this->gnupg->setarmor(1); $encrypted = $this->gnupg->encrypt($plaintext); if ($encrypted) { return $encrypted; } throw new Swift_SwiftException('Unable to encrypt message'); } /** * @param $identifier * @param $purpose * * @return string * * @throws Swift_SwiftException */ 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']; } } } } 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 Swift_SwiftException(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'])); } }