1<?php
2
3/**
4 * This file is part of the FreeDSx SASL package.
5 *
6 * (c) Chad Sikorra <Chad.Sikorra@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace FreeDSx\Sasl\Encoder;
13
14use FreeDSx\Sasl\Exception\SaslEncodingException;
15use FreeDSx\Sasl\Message;
16use FreeDSx\Sasl\SaslContext;
17
18/**
19 * Responsible for encoding / decoding CRAM-MD5 messages.
20 *
21 * @author Chad Sikorra <Chad.Sikorra@gmail.com>
22 */
23class CramMD5Encoder implements EncoderInterface
24{
25
26    /**
27     * {@inheritDoc}
28     */
29    public function encode(Message $message, SaslContext $context): string
30    {
31        if ($context->isServerMode()) {
32            return $this->encodeServerChallenge($message);
33        } else {
34            return $this->encodeClientResponse($message);
35        }
36    }
37
38    /**
39     * {@inheritDoc}
40     */
41    public function decode(string $data, SaslContext $context): Message
42    {
43        if ($context->isServerMode()) {
44            return $this->decodeClientResponse($data);
45        } else {
46            return $this->decodeServerChallenge($data);
47        }
48    }
49
50    /**
51     * @throws SaslEncodingException
52     */
53    protected function encodeServerChallenge(Message $message): string
54    {
55        if (!$message->has('challenge')) {
56            throw new SaslEncodingException('The server challenge message must contain a "challenge".');
57        }
58        $challenge = $message->get('challenge');
59
60        return '<' . $challenge . '>';
61    }
62
63    /**
64     * @throws SaslEncodingException
65     */
66    protected function encodeClientResponse(Message $message): string
67    {
68        if (!$message->has('username')) {
69            throw new SaslEncodingException('The client response must contain a username.');
70        }
71        if (!$message->has('digest')) {
72            throw new SaslEncodingException('The client response must contain a digest.');
73        }
74        $username = $message->get('username');
75        $digest = $message->get('digest');
76
77        if (!preg_match('/^[0-9a-f]{32}$/', $digest)) {
78            throw new SaslEncodingException('The client digest must be a 16 octet, lower-case, hexadecimal value');
79        }
80
81        return $username . ' ' . $digest;
82    }
83
84    /**
85     * @throws SaslEncodingException
86     */
87    protected function decodeServerChallenge(string $challenge): Message
88    {
89        if (!preg_match('/^<.*>$/', $challenge)) {
90            throw new SaslEncodingException('The server challenge is malformed.');
91        }
92
93        return new Message(['challenge' => $challenge]);
94    }
95
96    /**
97     * @throws SaslEncodingException
98     */
99    protected function decodeClientResponse(string $response): Message
100    {
101        if (!preg_match('/(.*) ([0-9a-f]{32})$/', $response, $matches)) {
102            throw new SaslEncodingException('The client response is malformed.');
103        }
104
105        return new Message([
106            'username' => $matches[1],
107            'digest' => $matches[2],
108        ]);
109    }
110}
111