4 * This file is part of SwiftMailer.
5 * (c) 2004-2009 Chris Corbyn
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
12 * DomainKey Signer used to apply DomainKeys Signature to a message.
14 * @author Xavier De Cock <xdecock@gmail.com>
16 class Swift_Signers_DomainKeySigner implements Swift_Signers_HeaderSigner
23 protected $privateKey;
30 protected $domainName;
40 * Hash algorithm used.
44 protected $hashAlgorithm = 'rsa-sha1';
47 * Canonisation method.
51 protected $canon = 'simple';
54 * Headers not being signed.
58 protected $ignoredHeaders = [];
65 protected $signerIdentity;
68 * Must we embed signed headers?
72 protected $debugHeaders = false;
76 * Headers used to generate hash.
80 private $signedHeaders = [];
83 * Stores the signature header.
85 * @var Swift_Mime_Headers_ParameterizedHeader
87 protected $domainKeyHeader;
96 private $canonData = '';
98 private $bodyCanonEmptyCounter = 0;
100 private $bodyCanonIgnoreStart = 2;
102 private $bodyCanonSpace = false;
104 private $bodyCanonLastChar = null;
106 private $bodyCanonLine = '';
113 * @param string $privateKey
114 * @param string $domainName
115 * @param string $selector
117 public function __construct($privateKey, $domainName, $selector)
119 $this->privateKey = $privateKey;
120 $this->domainName = $domainName;
121 $this->signerIdentity = '@'.$domainName;
122 $this->selector = $selector;
126 * Resets internal states.
130 public function reset()
132 $this->hashHandler = null;
133 $this->bodyCanonIgnoreStart = 2;
134 $this->bodyCanonEmptyCounter = 0;
135 $this->bodyCanonLastChar = null;
136 $this->bodyCanonSpace = false;
142 * Writes $bytes to the end of the stream.
144 * Writing may not happen immediately if the stream chooses to buffer. If
145 * you want to write these bytes with immediate effect, call {@link commit()}
146 * after calling write().
148 * This method returns the sequence ID of the write (i.e. 1 for first, 2 for
151 * @param string $bytes
155 * @throws Swift_IoException
159 public function write($bytes)
161 $this->canonicalizeBody($bytes);
162 foreach ($this->bound as $is) {
170 * For any bytes that are currently buffered inside the stream, force them
173 * @throws Swift_IoException
177 public function commit()
184 * Attach $is to this stream.
186 * The stream acts as an observer, receiving all data that is written.
187 * All {@link write()} and {@link flushBuffers()} operations will be mirrored.
191 public function bind(Swift_InputByteStream $is)
193 // Don't have to mirror anything
194 $this->bound[] = $is;
200 * Remove an already bound stream.
202 * If $is is not bound, no errors will be raised.
203 * If the stream currently has any buffered data it will be written to $is
204 * before unbinding occurs.
208 public function unbind(Swift_InputByteStream $is)
210 // Don't have to mirror anything
211 foreach ($this->bound as $k => $stream) {
212 if ($stream === $is) {
213 unset($this->bound[$k]);
223 * Flush the contents of the stream (empty it) and set the internal pointer
226 * @throws Swift_IoException
230 public function flushBuffers()
238 * Set hash_algorithm, must be one of rsa-sha256 | rsa-sha1 defaults to rsa-sha256.
240 * @param string $hash
244 public function setHashAlgorithm($hash)
246 $this->hashAlgorithm = 'rsa-sha1';
252 * Set the canonicalization algorithm.
254 * @param string $canon simple | nofws defaults to simple
258 public function setCanon($canon)
260 if ('nofws' == $canon) {
261 $this->canon = 'nofws';
263 $this->canon = 'simple';
270 * Set the signer identity.
272 * @param string $identity
276 public function setSignerIdentity($identity)
278 $this->signerIdentity = $identity;
284 * Enable / disable the DebugHeaders.
290 public function setDebugHeaders($debug)
292 $this->debugHeaders = (bool) $debug;
300 public function startBody()
307 public function endBody()
313 * Returns the list of Headers Tampered by this plugin.
317 public function getAlteredHeaders()
319 if ($this->debugHeaders) {
320 return ['DomainKey-Signature', 'X-DebugHash'];
323 return ['DomainKey-Signature'];
327 * Adds an ignored Header.
329 * @param string $header_name
333 public function ignoreHeader($header_name)
335 $this->ignoredHeaders[strtolower($header_name)] = true;
341 * Set the headers to sign.
345 public function setHeaders(Swift_Mime_SimpleHeaderSet $headers)
348 $this->canonData = '';
349 // Loop through Headers
350 $listHeaders = $headers->listAll();
351 foreach ($listHeaders as $hName) {
352 // Check if we need to ignore Header
353 if (!isset($this->ignoredHeaders[strtolower($hName)])) {
354 if ($headers->has($hName)) {
355 $tmp = $headers->getAll($hName);
356 foreach ($tmp as $header) {
357 if ('' != $header->getFieldBody()) {
358 $this->addHeader($header->toString());
359 $this->signedHeaders[] = $header->getFieldName();
365 $this->endOfHeaders();
371 * Add the signature to the given Headers.
375 public function addSignature(Swift_Mime_SimpleHeaderSet $headers)
377 // Prepare the DomainKey-Signature Header
378 $params = ['a' => $this->hashAlgorithm, 'b' => chunk_split(base64_encode($this->getEncryptedHash()), 73, ' '), 'c' => $this->canon, 'd' => $this->domainName, 'h' => implode(': ', $this->signedHeaders), 'q' => 'dns', 's' => $this->selector];
380 foreach ($params as $k => $v) {
381 $string .= $k.'='.$v.'; ';
383 $string = trim($string);
384 $headers->addTextHeader('DomainKey-Signature', $string);
389 /* Private helpers */
391 protected function addHeader($header)
393 switch ($this->canon) {
395 // Prepare Header and cascade
396 $exploded = explode(':', $header, 2);
397 $name = strtolower(trim($exploded[0]));
398 $value = str_replace("\r\n", '', $exploded[1]);
399 $value = preg_replace("/[ \t][ \t]+/", ' ', $value);
400 $header = $name.':'.trim($value)."\r\n";
405 $this->addToHash($header);
408 protected function endOfHeaders()
410 $this->bodyCanonEmptyCounter = 1;
413 protected function canonicalizeBody($string)
415 $len = \strlen($string);
417 $nofws = ('nofws' == $this->canon);
418 for ($i = 0; $i < $len; ++$i) {
419 if ($this->bodyCanonIgnoreStart > 0) {
420 --$this->bodyCanonIgnoreStart;
423 switch ($string[$i]) {
425 $this->bodyCanonLastChar = "\r";
428 if ("\r" == $this->bodyCanonLastChar) {
430 $this->bodyCanonSpace = false;
432 if ('' == $this->bodyCanonLine) {
433 ++$this->bodyCanonEmptyCounter;
435 $this->bodyCanonLine = '';
440 throw new Swift_SwiftException('Invalid new line sequence in mail found \n without preceding \r');
447 $this->bodyCanonSpace = true;
452 if ($this->bodyCanonEmptyCounter > 0) {
453 $canon .= str_repeat("\r\n", $this->bodyCanonEmptyCounter);
454 $this->bodyCanonEmptyCounter = 0;
456 $this->bodyCanonLine .= $string[$i];
457 $canon .= $string[$i];
460 $this->addToHash($canon);
463 protected function endOfBody()
465 if (\strlen($this->bodyCanonLine) > 0) {
466 $this->addToHash("\r\n");
470 private function addToHash($string)
472 $this->canonData .= $string;
473 hash_update($this->hashHandler, $string);
476 private function startHash()
479 switch ($this->hashAlgorithm) {
481 $this->hashHandler = hash_init('sha1');
484 $this->bodyCanonLine = '';
488 * @throws Swift_SwiftException
492 private function getEncryptedHash()
495 $pkeyId = openssl_get_privatekey($this->privateKey);
497 throw new Swift_SwiftException('Unable to load DomainKey Private Key ['.openssl_error_string().']');
499 if (openssl_sign($this->canonData, $signature, $pkeyId, OPENSSL_ALGO_SHA1)) {
502 throw new Swift_SwiftException('Unable to sign DomainKey Hash ['.openssl_error_string().']');