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