1<?php 2 3/* 4 * This file is part of SwiftMailer. 5 * (c) 2004-2009 Chris Corbyn 6 * 7 * This authentication is for Exchange servers. We support version 1 & 2. 8 * 9 * For the full copyright and license information, please view the LICENSE 10 * file that was distributed with this source code. 11 */ 12 13/** 14 * Handles NTLM authentication. 15 * 16 * @author Ward Peeters <ward@coding-tech.com> 17 */ 18class Swift_Transport_Esmtp_Auth_NTLMAuthenticator implements Swift_Transport_Esmtp_Authenticator 19{ 20 const NTLMSIG = "NTLMSSP\x00"; 21 const DESCONST = 'KGS!@#$%'; 22 23 /** 24 * Get the name of the AUTH mechanism this Authenticator handles. 25 * 26 * @return string 27 */ 28 public function getAuthKeyword() 29 { 30 return 'NTLM'; 31 } 32 33 /** 34 * Try to authenticate the user with $username and $password. 35 * 36 * @param Swift_Transport_SmtpAgent $agent 37 * @param string $username 38 * @param string $password 39 * 40 * @return bool 41 */ 42 public function authenticate(Swift_Transport_SmtpAgent $agent, $username, $password) 43 { 44 if (!function_exists('openssl_random_pseudo_bytes') || !function_exists('openssl_encrypt')) { 45 throw new LogicException('The OpenSSL extension must be enabled to use the NTLM authenticator.'); 46 } 47 48 if (!function_exists('bcmul')) { 49 throw new LogicException('The BCMath functions must be enabled to use the NTLM authenticator.'); 50 } 51 52 try { 53 // execute AUTH command and filter out the code at the beginning 54 // AUTH NTLM xxxx 55 $response = base64_decode(substr(trim($this->sendMessage1($agent)), 4)); 56 57 // extra parameters for our unit cases 58 $timestamp = func_num_args() > 3 ? func_get_arg(3) : $this->getCorrectTimestamp(bcmul(microtime(true), '1000')); 59 $client = func_num_args() > 4 ? func_get_arg(4) : $this->getRandomBytes(8); 60 61 // Message 3 response 62 $this->sendMessage3($response, $username, $password, $timestamp, $client, $agent); 63 64 return true; 65 } catch (Swift_TransportException $e) { 66 $agent->executeCommand("RSET\r\n", array(250)); 67 68 return false; 69 } 70 } 71 72 protected function si2bin($si, $bits = 32) 73 { 74 $bin = null; 75 if ($si >= -pow(2, $bits - 1) && ($si <= pow(2, $bits - 1))) { 76 // positive or zero 77 if ($si >= 0) { 78 $bin = base_convert($si, 10, 2); 79 // pad to $bits bit 80 $bin_length = strlen($bin); 81 if ($bin_length < $bits) { 82 $bin = str_repeat('0', $bits - $bin_length).$bin; 83 } 84 } else { 85 // negative 86 $si = -$si - pow(2, $bits); 87 $bin = base_convert($si, 10, 2); 88 $bin_length = strlen($bin); 89 if ($bin_length > $bits) { 90 $bin = str_repeat('1', $bits - $bin_length).$bin; 91 } 92 } 93 } 94 95 return $bin; 96 } 97 98 /** 99 * Send our auth message and returns the response. 100 * 101 * @param Swift_Transport_SmtpAgent $agent 102 * 103 * @return string SMTP Response 104 */ 105 protected function sendMessage1(Swift_Transport_SmtpAgent $agent) 106 { 107 $message = $this->createMessage1(); 108 109 return $agent->executeCommand(sprintf("AUTH %s %s\r\n", $this->getAuthKeyword(), base64_encode($message)), array(334)); 110 } 111 112 /** 113 * Fetch all details of our response (message 2). 114 * 115 * @param string $response 116 * 117 * @return array our response parsed 118 */ 119 protected function parseMessage2($response) 120 { 121 $responseHex = bin2hex($response); 122 $length = floor(hexdec(substr($responseHex, 28, 4)) / 256) * 2; 123 $offset = floor(hexdec(substr($responseHex, 32, 4)) / 256) * 2; 124 $challenge = $this->hex2bin(substr($responseHex, 48, 16)); 125 $context = $this->hex2bin(substr($responseHex, 64, 16)); 126 $targetInfoH = $this->hex2bin(substr($responseHex, 80, 16)); 127 $targetName = $this->hex2bin(substr($responseHex, $offset, $length)); 128 $offset = floor(hexdec(substr($responseHex, 88, 4)) / 256) * 2; 129 $targetInfoBlock = substr($responseHex, $offset); 130 list($domainName, $serverName, $DNSDomainName, $DNSServerName, $terminatorByte) = $this->readSubBlock($targetInfoBlock); 131 132 return array( 133 $challenge, 134 $context, 135 $targetInfoH, 136 $targetName, 137 $domainName, 138 $serverName, 139 $DNSDomainName, 140 $DNSServerName, 141 $this->hex2bin($targetInfoBlock), 142 $terminatorByte, 143 ); 144 } 145 146 /** 147 * Read the blob information in from message2. 148 * 149 * @param $block 150 * 151 * @return array 152 */ 153 protected function readSubBlock($block) 154 { 155 // remove terminatorByte cause it's always the same 156 $block = substr($block, 0, -8); 157 158 $length = strlen($block); 159 $offset = 0; 160 $data = array(); 161 while ($offset < $length) { 162 $blockLength = hexdec(substr(substr($block, $offset, 8), -4)) / 256; 163 $offset += 8; 164 $data[] = $this->hex2bin(substr($block, $offset, $blockLength * 2)); 165 $offset += $blockLength * 2; 166 } 167 168 if (count($data) == 3) { 169 $data[] = $data[2]; 170 $data[2] = ''; 171 } 172 173 $data[] = $this->createByte('00'); 174 175 return $data; 176 } 177 178 /** 179 * Send our final message with all our data. 180 * 181 * @param string $response Message 1 response (message 2) 182 * @param string $username 183 * @param string $password 184 * @param string $timestamp 185 * @param string $client 186 * @param Swift_Transport_SmtpAgent $agent 187 * @param bool $v2 Use version2 of the protocol 188 * 189 * @return string 190 */ 191 protected function sendMessage3($response, $username, $password, $timestamp, $client, Swift_Transport_SmtpAgent $agent, $v2 = true) 192 { 193 list($domain, $username) = $this->getDomainAndUsername($username); 194 //$challenge, $context, $targetInfoH, $targetName, $domainName, $workstation, $DNSDomainName, $DNSServerName, $blob, $ter 195 list($challenge, , , , , $workstation, , , $blob) = $this->parseMessage2($response); 196 197 if (!$v2) { 198 // LMv1 199 $lmResponse = $this->createLMPassword($password, $challenge); 200 // NTLMv1 201 $ntlmResponse = $this->createNTLMPassword($password, $challenge); 202 } else { 203 // LMv2 204 $lmResponse = $this->createLMv2Password($password, $username, $domain, $challenge, $client); 205 // NTLMv2 206 $ntlmResponse = $this->createNTLMv2Hash($password, $username, $domain, $challenge, $blob, $timestamp, $client); 207 } 208 209 $message = $this->createMessage3($domain, $username, $workstation, $lmResponse, $ntlmResponse); 210 211 return $agent->executeCommand(sprintf("%s\r\n", base64_encode($message)), array(235)); 212 } 213 214 /** 215 * Create our message 1. 216 * 217 * @return string 218 */ 219 protected function createMessage1() 220 { 221 return self::NTLMSIG 222 .$this->createByte('01') // Message 1 223.$this->createByte('0702'); // Flags 224 } 225 226 /** 227 * Create our message 3. 228 * 229 * @param string $domain 230 * @param string $username 231 * @param string $workstation 232 * @param string $lmResponse 233 * @param string $ntlmResponse 234 * 235 * @return string 236 */ 237 protected function createMessage3($domain, $username, $workstation, $lmResponse, $ntlmResponse) 238 { 239 // Create security buffers 240 $domainSec = $this->createSecurityBuffer($domain, 64); 241 $domainInfo = $this->readSecurityBuffer(bin2hex($domainSec)); 242 $userSec = $this->createSecurityBuffer($username, ($domainInfo[0] + $domainInfo[1]) / 2); 243 $userInfo = $this->readSecurityBuffer(bin2hex($userSec)); 244 $workSec = $this->createSecurityBuffer($workstation, ($userInfo[0] + $userInfo[1]) / 2); 245 $workInfo = $this->readSecurityBuffer(bin2hex($workSec)); 246 $lmSec = $this->createSecurityBuffer($lmResponse, ($workInfo[0] + $workInfo[1]) / 2, true); 247 $lmInfo = $this->readSecurityBuffer(bin2hex($lmSec)); 248 $ntlmSec = $this->createSecurityBuffer($ntlmResponse, ($lmInfo[0] + $lmInfo[1]) / 2, true); 249 250 return self::NTLMSIG 251 .$this->createByte('03') // TYPE 3 message 252.$lmSec // LM response header 253.$ntlmSec // NTLM response header 254.$domainSec // Domain header 255.$userSec // User header 256.$workSec // Workstation header 257.$this->createByte('000000009a', 8) // session key header (empty) 258.$this->createByte('01020000') // FLAGS 259.$this->convertTo16bit($domain) // domain name 260.$this->convertTo16bit($username) // username 261.$this->convertTo16bit($workstation) // workstation 262.$lmResponse 263 .$ntlmResponse; 264 } 265 266 /** 267 * @param string $timestamp Epoch timestamp in microseconds 268 * @param string $client Random bytes 269 * @param string $targetInfo 270 * 271 * @return string 272 */ 273 protected function createBlob($timestamp, $client, $targetInfo) 274 { 275 return $this->createByte('0101') 276 .$this->createByte('00') 277 .$timestamp 278 .$client 279 .$this->createByte('00') 280 .$targetInfo 281 .$this->createByte('00'); 282 } 283 284 /** 285 * Get domain and username from our username. 286 * 287 * @example DOMAIN\username 288 * 289 * @param string $name 290 * 291 * @return array 292 */ 293 protected function getDomainAndUsername($name) 294 { 295 if (strpos($name, '\\') !== false) { 296 return explode('\\', $name); 297 } 298 299 list($user, $domain) = explode('@', $name); 300 301 return array($domain, $user); 302 } 303 304 /** 305 * Create LMv1 response. 306 * 307 * @param string $password 308 * @param string $challenge 309 * 310 * @return string 311 */ 312 protected function createLMPassword($password, $challenge) 313 { 314 // FIRST PART 315 $password = $this->createByte(strtoupper($password), 14, false); 316 list($key1, $key2) = str_split($password, 7); 317 318 $desKey1 = $this->createDesKey($key1); 319 $desKey2 = $this->createDesKey($key2); 320 321 $constantDecrypt = $this->createByte($this->desEncrypt(self::DESCONST, $desKey1).$this->desEncrypt(self::DESCONST, $desKey2), 21, false); 322 323 // SECOND PART 324 list($key1, $key2, $key3) = str_split($constantDecrypt, 7); 325 326 $desKey1 = $this->createDesKey($key1); 327 $desKey2 = $this->createDesKey($key2); 328 $desKey3 = $this->createDesKey($key3); 329 330 return $this->desEncrypt($challenge, $desKey1).$this->desEncrypt($challenge, $desKey2).$this->desEncrypt($challenge, $desKey3); 331 } 332 333 /** 334 * Create NTLMv1 response. 335 * 336 * @param string $password 337 * @param string $challenge 338 * 339 * @return string 340 */ 341 protected function createNTLMPassword($password, $challenge) 342 { 343 // FIRST PART 344 $ntlmHash = $this->createByte($this->md4Encrypt($password), 21, false); 345 list($key1, $key2, $key3) = str_split($ntlmHash, 7); 346 347 $desKey1 = $this->createDesKey($key1); 348 $desKey2 = $this->createDesKey($key2); 349 $desKey3 = $this->createDesKey($key3); 350 351 return $this->desEncrypt($challenge, $desKey1).$this->desEncrypt($challenge, $desKey2).$this->desEncrypt($challenge, $desKey3); 352 } 353 354 /** 355 * Convert a normal timestamp to a tenth of a microtime epoch time. 356 * 357 * @param string $time 358 * 359 * @return string 360 */ 361 protected function getCorrectTimestamp($time) 362 { 363 // Get our timestamp (tricky!) 364 bcscale(0); 365 366 $time = number_format($time, 0, '.', ''); // save microtime to string 367 $time = bcadd($time, '11644473600000'); // add epoch time 368 $time = bcmul($time, 10000); // tenths of a microsecond. 369 370 $binary = $this->si2bin($time, 64); // create 64 bit binary string 371 $timestamp = ''; 372 for ($i = 0; $i < 8; ++$i) { 373 $timestamp .= chr(bindec(substr($binary, -(($i + 1) * 8), 8))); 374 } 375 376 return $timestamp; 377 } 378 379 /** 380 * Create LMv2 response. 381 * 382 * @param string $password 383 * @param string $username 384 * @param string $domain 385 * @param string $challenge NTLM Challenge 386 * @param string $client Random string 387 * 388 * @return string 389 */ 390 protected function createLMv2Password($password, $username, $domain, $challenge, $client) 391 { 392 $lmPass = '00'; // by default 00 393 // if $password > 15 than we can't use this method 394 if (strlen($password) <= 15) { 395 $ntlmHash = $this->md4Encrypt($password); 396 $ntml2Hash = $this->md5Encrypt($ntlmHash, $this->convertTo16bit(strtoupper($username).$domain)); 397 398 $lmPass = bin2hex($this->md5Encrypt($ntml2Hash, $challenge.$client).$client); 399 } 400 401 return $this->createByte($lmPass, 24); 402 } 403 404 /** 405 * Create NTLMv2 response. 406 * 407 * @param string $password 408 * @param string $username 409 * @param string $domain 410 * @param string $challenge Hex values 411 * @param string $targetInfo Hex values 412 * @param string $timestamp 413 * @param string $client Random bytes 414 * 415 * @return string 416 * 417 * @see http://davenport.sourceforge.net/ntlm.html#theNtlmResponse 418 */ 419 protected function createNTLMv2Hash($password, $username, $domain, $challenge, $targetInfo, $timestamp, $client) 420 { 421 $ntlmHash = $this->md4Encrypt($password); 422 $ntml2Hash = $this->md5Encrypt($ntlmHash, $this->convertTo16bit(strtoupper($username).$domain)); 423 424 // create blob 425 $blob = $this->createBlob($timestamp, $client, $targetInfo); 426 427 $ntlmv2Response = $this->md5Encrypt($ntml2Hash, $challenge.$blob); 428 429 return $ntlmv2Response.$blob; 430 } 431 432 protected function createDesKey($key) 433 { 434 $material = array(bin2hex($key[0])); 435 $len = strlen($key); 436 for ($i = 1; $i < $len; ++$i) { 437 list($high, $low) = str_split(bin2hex($key[$i])); 438 $v = $this->castToByte(ord($key[$i - 1]) << (7 + 1 - $i) | $this->uRShift(hexdec(dechex(hexdec($high) & 0xf).dechex(hexdec($low) & 0xf)), $i)); 439 $material[] = str_pad(substr(dechex($v), -2), 2, '0', STR_PAD_LEFT); // cast to byte 440 } 441 $material[] = str_pad(substr(dechex($this->castToByte(ord($key[6]) << 1)), -2), 2, '0'); 442 443 // odd parity 444 foreach ($material as $k => $v) { 445 $b = $this->castToByte(hexdec($v)); 446 $needsParity = (($this->uRShift($b, 7) ^ $this->uRShift($b, 6) ^ $this->uRShift($b, 5) 447 ^ $this->uRShift($b, 4) ^ $this->uRShift($b, 3) ^ $this->uRShift($b, 2) 448 ^ $this->uRShift($b, 1)) & 0x01) == 0; 449 450 list($high, $low) = str_split($v); 451 if ($needsParity) { 452 $material[$k] = dechex(hexdec($high) | 0x0).dechex(hexdec($low) | 0x1); 453 } else { 454 $material[$k] = dechex(hexdec($high) & 0xf).dechex(hexdec($low) & 0xe); 455 } 456 } 457 458 return $this->hex2bin(implode('', $material)); 459 } 460 461 /** HELPER FUNCTIONS */ 462 /** 463 * Create our security buffer depending on length and offset. 464 * 465 * @param string $value Value we want to put in 466 * @param int $offset start of value 467 * @param bool $is16 Do we 16bit string or not? 468 * 469 * @return string 470 */ 471 protected function createSecurityBuffer($value, $offset, $is16 = false) 472 { 473 $length = strlen(bin2hex($value)); 474 $length = $is16 ? $length / 2 : $length; 475 $length = $this->createByte(str_pad(dechex($length), 2, '0', STR_PAD_LEFT), 2); 476 477 return $length.$length.$this->createByte(dechex($offset), 4); 478 } 479 480 /** 481 * Read our security buffer to fetch length and offset of our value. 482 * 483 * @param string $value Securitybuffer in hex 484 * 485 * @return array array with length and offset 486 */ 487 protected function readSecurityBuffer($value) 488 { 489 $length = floor(hexdec(substr($value, 0, 4)) / 256) * 2; 490 $offset = floor(hexdec(substr($value, 8, 4)) / 256) * 2; 491 492 return array($length, $offset); 493 } 494 495 /** 496 * Cast to byte java equivalent to (byte). 497 * 498 * @param int $v 499 * 500 * @return int 501 */ 502 protected function castToByte($v) 503 { 504 return (($v + 128) % 256) - 128; 505 } 506 507 /** 508 * Java unsigned right bitwise 509 * $a >>> $b. 510 * 511 * @param int $a 512 * @param int $b 513 * 514 * @return int 515 */ 516 protected function uRShift($a, $b) 517 { 518 if ($b == 0) { 519 return $a; 520 } 521 522 return ($a >> $b) & ~(1 << (8 * PHP_INT_SIZE - 1) >> ($b - 1)); 523 } 524 525 /** 526 * Right padding with 0 to certain length. 527 * 528 * @param string $input 529 * @param int $bytes Length of bytes 530 * @param bool $isHex Did we provided hex value 531 * 532 * @return string 533 */ 534 protected function createByte($input, $bytes = 4, $isHex = true) 535 { 536 if ($isHex) { 537 $byte = $this->hex2bin(str_pad($input, $bytes * 2, '00')); 538 } else { 539 $byte = str_pad($input, $bytes, "\x00"); 540 } 541 542 return $byte; 543 } 544 545 /** 546 * Create random bytes. 547 * 548 * @param $length 549 * 550 * @return string 551 */ 552 protected function getRandomBytes($length) 553 { 554 $bytes = openssl_random_pseudo_bytes($length, $strong); 555 556 if (false !== $bytes && true === $strong) { 557 return $bytes; 558 } 559 560 throw new RuntimeException('OpenSSL did not produce a secure random number.'); 561 } 562 563 /** ENCRYPTION ALGORITHMS */ 564 /** 565 * DES Encryption. 566 * 567 * @param string $value An 8-byte string 568 * @param string $key 569 * 570 * @return string 571 */ 572 protected function desEncrypt($value, $key) 573 { 574 // 1 == OPENSSL_RAW_DATA - but constant is only available as of PHP 5.4. 575 return substr(openssl_encrypt($value, 'DES-ECB', $key, 1), 0, 8); 576 } 577 578 /** 579 * MD5 Encryption. 580 * 581 * @param string $key Encryption key 582 * @param string $msg Message to encrypt 583 * 584 * @return string 585 */ 586 protected function md5Encrypt($key, $msg) 587 { 588 $blocksize = 64; 589 if (strlen($key) > $blocksize) { 590 $key = pack('H*', md5($key)); 591 } 592 593 $key = str_pad($key, $blocksize, "\0"); 594 $ipadk = $key ^ str_repeat("\x36", $blocksize); 595 $opadk = $key ^ str_repeat("\x5c", $blocksize); 596 597 return pack('H*', md5($opadk.pack('H*', md5($ipadk.$msg)))); 598 } 599 600 /** 601 * MD4 Encryption. 602 * 603 * @param string $input 604 * 605 * @return string 606 * 607 * @see http://php.net/manual/en/ref.hash.php 608 */ 609 protected function md4Encrypt($input) 610 { 611 $input = $this->convertTo16bit($input); 612 613 return function_exists('hash') ? $this->hex2bin(hash('md4', $input)) : mhash(MHASH_MD4, $input); 614 } 615 616 /** 617 * Convert UTF-8 to UTF-16. 618 * 619 * @param string $input 620 * 621 * @return string 622 */ 623 protected function convertTo16bit($input) 624 { 625 return iconv('UTF-8', 'UTF-16LE', $input); 626 } 627 628 /** 629 * Hex2bin replacement for < PHP 5.4. 630 * 631 * @param string $hex 632 * 633 * @return string Binary 634 */ 635 protected function hex2bin($hex) 636 { 637 if (function_exists('hex2bin')) { 638 return hex2bin($hex); 639 } else { 640 return pack('H*', $hex); 641 } 642 } 643 644 /** 645 * @param string $message 646 */ 647 protected function debug($message) 648 { 649 $message = bin2hex($message); 650 $messageId = substr($message, 16, 8); 651 echo substr($message, 0, 16)." NTLMSSP Signature<br />\n"; 652 echo $messageId." Type Indicator<br />\n"; 653 654 if ($messageId == '02000000') { 655 $map = array( 656 'Challenge', 657 'Context', 658 'Target Information Security Buffer', 659 'Target Name Data', 660 'NetBIOS Domain Name', 661 'NetBIOS Server Name', 662 'DNS Domain Name', 663 'DNS Server Name', 664 'BLOB', 665 'Target Information Terminator', 666 ); 667 668 $data = $this->parseMessage2($this->hex2bin($message)); 669 670 foreach ($map as $key => $value) { 671 echo bin2hex($data[$key]).' - '.$data[$key].' ||| '.$value."<br />\n"; 672 } 673 } elseif ($messageId == '03000000') { 674 $i = 0; 675 $data[$i++] = substr($message, 24, 16); 676 list($lmLength, $lmOffset) = $this->readSecurityBuffer($data[$i - 1]); 677 678 $data[$i++] = substr($message, 40, 16); 679 list($ntmlLength, $ntmlOffset) = $this->readSecurityBuffer($data[$i - 1]); 680 681 $data[$i++] = substr($message, 56, 16); 682 list($targetLength, $targetOffset) = $this->readSecurityBuffer($data[$i - 1]); 683 684 $data[$i++] = substr($message, 72, 16); 685 list($userLength, $userOffset) = $this->readSecurityBuffer($data[$i - 1]); 686 687 $data[$i++] = substr($message, 88, 16); 688 list($workLength, $workOffset) = $this->readSecurityBuffer($data[$i - 1]); 689 690 $data[$i++] = substr($message, 104, 16); 691 $data[$i++] = substr($message, 120, 8); 692 $data[$i++] = substr($message, $targetOffset, $targetLength); 693 $data[$i++] = substr($message, $userOffset, $userLength); 694 $data[$i++] = substr($message, $workOffset, $workLength); 695 $data[$i++] = substr($message, $lmOffset, $lmLength); 696 $data[$i] = substr($message, $ntmlOffset, $ntmlLength); 697 698 $map = array( 699 'LM Response Security Buffer', 700 'NTLM Response Security Buffer', 701 'Target Name Security Buffer', 702 'User Name Security Buffer', 703 'Workstation Name Security Buffer', 704 'Session Key Security Buffer', 705 'Flags', 706 'Target Name Data', 707 'User Name Data', 708 'Workstation Name Data', 709 'LM Response Data', 710 'NTLM Response Data', 711 ); 712 713 foreach ($map as $key => $value) { 714 echo $data[$key].' - '.$this->hex2bin($data[$key]).' ||| '.$value."<br />\n"; 715 } 716 } 717 718 echo '<br /><br />'; 719 } 720} 721