DigestMD5::checkToken()   B
last analyzed

Complexity

Conditions 7
Paths 5

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 7

Importance

Changes 0
Metric Value
cc 7
eloc 11
c 0
b 0
f 0
nc 5
nop 3
dl 0
loc 21
ccs 10
cts 10
cp 1
crap 7
rs 8.8333
1
<?php
2
3
/**
4
 * Sasl library.
5
 *
6
 * Copyright (c) 2002-2003 Richard Heyes,
7
 *               2014-2024 Fabian Grutschus
8
 * All rights reserved.
9
 *
10
 * Redistribution and use in source and binary forms, with or without
11
 * modification, are permitted provided that the following conditions
12
 * are met:
13
 *
14
 * o Redistributions of source code must retain the above copyright
15
 *   notice, this list of conditions and the following disclaimer.
16
 * o Redistributions in binary form must reproduce the above copyright
17
 *   notice, this list of conditions and the following disclaimer in the
18
 *   documentation and/or other materials provided with the distribution.|
19
 * o The names of the authors may not be used to endorse or promote
20
 *   products derived from this software without specific prior written
21
 *   permission.
22
 *
23
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
24
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
25
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
26
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
27
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
28
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
29
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
30
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
31
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
32
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
33
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34
 *
35
 * @author Richard Heyes <[email protected]>
36
 */
37
38
namespace Fabiang\Sasl\Authentication;
39
40
use Fabiang\Sasl\Exception\InvalidArgumentException;
41
use Fabiang\Sasl\Exception\RuntimeException;
42
43
/**
44
 * Implmentation of DIGEST-MD5 SASL mechanism
45
 *
46
 * @author Richard Heyes <[email protected]>
47
 */
