1<?php 2 3/* 4 * This file is part of SwiftMailer. 5 * (c) 2004-2009 Chris Corbyn 6 * 7 * For the full copyright and license information, please view the LICENSE 8 * file that was distributed with this source code. 9 */ 10 11/** 12 * DomainKey Signer used to apply DomainKeys Signature to a message. 13 * 14 * @author Xavier De Cock <xdecock@gmail.com> 15 */ 16class Swift_Signers_DomainKeySigner implements Swift_Signers_HeaderSigner 17{ 18 /** 19 * PrivateKey. 20 * 21 * @var string 22 */ 23 protected $_privateKey; 24 25 /** 26 * DomainName. 27 * 28 * @var string 29 */ 30 protected $_domainName; 31 32 /** 33 * Selector. 34 * 35 * @var string 36 */ 37 protected $_selector; 38 39 /** 40 * Hash algorithm used. 41 * 42 * @var string 43 */ 44 protected $_hashAlgorithm = 'rsa-sha1'; 45 46 /** 47 * Canonisation method. 48 * 49 * @var string 50 */ 51 protected $_canon = 'simple'; 52 53 /** 54 * Headers not being signed. 55 * 56 * @var array 57 */ 58 protected $_ignoredHeaders = array(); 59 60 /** 61 * Signer identity. 62 * 63 * @var string 64 */ 65 protected $_signerIdentity; 66 67 /** 68 * Must we embed signed headers? 69 * 70 * @var bool 71 */ 72 protected $_debugHeaders = false; 73 74 // work variables 75 /** 76 * Headers used to generate hash. 77 * 78 * @var array 79 */ 80 private $_signedHeaders = array(); 81 82 /** 83 * Stores the signature header. 84 * 85 * @var Swift_Mime_Headers_ParameterizedHeader 86 */ 87 protected $_domainKeyHeader; 88 89 /** 90 * Hash Handler. 91 * 92 * @var resource|null 93 */ 94 private $_hashHandler; 95 96 private $_hash; 97 98 private $_canonData = ''; 99 100 private $_bodyCanonEmptyCounter = 0; 101 102 private $_bodyCanonIgnoreStart = 2; 103 104 private $_bodyCanonSpace = false; 105 106 private $_bodyCanonLastChar = null; 107 108 private $_bodyCanonLine = ''; 109 110 private $_bound = array(); 111 112 /** 113 * Constructor. 114 * 115 * @param string $privateKey 116 * @param string $domainName 117 * @param string $selector 118 */ 119 public function __construct($privateKey, $domainName, $selector) 120 { 121 $this->_privateKey = $privateKey; 122 $this->_domainName = $domainName; 123 $this->_signerIdentity = '@'.$domainName; 124 $this->_selector = $selector; 125 } 126 127 /** 128 * Instanciate DomainKeySigner. 129 * 130 * @param string $privateKey 131 * @param string $domainName 132 * @param string $selector 133 * 134 * @return self 135 */ 136 public static function newInstance($privateKey, $domainName, $selector) 137 { 138 return new static($privateKey, $domainName, $selector); 139 } 140 141 /** 142 * Resets internal states. 143 * 144 * @return $this 145 */ 146 public function reset() 147 { 148 $this->_hash = null; 149 $this->_hashHandler = null; 150 $this->_bodyCanonIgnoreStart = 2; 151 $this->_bodyCanonEmptyCounter = 0; 152 $this->_bodyCanonLastChar = null; 153 $this->_bodyCanonSpace = false; 154 155 return $this; 156 } 157 158 /** 159 * Writes $bytes to the end of the stream. 160 * 161 * Writing may not happen immediately if the stream chooses to buffer. If 162 * you want to write these bytes with immediate effect, call {@link commit()} 163 * after calling write(). 164 * 165 * This method returns the sequence ID of the write (i.e. 1 for first, 2 for 166 * second, etc etc). 167 * 168 * @param string $bytes 169 * 170 * @throws Swift_IoException 171 * 172 * @return $this 173 */ 174 public function write($bytes) 175 { 176 $this->_canonicalizeBody($bytes); 177 foreach ($this->_bound as $is) { 178 $is->write($bytes); 179 } 180 181 return $this; 182 } 183 184 /** 185 * For any bytes that are currently buffered inside the stream, force them 186 * off the buffer. 187 * 188 * @throws Swift_IoException 189 * 190 * @return $this 191 */ 192 public function commit() 193 { 194 // Nothing to do 195 return $this; 196 } 197 198 /** 199 * Attach $is to this stream. 200 * The stream acts as an observer, receiving all data that is written. 201 * All {@link write()} and {@link flushBuffers()} operations will be mirrored. 202 * 203 * @param Swift_InputByteStream $is 204 * 205 * @return $this 206 */ 207 public function bind(Swift_InputByteStream $is) 208 { 209 // Don't have to mirror anything 210 $this->_bound[] = $is; 211 212 return $this; 213 } 214 215 /** 216 * Remove an already bound stream. 217 * If $is is not bound, no errors will be raised. 218 * If the stream currently has any buffered data it will be written to $is 219 * before unbinding occurs. 220 * 221 * @param Swift_InputByteStream $is 222 * 223 * @return $this 224 */ 225 public function unbind(Swift_InputByteStream $is) 226 { 227 // Don't have to mirror anything 228 foreach ($this->_bound as $k => $stream) { 229 if ($stream === $is) { 230 unset($this->_bound[$k]); 231 232 break; 233 } 234 } 235 236 return $this; 237 } 238 239 /** 240 * Flush the contents of the stream (empty it) and set the internal pointer 241 * to the beginning. 242 * 243 * @throws Swift_IoException 244 * 245 * @return $this 246 */ 247 public function flushBuffers() 248 { 249 $this->reset(); 250 251 return $this; 252 } 253 254 /** 255 * Set hash_algorithm, must be one of rsa-sha256 | rsa-sha1 defaults to rsa-sha256. 256 * 257 * @param string $hash 258 * 259 * @return $this 260 */ 261 public function setHashAlgorithm($hash) 262 { 263 $this->_hashAlgorithm = 'rsa-sha1'; 264 265 return $this; 266 } 267 268 /** 269 * Set the canonicalization algorithm. 270 * 271 * @param string $canon simple | nofws defaults to simple 272 * 273 * @return $this 274 */ 275 public function setCanon($canon) 276 { 277 if ($canon == 'nofws') { 278 $this->_canon = 'nofws'; 279 } else { 280 $this->_canon = 'simple'; 281 } 282 283 return $this; 284 } 285 286 /** 287 * Set the signer identity. 288 * 289 * @param string $identity 290 * 291 * @return $this 292 */ 293 public function setSignerIdentity($identity) 294 { 295 $this->_signerIdentity = $identity; 296 297 return $this; 298 } 299 300 /** 301 * Enable / disable the DebugHeaders. 302 * 303 * @param bool $debug 304 * 305 * @return $this 306 */ 307 public function setDebugHeaders($debug) 308 { 309 $this->_debugHeaders = (bool) $debug; 310 311 return $this; 312 } 313 314 /** 315 * Start Body. 316 */ 317 public function startBody() 318 { 319 } 320 321 /** 322 * End Body. 323 */ 324 public function endBody() 325 { 326 $this->_endOfBody(); 327 } 328 329 /** 330 * Returns the list of Headers Tampered by this plugin. 331 * 332 * @return array 333 */ 334 public function getAlteredHeaders() 335 { 336 if ($this->_debugHeaders) { 337 return array('DomainKey-Signature', 'X-DebugHash'); 338 } 339 340 return array('DomainKey-Signature'); 341 } 342 343 /** 344 * Adds an ignored Header. 345 * 346 * @param string $header_name 347 * 348 * @return $this 349 */ 350 public function ignoreHeader($header_name) 351 { 352 $this->_ignoredHeaders[strtolower($header_name)] = true; 353 354 return $this; 355 } 356 357 /** 358 * Set the headers to sign. 359 * 360 * @param Swift_Mime_HeaderSet $headers 361 * 362 * @return $this 363 */ 364 public function setHeaders(Swift_Mime_HeaderSet $headers) 365 { 366 $this->_startHash(); 367 $this->_canonData = ''; 368 // Loop through Headers 369 $listHeaders = $headers->listAll(); 370 foreach ($listHeaders as $hName) { 371 // Check if we need to ignore Header 372 if (!isset($this->_ignoredHeaders[strtolower($hName)])) { 373 if ($headers->has($hName)) { 374 $tmp = $headers->getAll($hName); 375 foreach ($tmp as $header) { 376 if ($header->getFieldBody() != '') { 377 $this->_addHeader($header->toString()); 378 $this->_signedHeaders[] = $header->getFieldName(); 379 } 380 } 381 } 382 } 383 } 384 $this->_endOfHeaders(); 385 386 return $this; 387 } 388 389 /** 390 * Add the signature to the given Headers. 391 * 392 * @param Swift_Mime_HeaderSet $headers 393 * 394 * @return $this 395 */ 396 public function addSignature(Swift_Mime_HeaderSet $headers) 397 { 398 // Prepare the DomainKey-Signature Header 399 $params = array('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); 400 $string = ''; 401 foreach ($params as $k => $v) { 402 $string .= $k.'='.$v.'; '; 403 } 404 $string = trim($string); 405 $headers->addTextHeader('DomainKey-Signature', $string); 406 407 return $this; 408 } 409 410 /* Private helpers */ 411 412 protected function _addHeader($header) 413 { 414 switch ($this->_canon) { 415 case 'nofws': 416 // Prepare Header and cascade 417 $exploded = explode(':', $header, 2); 418 $name = strtolower(trim($exploded[0])); 419 $value = str_replace("\r\n", '', $exploded[1]); 420 $value = preg_replace("/[ \t][ \t]+/", ' ', $value); 421 $header = $name.':'.trim($value)."\r\n"; 422 case 'simple': 423 // Nothing to do 424 } 425 $this->_addToHash($header); 426 } 427 428 protected function _endOfHeaders() 429 { 430 $this->_bodyCanonEmptyCounter = 1; 431 } 432 433 protected function _canonicalizeBody($string) 434 { 435 $len = strlen($string); 436 $canon = ''; 437 $nofws = ($this->_canon == 'nofws'); 438 for ($i = 0; $i < $len; ++$i) { 439 if ($this->_bodyCanonIgnoreStart > 0) { 440 --$this->_bodyCanonIgnoreStart; 441 continue; 442 } 443 switch ($string[$i]) { 444 case "\r": 445 $this->_bodyCanonLastChar = "\r"; 446 break; 447 case "\n": 448 if ($this->_bodyCanonLastChar == "\r") { 449 if ($nofws) { 450 $this->_bodyCanonSpace = false; 451 } 452 if ($this->_bodyCanonLine == '') { 453 ++$this->_bodyCanonEmptyCounter; 454 } else { 455 $this->_bodyCanonLine = ''; 456 $canon .= "\r\n"; 457 } 458 } else { 459 // Wooops Error 460 throw new Swift_SwiftException('Invalid new line sequence in mail found \n without preceding \r'); 461 } 462 break; 463 case ' ': 464 case "\t": 465 case "\x09": //HTAB 466 if ($nofws) { 467 $this->_bodyCanonSpace = true; 468 break; 469 } 470 default: 471 if ($this->_bodyCanonEmptyCounter > 0) { 472 $canon .= str_repeat("\r\n", $this->_bodyCanonEmptyCounter); 473 $this->_bodyCanonEmptyCounter = 0; 474 } 475 $this->_bodyCanonLine .= $string[$i]; 476 $canon .= $string[$i]; 477 } 478 } 479 $this->_addToHash($canon); 480 } 481 482 protected function _endOfBody() 483 { 484 if (strlen($this->_bodyCanonLine) > 0) { 485 $this->_addToHash("\r\n"); 486 } 487 $this->_hash = hash_final($this->_hashHandler, true); 488 } 489 490 private function _addToHash($string) 491 { 492 $this->_canonData .= $string; 493 hash_update($this->_hashHandler, $string); 494 } 495 496 private function _startHash() 497 { 498 // Init 499 switch ($this->_hashAlgorithm) { 500 case 'rsa-sha1': 501 $this->_hashHandler = hash_init('sha1'); 502 break; 503 } 504 $this->_bodyCanonLine = ''; 505 } 506 507 /** 508 * @throws Swift_SwiftException 509 * 510 * @return string 511 */ 512 private function _getEncryptedHash() 513 { 514 $signature = ''; 515 $pkeyId = openssl_get_privatekey($this->_privateKey); 516 if (!$pkeyId) { 517 throw new Swift_SwiftException('Unable to load DomainKey Private Key ['.openssl_error_string().']'); 518 } 519 if (openssl_sign($this->_canonData, $signature, $pkeyId, OPENSSL_ALGO_SHA1)) { 520 return $signature; 521 } 522 throw new Swift_SwiftException('Unable to sign DomainKey Hash ['.openssl_error_string().']'); 523 } 524} 525