Passed
Push — develop ( 046a3b...2fdc71 )
by Fabian
02:29
created

DigestMD5::checkToken()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 7

Importance

Changes 0
Metric Value
cc 7
eloc 11
nc 5
nop 3
dl 0
loc 22
ccs 10
cts 10
cp 1
crap 7
rs 8.8333
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Sasl library.
5
 *
6
 * Copyright (c) 2002-2003 Richard Heyes,
7
 *               2014-2021 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
    /**
52
     * Provides the (main) client response for DIGEST-MD5
53
     * requires a few extra parameters than the other
54
     * mechanisms, which are unavoidable.
55
     *
56
     * @param  string $challenge The digest challenge sent by the server
57
     * @return string            The digest response (NOT base64 encoded)
58
     */
59 12
    public function createResponse($challenge = null)
60
    {
61 12
        $parsedChallenge = $this->parseChallenge($challenge);
62 12
        $authzidString = '';
63
64 12
        $authcid  = $this->options->getAuthcid();
65 12
        $pass     = $this->options->getSecret();
66 12
        $authzid  = $this->options->getAuthzid();
67 12
        $service  = $this->options->getService();
68 12
        $hostname = $this->options->getHostname();
69 12
        if (!empty($authzid)) {
70 10
            $authzidString = 'authzid="' . $authzid . '",';
71
        }
72
73 12
        if (!empty($parsedChallenge)) {
74 8
            $cnonce         = $this->generateCnonce();
75 8
            $digestUri      = sprintf('%s/%s', $service, $hostname);
76 8
            $responseValue = $this->getResponseValue(
77
                $authcid,
78
                $pass,
79 8
                $parsedChallenge['realm'],
80 8
                $parsedChallenge['nonce'],
81
                $cnonce,
82
                $digestUri,
83
                $authzid
84
            );
85
86 8
            $realm  = '';
87 8
            if ($parsedChallenge['realm']) {
88 4
                $realm = 'realm="' . $parsedChallenge['realm'] . '",';
89
            }
90
91 8
            return sprintf(
92 8
                'username="%s",%s%snonce="%s",cnonce="%s",nc=00000001,qop=auth,digest-uri="%s",'
93
                . 'response=%s,maxbuf=%d',
94
                $authcid,
95
                $realm,
96
                $authzidString,
97 8
                $parsedChallenge['nonce'],
98
                $cnonce,
99
                $digestUri,
100
                $responseValue,
101 8
                $parsedChallenge['maxbuf']
102
            );
103
        }
104
105 4
        throw new InvalidArgumentException('Invalid digest challenge');
106
    }
107
108
    /**
109
     * Parses and verifies the digest challenge*
110
     *
111
     * @param  string $challenge The digest challenge
112
     * @return array             The parsed challenge as an assoc
113
     *                           array in the form "directive => value".
114
     * @access private
115
     */
116 12
    private function parseChallenge($challenge)
117
    {
118
        /**
119
         * Defaults and required directives
120
         */
121
        $tokens  = array(
122
            'realm'  => '',
123
            'maxbuf' => 65536,
124
        );
125
126 12
        $matches = array();
127 12
        while (preg_match('/^(?<key>[a-z-]+)=(?<value>"[^"]+(?<!\\\)"|[^,]+)/i', $challenge, $matches)) {
128 10
            $match = $matches[0];
129 10
            $key   = $matches['key'];
130 10
            $value = $matches['value'];
131
132 10
            $this->checkToken($tokens, $key, $value);
133
134
            // Remove the just parsed directive from the challenge
135 10
            $challenge = substr($challenge, strlen($match) + 1);
136
        }
137
138
        // Required: nonce, algorithm
139 12
        if (empty($tokens['nonce']) || empty($tokens['algorithm'])) {
140 4
            return array();
141
        }
142
143 8
        return $tokens;
144
    }
145
146
    /**
147
     * Check found token.
148
     *
149
     * @param array  $tokens
150
     * @param string $key
151
     * @param string $value
152
     */
153 12
    private function checkToken(array &$tokens, $key, $value)
154
    {
155
        // Ignore these as per rfc2831
156 12
        if ($key !== 'opaque' && $key !== 'domain') {
157 12
            if (!empty($tokens[$key])) {
158
                // Allowed multiple "realm" and "auth-param"
159 6
                if ('realm' === $key || 'auth-param' === $key) {
160
161
                    // we don't support multiple realms yet
162 4
                    if ('realm' === $key) {
163 2
                        throw new RuntimeException('Multiple realms are not supported');
164
                    }
165
166 2
                    $tokens[$key] = (array) $tokens[$key];
167 2
                    $tokens[$key][] = $this->trim($value);
168
169
                    // Any other multiple instance = failure
170
                } else {
171 4
                    return array();
172
                }
173
            } else {
174 12
                $tokens[$key] = $this->trim($value);
175
            }
176
        }
177
    }
178
179
    /**
180
     *
181
     * @param string $string
182
     * @return string
183
     */
184 10
    private function trim($string)
185
    {
186 10
        return trim($string, '"');
187
    }
188
189
    /**
190
     * Creates the response= part of the digest response
191
     *
192
     * @param  string $authcid    Authentication id (username)
193
     * @param  string $pass       Password
194
     * @param  string $realm      Realm as provided by the server
195
     * @param  string $nonce      Nonce as provided by the server
196
     * @param  string $cnonce     Client nonce
197
     * @param  string $digest_uri The digest-uri= value part of the response
198
     * @param  string $authzid    Authorization id
199
     * @return string             The response= part of the digest response
200
     * @access private
201
     */
202 8
    private function getResponseValue($authcid, $pass, $realm, $nonce, $cnonce, $digest_uri, $authzid = '')
203
    {
204 8
        if ($authzid == '') {
205 2
            $A1 = sprintf('%s:%s:%s', pack('H32', md5(sprintf('%s:%s:%s', $authcid, $realm, $pass))), $nonce, $cnonce);
206
        } else {
207 6
            $A1 = sprintf('%s:%s:%s:%s', pack('H32', md5(sprintf('%s:%s:%s', $authcid, $realm, $pass))), $nonce, $cnonce, $authzid);
208
        }
209 8
        $A2 = 'AUTHENTICATE:' . $digest_uri;
210 8
        return md5(sprintf('%s:%s:00000001:%s:auth:%s', md5($A1), $nonce, $cnonce, md5($A2)));
211
    }
212
}
213