48
class DigestMD5 extends AbstractAuthentication implements ChallengeAuthenticationInterface
49
{
50
    /**
51
     * Provides the (main) client response for DIGEST-MD5
52
     * requires a few extra parameters than the other
53
     * mechanisms, which are unavoidable.
54
     *
55
     * @param  string $challenge The digest challenge sent by the server
56
     * @return string            The digest response (NOT base64 encoded)
57
     */
58 12
    public function createResponse($challenge = null)
59
    {
60 12
        $parsedChallenge = $this->parseChallenge($challenge);
61 12
        $authzidString = '';
62
63 12
        $authcid  = $this->options->getAuthcid();
64 12
        $pass     = $this->options->getSecret();
65 12
        $authzid  = $this->options->getAuthzid();
66 12
        $service  = $this->options->getService();
67 12
        $hostname = $this->options->getHostname();
68 12
        if (!empty($authzid)) {
69 10
            $authzidString = 'authzid="' . $authzid . '",';
70
        }
71
72 12
        if (!empty($parsedChallenge)) {
73 8
            $cnonce         = $this->generateCnonce();
74 8
            $digestUri      = sprintf('%s/%s', $service, $hostname);
75 8
            $responseValue = $this->getResponseValue(
76
                $authcid,
77
                $pass,
78 8
                $parsedChallenge['realm'],
79 8
                $parsedChallenge['nonce'],
80
                $cnonce,
81
                $digestUri,
82
                $authzid
83
            );
84
85 8
            $realm  = '';
86 8
            if ($parsedChallenge['realm']) {
87 4
                $realm = 'realm="' . $parsedChallenge['realm'] . '",';
88
            }
89
90 8
            return sprintf(
91 8
                'username="%s",%s%snonce="%s",cnonce="%s",nc=00000001,qop=auth,digest-uri="%s",'
92
                . 'response=%s,maxbuf=%d',
93
                $authcid,
94
                $realm,
95
                $authzidString,
96 8
                $parsedChallenge['nonce'],
97
                $cnonce,
98
                $digestUri,
99
                $responseValue,
100 8
                $parsedChallenge['maxbuf']
101
            );
102
        }
103
104 4
        throw new InvalidArgumentException('Invalid digest challenge');
105
    }
106
107
    /**
108
     * Parses and verifies the digest challenge*
109
     *
110
     * @param  string $challenge The digest challenge
111
     * @return array             The parsed challenge as an assoc
112
     *                           array in the form "directive => value".
113
     * @access private
114
     */
115 12
    private function parseChallenge($challenge)
116
    {
117
        /**
118
         * Defaults and required directives
119
         */
120
        $tokens  = array(
121
            'realm'  => '',
122
            'maxbuf' => 65536,
123
        );
124
125 12
        $matches = array();
126 12
        while (preg_match('/^(?<key>[a-z-]+)=(?<value>"[^"]+(?<!\\\)"|[^,]+)/i', $challenge, $matches)) {
127 10
            $match = $matches[0];
128 10
            $key   = $matches['key'];
129 10
            $value = $matches['value'];
130
131 10
            $this->checkToken($tokens, $key, $value);
132
133
            // Remove the just parsed directive from the challenge
134 10
            $challenge = substr($challenge, strlen($match) + 1);
135
        }
136
137
        // Required: nonce, algorithm
138 12
        if (empty($tokens['nonce']) || empty($tokens['algorithm'])) {
139 4
            return array();
140
        }
141
142 8
        return $tokens;
143
    }
144
145
    /**
146
     * Check found token.
147
     *
148
     * @param array  $tokens
149
     * @param string $key
150
     * @param string $value
151
     */
152 12
    private function checkToken(array &$tokens, $key, $value)
153
    {
154
        // Ignore these as per rfc2831
155 12
        if ($key !== 'opaque' && $key !== 'domain') {
156 12
            if (!empty($tokens[$key])) {
157
                // Allowed multiple "realm" and "auth-param"
158 6
                if ('realm' === $key || 'auth-param' === $key) {
159
                    // we don't support multiple realms yet
160 4
                    if ('realm' === $key) {
161 2
                        throw new RuntimeException('Multiple realms are not supported');
162
                    }
163
164 2
                    $tokens[$key] = (array) $tokens[$key];
165 2
                    $tokens[$key][] = $this->trim($value);
166
167
                    // Any other multiple instance = failure
168
                } else {
169 4
                    return array();
170
                }
171
            } else {
172 12
                $tokens[$key] = $this->trim($value);
173
            }
174
        }
175
    }
176
177
    /**
178
     *
179
     * @param string $string
180
     * @return string
181
     */
182 10
    private function trim($string)
183
    {
184 10
        return trim($string, '"');
185
    }
186
187
    /**
188
     * Creates the response= part of the digest response
189
     *
190
     * @param  string $authcid    Authentication id (username)
191
     * @param  string $pass       Password
192
     * @param  string $realm      Realm as provided by the server
193
     * @param  string $nonce      Nonce as provided by the server
194
     * @param  string $cnonce     Client nonce
195
     * @param  string $digest_uri The digest-uri= value part of the response
196
     * @param  string $authzid    Authorization id
197
     * @return string             The response= part of the digest response
198
     * @access private
199
     */
200 8
    private function getResponseValue($authcid, $pass, $realm, $nonce, $cnonce, $digest_uri, $authzid = '')
201
    {
202 8
        if ($authzid == '') {
203 2
            $A1 = sprintf('%s:%s:%s', pack('H32', md5(sprintf('%s:%s:%s', $authcid, $realm, $pass))), $nonce, $cnonce);
204
        } else {
205 6
            $A1 = sprintf(
206
                '%s:%s:%s:%s',
207 6
                pack('H32', md5(sprintf('%s:%s:%s', $authcid, $realm, $pass))),
208
                $nonce,
209
                $cnonce,
210
                $authzid
211
            );
212
        }
213 8
        $A2 = 'AUTHENTICATE:' . $digest_uri;
214 8
        return md5(sprintf('%s:%s:00000001:%s:auth:%s', md5($A1), $nonce, $cnonce, md5($A2)));
215
    }
216
}
217