xref: /plugin/pureldap/vendor/freedsx/sasl/src/FreeDSx/Sasl/Encoder/DigestMD5Encoder.php (revision 0b3fd2d31e4d1997548a8fbc53fa771027c4a47f)
1*0b3fd2d3SAndreas Gohr<?php
2*0b3fd2d3SAndreas Gohr
3*0b3fd2d3SAndreas Gohr/**
4*0b3fd2d3SAndreas Gohr * This file is part of the FreeDSx SASL package.
5*0b3fd2d3SAndreas Gohr *
6*0b3fd2d3SAndreas Gohr * (c) Chad Sikorra <Chad.Sikorra@gmail.com>
7*0b3fd2d3SAndreas Gohr *
8*0b3fd2d3SAndreas Gohr * For the full copyright and license information, please view the LICENSE
9*0b3fd2d3SAndreas Gohr * file that was distributed with this source code.
10*0b3fd2d3SAndreas Gohr */
11*0b3fd2d3SAndreas Gohr
12*0b3fd2d3SAndreas Gohrnamespace FreeDSx\Sasl\Encoder;
13*0b3fd2d3SAndreas Gohr
14*0b3fd2d3SAndreas Gohruse FreeDSx\Sasl\Exception\SaslEncodingException;
15*0b3fd2d3SAndreas Gohruse FreeDSx\Sasl\Message;
16*0b3fd2d3SAndreas Gohruse FreeDSx\Sasl\SaslContext;
17*0b3fd2d3SAndreas Gohruse function dechex, explode, implode, preg_match, sprintf, str_pad, strlen, substr;
18*0b3fd2d3SAndreas Gohr
19*0b3fd2d3SAndreas Gohr/**
20*0b3fd2d3SAndreas Gohr * Responsible for encoding / decoding DIGEST-MD5 messages.
21*0b3fd2d3SAndreas Gohr *
22*0b3fd2d3SAndreas Gohr * @author Chad Sikorra <Chad.Sikorra@gmail.com>
23*0b3fd2d3SAndreas Gohr */
24*0b3fd2d3SAndreas Gohrclass DigestMD5Encoder implements EncoderInterface
25*0b3fd2d3SAndreas Gohr{
26*0b3fd2d3SAndreas Gohr    protected const MATCH_KEY = '/(([a-zA-Z-]+)=)/';
27*0b3fd2d3SAndreas Gohr
28*0b3fd2d3SAndreas Gohr    protected const MATCH_QD_STR_VAL = '/("((.*?)(?<!\\\))")/';
29*0b3fd2d3SAndreas Gohr
30*0b3fd2d3SAndreas Gohr    protected const MATCH_DIGITS = '/([0-9]+)/';
31*0b3fd2d3SAndreas Gohr
32*0b3fd2d3SAndreas Gohr    protected const MATCH_ALPHA_NUMERIC = '/([A-Za-z0-9-]+)/';
33*0b3fd2d3SAndreas Gohr
34*0b3fd2d3SAndreas Gohr    protected const MATCH_LHEX = '/([0-9a-fA-F]{1,})/';
35*0b3fd2d3SAndreas Gohr
36*0b3fd2d3SAndreas Gohr    protected const ONCE_ONLY = [
37*0b3fd2d3SAndreas Gohr        'stale',
38*0b3fd2d3SAndreas Gohr        'maxbuf',
39*0b3fd2d3SAndreas Gohr        'charset',
40*0b3fd2d3SAndreas Gohr        'algorithm',
41*0b3fd2d3SAndreas Gohr        'nonce',
42*0b3fd2d3SAndreas Gohr        'cnonce',
43*0b3fd2d3SAndreas Gohr        'nc',
44*0b3fd2d3SAndreas Gohr        'qop',
45*0b3fd2d3SAndreas Gohr        'digest-uri',
46*0b3fd2d3SAndreas Gohr        'response',
47*0b3fd2d3SAndreas Gohr        'cipher',
48*0b3fd2d3SAndreas Gohr    ];
49*0b3fd2d3SAndreas Gohr
50*0b3fd2d3SAndreas Gohr    /**
51*0b3fd2d3SAndreas Gohr     * @var string
52*0b3fd2d3SAndreas Gohr     */
53*0b3fd2d3SAndreas Gohr    protected $binary;
54*0b3fd2d3SAndreas Gohr
55*0b3fd2d3SAndreas Gohr    /**
56*0b3fd2d3SAndreas Gohr     * @var int
57*0b3fd2d3SAndreas Gohr     */
58*0b3fd2d3SAndreas Gohr    protected $pos = 0;
59*0b3fd2d3SAndreas Gohr
60*0b3fd2d3SAndreas Gohr    /**
61*0b3fd2d3SAndreas Gohr     * @var int
62*0b3fd2d3SAndreas Gohr     */
63*0b3fd2d3SAndreas Gohr    protected $length = 0;
64*0b3fd2d3SAndreas Gohr
65*0b3fd2d3SAndreas Gohr    /**
66*0b3fd2d3SAndreas Gohr     * Tracks the number of times a specific option is encountered during decoding.
67*0b3fd2d3SAndreas Gohr     */
68*0b3fd2d3SAndreas Gohr    protected $occurrences = [];
69*0b3fd2d3SAndreas Gohr
70*0b3fd2d3SAndreas Gohr    /**
71*0b3fd2d3SAndreas Gohr     * {@inheritDoc}
72*0b3fd2d3SAndreas Gohr     */
73*0b3fd2d3SAndreas Gohr    public function decode(string $data, SaslContext $context): Message
74*0b3fd2d3SAndreas Gohr    {
75*0b3fd2d3SAndreas Gohr        return $this->parse($data, !$context->isServerMode());
76*0b3fd2d3SAndreas Gohr    }
77*0b3fd2d3SAndreas Gohr
78*0b3fd2d3SAndreas Gohr    /**
79*0b3fd2d3SAndreas Gohr     * {@inheritDoc}
80*0b3fd2d3SAndreas Gohr     */
81*0b3fd2d3SAndreas Gohr    public function encode(Message $message, SaslContext $context): string
82*0b3fd2d3SAndreas Gohr    {
83*0b3fd2d3SAndreas Gohr        $response = '';
84*0b3fd2d3SAndreas Gohr
85*0b3fd2d3SAndreas Gohr        foreach ($message->toArray() as $key => $value) {
86*0b3fd2d3SAndreas Gohr            if ($response !== '') {
87*0b3fd2d3SAndreas Gohr                $response .= ',';
88*0b3fd2d3SAndreas Gohr            }
89*0b3fd2d3SAndreas Gohr            $response .= $key . '=' . $this->encodeOptValue(
90*0b3fd2d3SAndreas Gohr                $key,
91*0b3fd2d3SAndreas Gohr                $value,
92*0b3fd2d3SAndreas Gohr                $context->isServerMode()
93*0b3fd2d3SAndreas Gohr            );
94*0b3fd2d3SAndreas Gohr        }
95*0b3fd2d3SAndreas Gohr
96*0b3fd2d3SAndreas Gohr        return $response;
97*0b3fd2d3SAndreas Gohr    }
98*0b3fd2d3SAndreas Gohr
99*0b3fd2d3SAndreas Gohr    protected function startParsing(string $binary): void
100*0b3fd2d3SAndreas Gohr    {
101*0b3fd2d3SAndreas Gohr        $this->binary = $binary;
102*0b3fd2d3SAndreas Gohr        $this->pos = 0;
103*0b3fd2d3SAndreas Gohr        $this->length = strlen($binary);
104*0b3fd2d3SAndreas Gohr        $this->occurrences = [];
105*0b3fd2d3SAndreas Gohr    }
106*0b3fd2d3SAndreas Gohr
107*0b3fd2d3SAndreas Gohr    protected function endParsing(): void
108*0b3fd2d3SAndreas Gohr    {
109*0b3fd2d3SAndreas Gohr        $this->binary = '';
110*0b3fd2d3SAndreas Gohr        $this->pos = 0;
111*0b3fd2d3SAndreas Gohr        $this->length = 0;
112*0b3fd2d3SAndreas Gohr        $this->occurrences = [];
113*0b3fd2d3SAndreas Gohr    }
114*0b3fd2d3SAndreas Gohr
115*0b3fd2d3SAndreas Gohr    /**
116*0b3fd2d3SAndreas Gohr     * @throws SaslEncodingException
117*0b3fd2d3SAndreas Gohr     */
118*0b3fd2d3SAndreas Gohr    protected function parse(string $digest, bool $isServerMode): Message
119*0b3fd2d3SAndreas Gohr    {
120*0b3fd2d3SAndreas Gohr        $this->startParsing($digest);
121*0b3fd2d3SAndreas Gohr
122*0b3fd2d3SAndreas Gohr        $message = new Message();
123*0b3fd2d3SAndreas Gohr        while ($this->pos < $this->length) {
124*0b3fd2d3SAndreas Gohr            $keyMatches = null;
125*0b3fd2d3SAndreas Gohr            if (!preg_match(self::MATCH_KEY, substr($this->binary, $this->pos), $keyMatches)) {
126*0b3fd2d3SAndreas Gohr                throw new SaslEncodingException('The digest is malformed. Expected a key, but none was found.');
127*0b3fd2d3SAndreas Gohr            }
128*0b3fd2d3SAndreas Gohr            $this->pos += strlen($keyMatches[1]);
129*0b3fd2d3SAndreas Gohr            if (!isset($this->binary[$this->pos])) {
130*0b3fd2d3SAndreas Gohr                throw new SaslEncodingException('Unexpected end of digest. Expected a value following a key.');
131*0b3fd2d3SAndreas Gohr            }
132*0b3fd2d3SAndreas Gohr            $message->set($keyMatches[2], $this->parseOptValue($keyMatches[2], $isServerMode));
133*0b3fd2d3SAndreas Gohr        }
134*0b3fd2d3SAndreas Gohr        $this->endParsing();
135*0b3fd2d3SAndreas Gohr
136*0b3fd2d3SAndreas Gohr        return $message;
137*0b3fd2d3SAndreas Gohr    }
138*0b3fd2d3SAndreas Gohr
139*0b3fd2d3SAndreas Gohr    /**
140*0b3fd2d3SAndreas Gohr     * @return mixed
141*0b3fd2d3SAndreas Gohr     * @throws SaslEncodingException
142*0b3fd2d3SAndreas Gohr     */
143*0b3fd2d3SAndreas Gohr    protected function parseOptValue(string $opt, bool $isServerMode)
144*0b3fd2d3SAndreas Gohr    {
145*0b3fd2d3SAndreas Gohr        $value = null;
146*0b3fd2d3SAndreas Gohr
147*0b3fd2d3SAndreas Gohr        switch ($opt) {
148*0b3fd2d3SAndreas Gohr            case 'realm':
149*0b3fd2d3SAndreas Gohr            case 'nonce':
150*0b3fd2d3SAndreas Gohr            case 'username':
151*0b3fd2d3SAndreas Gohr            case 'cnonce':
152*0b3fd2d3SAndreas Gohr            case 'authzid':
153*0b3fd2d3SAndreas Gohr            case 'digest-uri':
154*0b3fd2d3SAndreas Gohr                $value = $this->parseQuotedValue();
155*0b3fd2d3SAndreas Gohr                break;
156*0b3fd2d3SAndreas Gohr            case 'qop':
157*0b3fd2d3SAndreas Gohr            case 'cipher':
158*0b3fd2d3SAndreas Gohr                if ($isServerMode) {
159*0b3fd2d3SAndreas Gohr                    $value = $this->parseQuotedCommaList();
160*0b3fd2d3SAndreas Gohr                } else {
161*0b3fd2d3SAndreas Gohr                    $value = $this->parseRegex(self::MATCH_ALPHA_NUMERIC, 'The value is malformed.');
162*0b3fd2d3SAndreas Gohr                }
163*0b3fd2d3SAndreas Gohr                break;
164*0b3fd2d3SAndreas Gohr            case 'stale':
165*0b3fd2d3SAndreas Gohr                $value = $this->parseExact('true');
166*0b3fd2d3SAndreas Gohr                break;
167*0b3fd2d3SAndreas Gohr            case 'maxbuf':
168*0b3fd2d3SAndreas Gohr                $value = $this->parseRegex(self::MATCH_DIGITS, 'Expected a series of digits for a key value.');
169*0b3fd2d3SAndreas Gohr                break;
170*0b3fd2d3SAndreas Gohr            case 'algorithm':
171*0b3fd2d3SAndreas Gohr                $value = $this->parseExact('md5-sess');
172*0b3fd2d3SAndreas Gohr                break;
173*0b3fd2d3SAndreas Gohr            case 'charset':
174*0b3fd2d3SAndreas Gohr                $value = $this->parseExact('utf-8');
175*0b3fd2d3SAndreas Gohr                break;
176*0b3fd2d3SAndreas Gohr            case 'nc':
177*0b3fd2d3SAndreas Gohr                $value = $this->parseLHexValue(8);
178*0b3fd2d3SAndreas Gohr                break;
179*0b3fd2d3SAndreas Gohr            case 'response':
180*0b3fd2d3SAndreas Gohr            case 'rspauth':
181*0b3fd2d3SAndreas Gohr                $value = $this->parseLHexValue(32);
182*0b3fd2d3SAndreas Gohr                break;
183*0b3fd2d3SAndreas Gohr            default:
184*0b3fd2d3SAndreas Gohr                throw new SaslEncodingException(sprintf(
185*0b3fd2d3SAndreas Gohr                    'Digest option %s is not supported.',
186*0b3fd2d3SAndreas Gohr                    $opt
187*0b3fd2d3SAndreas Gohr                ));
188*0b3fd2d3SAndreas Gohr                break;
189*0b3fd2d3SAndreas Gohr        }
190*0b3fd2d3SAndreas Gohr
191*0b3fd2d3SAndreas Gohr        if (isset($this->binary[$this->pos]) && $this->binary[$this->pos] !== ',') {
192*0b3fd2d3SAndreas Gohr            throw new SaslEncodingException(sprintf(
193*0b3fd2d3SAndreas Gohr                'Expected a comma following digest value for %s.',
194*0b3fd2d3SAndreas Gohr                $opt
195*0b3fd2d3SAndreas Gohr            ));
196*0b3fd2d3SAndreas Gohr        }
197*0b3fd2d3SAndreas Gohr        if (isset($this->binary[$this->pos]) && $this->binary[$this->pos] === ',') {
198*0b3fd2d3SAndreas Gohr            $this->pos++;
199*0b3fd2d3SAndreas Gohr        }
200*0b3fd2d3SAndreas Gohr
201*0b3fd2d3SAndreas Gohr        if (isset($this->occurrences[$opt]) && in_array($opt, self::ONCE_ONLY, true)) {
202*0b3fd2d3SAndreas Gohr            throw new SaslEncodingException(sprintf('The option "%s" may occur only once.', $opt));
203*0b3fd2d3SAndreas Gohr        } elseif (isset($this->occurrences[$opt])) {
204*0b3fd2d3SAndreas Gohr            $this->occurrences[$opt]++;
205*0b3fd2d3SAndreas Gohr        } else {
206*0b3fd2d3SAndreas Gohr            $this->occurrences[$opt] = 1;
207*0b3fd2d3SAndreas Gohr        }
208*0b3fd2d3SAndreas Gohr
209*0b3fd2d3SAndreas Gohr        return $value;
210*0b3fd2d3SAndreas Gohr    }
211*0b3fd2d3SAndreas Gohr
212*0b3fd2d3SAndreas Gohr    /**
213*0b3fd2d3SAndreas Gohr     * @return mixed
214*0b3fd2d3SAndreas Gohr     * @throws SaslEncodingException
215*0b3fd2d3SAndreas Gohr     */
216*0b3fd2d3SAndreas Gohr    protected function encodeOptValue(string $name, $value, bool $isServerMode)
217*0b3fd2d3SAndreas Gohr    {
218*0b3fd2d3SAndreas Gohr        $encoded = null;
219*0b3fd2d3SAndreas Gohr
220*0b3fd2d3SAndreas Gohr        switch ($name) {
221*0b3fd2d3SAndreas Gohr            case 'realm':
222*0b3fd2d3SAndreas Gohr            case 'nonce':
223*0b3fd2d3SAndreas Gohr            case 'username':
224*0b3fd2d3SAndreas Gohr            case 'cnonce':
225*0b3fd2d3SAndreas Gohr            case 'authzid':
226*0b3fd2d3SAndreas Gohr            case 'digest-uri':
227*0b3fd2d3SAndreas Gohr                $encoded = '"' . str_replace(['\\', '"'], ['\\\\', '\"'], $value) . '"';
228*0b3fd2d3SAndreas Gohr                break;
229*0b3fd2d3SAndreas Gohr            case 'qop':
230*0b3fd2d3SAndreas Gohr            case 'cipher':
231*0b3fd2d3SAndreas Gohr                if ($isServerMode) {
232*0b3fd2d3SAndreas Gohr                    $encoded = '"' . implode(',', (array) $value) . '"';
233*0b3fd2d3SAndreas Gohr                } else {
234*0b3fd2d3SAndreas Gohr                    $encoded = (string) $value;
235*0b3fd2d3SAndreas Gohr                }
236*0b3fd2d3SAndreas Gohr                break;
237*0b3fd2d3SAndreas Gohr            case 'stale':
238*0b3fd2d3SAndreas Gohr                $encoded = 'true';
239*0b3fd2d3SAndreas Gohr                break;
240*0b3fd2d3SAndreas Gohr            case 'maxbuf':
241*0b3fd2d3SAndreas Gohr            case 'algorithm':
242*0b3fd2d3SAndreas Gohr            case 'charset':
243*0b3fd2d3SAndreas Gohr                $encoded = (string) $value;
244*0b3fd2d3SAndreas Gohr                break;
245*0b3fd2d3SAndreas Gohr            case 'nc':
246*0b3fd2d3SAndreas Gohr                $encoded = str_pad(dechex($value), 8, '0', STR_PAD_LEFT);
247*0b3fd2d3SAndreas Gohr                break;
248*0b3fd2d3SAndreas Gohr            case 'response':
249*0b3fd2d3SAndreas Gohr            case 'rspauth':
250*0b3fd2d3SAndreas Gohr                $encoded = $this->encodeLHexValue($value, 32);
251*0b3fd2d3SAndreas Gohr                break;
252*0b3fd2d3SAndreas Gohr            default:
253*0b3fd2d3SAndreas Gohr                throw new SaslEncodingException(sprintf(
254*0b3fd2d3SAndreas Gohr                    'Digest option %s is not supported.',
255*0b3fd2d3SAndreas Gohr                    $name
256*0b3fd2d3SAndreas Gohr                ));
257*0b3fd2d3SAndreas Gohr                break;
258*0b3fd2d3SAndreas Gohr        }
259*0b3fd2d3SAndreas Gohr
260*0b3fd2d3SAndreas Gohr        return $encoded;
261*0b3fd2d3SAndreas Gohr    }
262*0b3fd2d3SAndreas Gohr
263*0b3fd2d3SAndreas Gohr    /**
264*0b3fd2d3SAndreas Gohr     * @throws SaslEncodingException
265*0b3fd2d3SAndreas Gohr     */
266*0b3fd2d3SAndreas Gohr    protected function parseExact(string $expected): string
267*0b3fd2d3SAndreas Gohr    {
268*0b3fd2d3SAndreas Gohr        $length = strlen($expected);
269*0b3fd2d3SAndreas Gohr        if (substr($this->binary, $this->pos, $length) !== $expected) {
270*0b3fd2d3SAndreas Gohr            throw new SaslEncodingException(sprintf(
271*0b3fd2d3SAndreas Gohr                'Expected the directive value to be "%s", but it is not.',
272*0b3fd2d3SAndreas Gohr                $expected
273*0b3fd2d3SAndreas Gohr            ));
274*0b3fd2d3SAndreas Gohr        }
275*0b3fd2d3SAndreas Gohr        $this->pos += $length;
276*0b3fd2d3SAndreas Gohr
277*0b3fd2d3SAndreas Gohr        return $expected;
278*0b3fd2d3SAndreas Gohr    }
279*0b3fd2d3SAndreas Gohr
280*0b3fd2d3SAndreas Gohr    /**
281*0b3fd2d3SAndreas Gohr     * @throws SaslEncodingException
282*0b3fd2d3SAndreas Gohr     */
283*0b3fd2d3SAndreas Gohr    protected function parseQuotedValue(): string
284*0b3fd2d3SAndreas Gohr    {
285*0b3fd2d3SAndreas Gohr        if (!preg_match(self::MATCH_QD_STR_VAL, substr($this->binary, $this->pos), $matches)) {
286*0b3fd2d3SAndreas Gohr            throw new SaslEncodingException('The value is malformed. Expected a qdstr-val.');
287*0b3fd2d3SAndreas Gohr        }
288*0b3fd2d3SAndreas Gohr        $this->pos += strlen($matches[1]);
289*0b3fd2d3SAndreas Gohr
290*0b3fd2d3SAndreas Gohr        return stripslashes($matches[2]);
291*0b3fd2d3SAndreas Gohr    }
292*0b3fd2d3SAndreas Gohr
293*0b3fd2d3SAndreas Gohr    /**
294*0b3fd2d3SAndreas Gohr     * @throws SaslEncodingException
295*0b3fd2d3SAndreas Gohr     */
296*0b3fd2d3SAndreas Gohr    protected function parseQuotedCommaList(): array
297*0b3fd2d3SAndreas Gohr    {
298*0b3fd2d3SAndreas Gohr        $value = $this->parseQuotedValue();
299*0b3fd2d3SAndreas Gohr
300*0b3fd2d3SAndreas Gohr        return explode(',', $value);
301*0b3fd2d3SAndreas Gohr    }
302*0b3fd2d3SAndreas Gohr
303*0b3fd2d3SAndreas Gohr    /**
304*0b3fd2d3SAndreas Gohr     * @throws SaslEncodingException
305*0b3fd2d3SAndreas Gohr     */
306*0b3fd2d3SAndreas Gohr    protected function parseLHexValue(int $length): string
307*0b3fd2d3SAndreas Gohr    {
308*0b3fd2d3SAndreas Gohr        if (!preg_match(self::MATCH_LHEX, substr($this->binary, $this->pos), $matches)) {
309*0b3fd2d3SAndreas Gohr            throw new SaslEncodingException('Expected a hex value.');
310*0b3fd2d3SAndreas Gohr        }
311*0b3fd2d3SAndreas Gohr        if (strlen($matches[1]) !== $length) {
312*0b3fd2d3SAndreas Gohr            throw new SaslEncodingException(sprintf('Expected the hex value to be %s characters long.', $length));
313*0b3fd2d3SAndreas Gohr        }
314*0b3fd2d3SAndreas Gohr        $this->pos += strlen($matches[1]);
315*0b3fd2d3SAndreas Gohr
316*0b3fd2d3SAndreas Gohr        return $matches[1];
317*0b3fd2d3SAndreas Gohr    }
318*0b3fd2d3SAndreas Gohr
319*0b3fd2d3SAndreas Gohr    /**
320*0b3fd2d3SAndreas Gohr     * @throws SaslEncodingException
321*0b3fd2d3SAndreas Gohr     */
322*0b3fd2d3SAndreas Gohr    protected function parseRegex(string $regex, string $errorMessage)
323*0b3fd2d3SAndreas Gohr    {
324*0b3fd2d3SAndreas Gohr        if (!preg_match($regex, substr($this->binary, $this->pos), $matches)) {
325*0b3fd2d3SAndreas Gohr            throw new SaslEncodingException($errorMessage);
326*0b3fd2d3SAndreas Gohr        }
327*0b3fd2d3SAndreas Gohr        $this->pos += strlen($matches[1]);
328*0b3fd2d3SAndreas Gohr
329*0b3fd2d3SAndreas Gohr        return $matches[1];
330*0b3fd2d3SAndreas Gohr    }
331*0b3fd2d3SAndreas Gohr
332*0b3fd2d3SAndreas Gohr    /**
333*0b3fd2d3SAndreas Gohr     * @throws SaslEncodingException
334*0b3fd2d3SAndreas Gohr     */
335*0b3fd2d3SAndreas Gohr    protected function encodeLHexValue(string $data, int $length): string
336*0b3fd2d3SAndreas Gohr    {
337*0b3fd2d3SAndreas Gohr        if (strlen($data) !== $length) {
338*0b3fd2d3SAndreas Gohr            throw new SaslEncodingException(sprintf('Expected the encoded hex value to be %s characters long.', $length));
339*0b3fd2d3SAndreas Gohr        }
340*0b3fd2d3SAndreas Gohr
341*0b3fd2d3SAndreas Gohr        return $data;
342*0b3fd2d3SAndreas Gohr    }
343*0b3fd2d3SAndreas Gohr}
344