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