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