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