1<?php
2/**
3 * Copyright (c) 2002-2003 Richard Heyes
4 * Copyright 2011-2017 Horde LLC (http://www.horde.org/)
5 *
6 * This code is based on the original code contained in the PEAR Auth_SASL
7 * package (v0.5.1):
8 *   $Id: DigestMD5.php 294702 2010-02-07 16:03:55Z cweiske $
9 *
10 * That code is covered by the BSD 3-Clause license, as set forth below:
11 * +-----------------------------------------------------------------------+
12 * | Copyright (c) 2002-2003 Richard Heyes                                 |
13 * | All rights reserved.                                                  |
14 * |                                                                       |
15 * | Redistribution and use in source and binary forms, with or without    |
16 * | modification, are permitted provided that the following conditions    |
17 * | are met:                                                              |
18 * |                                                                       |
19 * | o Redistributions of source code must retain the above copyright      |
20 * |   notice, this list of conditions and the following disclaimer.       |
21 * | o Redistributions in binary form must reproduce the above copyright   |
22 * |   notice, this list of conditions and the following disclaimer in the |
23 * |   documentation and/or other materials provided with the distribution.|
24 * | o The names of the authors may not be used to endorse or promote      |
25 * |   products derived from this software without specific prior written  |
26 * |   permission.                                                         |
27 * |                                                                       |
28 * | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS   |
29 * | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT     |
30 * | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
31 * | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT  |
32 * | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
33 * | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT      |
34 * | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
35 * | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
36 * | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT   |
37 * | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
38 * | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.  |
39 * +-----------------------------------------------------------------------+
40 *
41 * @category  Horde
42 * @copyright 2002-2003 Richard Heyes
43 * @copyright 2011-2017 Horde LLC
44 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
45 * @package   Imap_Client
46 */
47
48/**
49 * Provides the code needed to authenticate via the DIGEST-MD5 SASL mechanism
50 * (defined in RFC 2831). This method has been obsoleted by RFC 6331, but
51 * still is in use on legacy servers.
52 *
53 * @author    Richard Heyes <richard@php.net>
54 * @author    Michael Slusarz <slusarz@horde.org>
55 * @copyright 2002-2003 Richard Heyes
56 * @copyright 2011-2017 Horde LLC
57 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
58 * @package   Imap_Client
59 */
60class Horde_Imap_Client_Auth_DigestMD5
61{
62    /**
63     * Digest response components.
64     *
65     * @var string
66     */
67    protected $_response;
68
69    /**
70     * Generate the Digest-MD5 response.
71     *
72     * @param string $id         Authentication id (username).
73     * @param string $pass       Password.
74     * @param string $challenge  The digest challenge sent by the server.
75     * @param string $hostname   The hostname of the machine connecting to.
76     * @param string $service    The service name (e.g. 'imap', 'pop3').
77     *
78     * @throws Horde_Imap_Client_Exception
79     */
80    public function __construct($id, $pass, $challenge, $hostname, $service)
81    {
82        $challenge = $this->_parseChallenge($challenge);
83        $cnonce = $this->_getCnonce();
84        $digest_uri = sprintf('%s/%s', $service, $hostname);
85
86        /* Get response value. */
87        $A1 = sprintf('%s:%s:%s', pack('H32', hash('md5', sprintf('%s:%s:%s', $id, $challenge['realm'], $pass))), $challenge['nonce'], $cnonce);
88        $A2 = 'AUTHENTICATE:' . $digest_uri;
89        $response_value = hash('md5', sprintf('%s:%s:00000001:%s:auth:%s', hash('md5', $A1), $challenge['nonce'], $cnonce, hash('md5', $A2)));
90
91        $this->_response = array(
92            'cnonce' => '"' . $cnonce . '"',
93            'digest-uri' => '"' . $digest_uri . '"',
94            'maxbuf' => $challenge['maxbuf'],
95            'nc' => '00000001',
96            'nonce' => '"' . $challenge['nonce'] . '"',
97            'qop' => 'auth',
98            'response' => $response_value,
99            'username' => '"' . $id . '"'
100        );
101
102        if (strlen($challenge['realm'])) {
103            $this->_response['realm'] = '"' . $challenge['realm'] . '"';
104        }
105    }
106
107    /**
108     * Cooerce to string.
109     *
110     * @return string  The digest response (not base64 encoded).
111     */
112    public function __toString()
113    {
114        $out = array();
115        foreach ($this->_response as $key => $val) {
116            $out[] = $key . '=' . $val;
117        }
118        return implode(',', $out);
119    }
120
121    /**
122     * Return specific digest response directive.
123     *
124     * @return mixed  Requested directive, or null if it does not exist.
125     */
126    public function __get($name)
127    {
128        return isset($this->_response[$name])
129            ? $this->_response[$name]
130            : null;
131    }
132
133    /**
134    * Parses and verifies the digest challenge.
135    *
136    * @param string $challenge  The digest challenge
137    *
138    * @return array  The parsed challenge as an array with directives as keys.
139    *
140    * @throws Horde_Imap_Client_Exception
141    */
142    protected function _parseChallenge($challenge)
143    {
144        $tokens = array(
145            'maxbuf' => 65536,
146            'realm' => ''
147        );
148
149        preg_match_all('/([a-z-]+)=("[^"]+(?<!\\\)"|[^,]+)/i', $challenge, $matches, PREG_SET_ORDER);
150
151        foreach ($matches as $val) {
152            $tokens[$val[1]] = trim($val[2], '"');
153        }
154
155        // Required directives.
156        if (!isset($tokens['nonce']) || !isset($tokens['algorithm'])) {
157            throw new Horde_Imap_Client_Exception(
158                Horde_Imap_Client_Translation::r("Authentication failure."),
159                Horde_Imap_Client_Exception::SERVER_CONNECT
160            );
161        }
162
163        return $tokens;
164    }
165
166    /**
167     * Creates the client nonce for the response
168     *
169     * @return string  The cnonce value.
170     */
171    protected function _getCnonce()
172    {
173        if ((@is_readable('/dev/urandom') &&
174             ($fd = @fopen('/dev/urandom', 'r'))) ||
175            (@is_readable('/dev/random') &&
176             ($fd = @fopen('/dev/random', 'r')))) {
177            $str = fread($fd, 32);
178            fclose($fd);
179        } else {
180            $str = '';
181            for ($i = 0; $i < 32; ++$i) {
182                $str .= chr(mt_rand(0, 255));
183            }
184        }
185
186        return base64_encode($str);
187    }
188
189}
190