xref: /plugin/pureldap/vendor/freedsx/sasl/src/FreeDSx/Sasl/Challenge/DigestMD5Challenge.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\Challenge;
13*0b3fd2d3SAndreas Gohr
14*0b3fd2d3SAndreas Gohruse FreeDSx\Sasl\Encoder\DigestMD5Encoder;
15*0b3fd2d3SAndreas Gohruse FreeDSx\Sasl\Exception\SaslException;
16*0b3fd2d3SAndreas Gohruse FreeDSx\Sasl\Factory\DigestMD5MessageFactory;
17*0b3fd2d3SAndreas Gohruse FreeDSx\Sasl\Mechanism\DigestMD5Mechanism;
18*0b3fd2d3SAndreas Gohruse FreeDSx\Sasl\Message;
19*0b3fd2d3SAndreas Gohruse FreeDSx\Sasl\SaslContext;
20*0b3fd2d3SAndreas Gohr
21*0b3fd2d3SAndreas Gohr/**
22*0b3fd2d3SAndreas Gohr * The DIGEST-MD5 challenge / response class.
23*0b3fd2d3SAndreas Gohr *
24*0b3fd2d3SAndreas Gohr * @author Chad Sikorra <Chad.Sikorra@gmail.com>
25*0b3fd2d3SAndreas Gohr */
26*0b3fd2d3SAndreas Gohrclass DigestMD5Challenge implements ChallengeInterface
27*0b3fd2d3SAndreas Gohr{
28*0b3fd2d3SAndreas Gohr    /**
29*0b3fd2d3SAndreas Gohr     * @var array
30*0b3fd2d3SAndreas Gohr     */
31*0b3fd2d3SAndreas Gohr    protected const DEFAULTS = [
32*0b3fd2d3SAndreas Gohr        'use_integrity' => false,
33*0b3fd2d3SAndreas Gohr        'use_privacy' => false,
34*0b3fd2d3SAndreas Gohr        'service' => 'ldap',
35*0b3fd2d3SAndreas Gohr        'nonce_size' => null,
36*0b3fd2d3SAndreas Gohr    ];
37*0b3fd2d3SAndreas Gohr
38*0b3fd2d3SAndreas Gohr    /**
39*0b3fd2d3SAndreas Gohr     * @var SaslContext
40*0b3fd2d3SAndreas Gohr     */
41*0b3fd2d3SAndreas Gohr    protected $context;
42*0b3fd2d3SAndreas Gohr
43*0b3fd2d3SAndreas Gohr    /**
44*0b3fd2d3SAndreas Gohr     * @var DigestMD5MessageFactory
45*0b3fd2d3SAndreas Gohr     */
46*0b3fd2d3SAndreas Gohr    protected $factory;
47*0b3fd2d3SAndreas Gohr
48*0b3fd2d3SAndreas Gohr    /**
49*0b3fd2d3SAndreas Gohr     * @var DigestMD5Encoder
50*0b3fd2d3SAndreas Gohr     */
51*0b3fd2d3SAndreas Gohr    protected $encoder;
52*0b3fd2d3SAndreas Gohr
53*0b3fd2d3SAndreas Gohr    /**
54*0b3fd2d3SAndreas Gohr     * @var null|Message
55*0b3fd2d3SAndreas Gohr     */
56*0b3fd2d3SAndreas Gohr    protected $challenge;
57*0b3fd2d3SAndreas Gohr
58*0b3fd2d3SAndreas Gohr    public function __construct(bool $isServerMode = false)
59*0b3fd2d3SAndreas Gohr    {
60*0b3fd2d3SAndreas Gohr        $this->factory = new DigestMD5MessageFactory();
61*0b3fd2d3SAndreas Gohr        $this->encoder = new DigestMD5Encoder();
62*0b3fd2d3SAndreas Gohr        $this->context = new SaslContext();
63*0b3fd2d3SAndreas Gohr        $this->context->setIsServerMode($isServerMode);
64*0b3fd2d3SAndreas Gohr    }
65*0b3fd2d3SAndreas Gohr
66*0b3fd2d3SAndreas Gohr    /**
67*0b3fd2d3SAndreas Gohr     * {@inheritDoc}
68*0b3fd2d3SAndreas Gohr     */
69*0b3fd2d3SAndreas Gohr    public function challenge(?string $received = null, array $options = []): SaslContext
70*0b3fd2d3SAndreas Gohr    {
71*0b3fd2d3SAndreas Gohr        $options = $options + self::DEFAULTS;
72*0b3fd2d3SAndreas Gohr
73*0b3fd2d3SAndreas Gohr        $received = $received === null ? null : $this->encoder->decode($received, $this->context);
74*0b3fd2d3SAndreas Gohr        if ($this->context->isServerMode()) {
75*0b3fd2d3SAndreas Gohr            $response = $this->generateServerResponse($received, $options);
76*0b3fd2d3SAndreas Gohr        } else {
77*0b3fd2d3SAndreas Gohr            $response = $this->generateClientResponse($received, $options);
78*0b3fd2d3SAndreas Gohr        }
79*0b3fd2d3SAndreas Gohr        $this->context->setResponse($response);
80*0b3fd2d3SAndreas Gohr
81*0b3fd2d3SAndreas Gohr        return $this->context;
82*0b3fd2d3SAndreas Gohr    }
83*0b3fd2d3SAndreas Gohr
84*0b3fd2d3SAndreas Gohr    /**
85*0b3fd2d3SAndreas Gohr     * @throws SaslException
86*0b3fd2d3SAndreas Gohr     */
87*0b3fd2d3SAndreas Gohr    protected function generateClientResponse(?Message $message, $options): ?string
88*0b3fd2d3SAndreas Gohr    {
89*0b3fd2d3SAndreas Gohr        if ($message === null) {
90*0b3fd2d3SAndreas Gohr            return null;
91*0b3fd2d3SAndreas Gohr        }
92*0b3fd2d3SAndreas Gohr        if ($this->isClientChallengeNeeded($message)) {
93*0b3fd2d3SAndreas Gohr            return $this->createClientResponse($message, $options);
94*0b3fd2d3SAndreas Gohr        }
95*0b3fd2d3SAndreas Gohr
96*0b3fd2d3SAndreas Gohr        if ($message->has('rspauth') && $this->context->get('verification') === null) {
97*0b3fd2d3SAndreas Gohr            throw new SaslException('The rspauth value was received before the response was generated.');
98*0b3fd2d3SAndreas Gohr        }
99*0b3fd2d3SAndreas Gohr        if ($message->has('rspauth') && $message->get('rspauth') === $this->context->get('verification')) {
100*0b3fd2d3SAndreas Gohr            $this->context->setIsAuthenticated(true);
101*0b3fd2d3SAndreas Gohr            $this->context->setHasSecurityLayer($options['use_integrity'] || $options['use_privacy']);
102*0b3fd2d3SAndreas Gohr        }
103*0b3fd2d3SAndreas Gohr        if ($this->context->hasSecurityLayer()) {
104*0b3fd2d3SAndreas Gohr            $this->context->set('seqnumsnt', 0);
105*0b3fd2d3SAndreas Gohr            $this->context->set('seqnumrcv', 0);
106*0b3fd2d3SAndreas Gohr        }
107*0b3fd2d3SAndreas Gohr        $this->context->setIsComplete($message->has('rspauth'));
108*0b3fd2d3SAndreas Gohr
109*0b3fd2d3SAndreas Gohr        return null;
110*0b3fd2d3SAndreas Gohr    }
111*0b3fd2d3SAndreas Gohr
112*0b3fd2d3SAndreas Gohr    protected function generateServerResponse(?Message $received, array $options): ?string
113*0b3fd2d3SAndreas Gohr    {
114*0b3fd2d3SAndreas Gohr        if ($received === null) {
115*0b3fd2d3SAndreas Gohr            $response = $this->generateServerChallenge($options);
116*0b3fd2d3SAndreas Gohr        } else {
117*0b3fd2d3SAndreas Gohr            $response = $this->generateServerVerification($received, $options);
118*0b3fd2d3SAndreas Gohr        }
119*0b3fd2d3SAndreas Gohr
120*0b3fd2d3SAndreas Gohr        return $response === null ? null : $this->encoder->encode($response, $this->context);
121*0b3fd2d3SAndreas Gohr    }
122*0b3fd2d3SAndreas Gohr
123*0b3fd2d3SAndreas Gohr    protected function isClientChallengeNeeded(Message $message): bool
124*0b3fd2d3SAndreas Gohr    {
125*0b3fd2d3SAndreas Gohr        if ($this->context->isServerMode()) {
126*0b3fd2d3SAndreas Gohr            return false;
127*0b3fd2d3SAndreas Gohr        }
128*0b3fd2d3SAndreas Gohr
129*0b3fd2d3SAndreas Gohr        return $message->get('rspauth') === null;
130*0b3fd2d3SAndreas Gohr    }
131*0b3fd2d3SAndreas Gohr
132*0b3fd2d3SAndreas Gohr    /**
133*0b3fd2d3SAndreas Gohr     * @throws SaslException
134*0b3fd2d3SAndreas Gohr     */
135*0b3fd2d3SAndreas Gohr    protected function createClientResponse(Message $message, array $options): string
136*0b3fd2d3SAndreas Gohr    {
137*0b3fd2d3SAndreas Gohr        $password = $options['password'] ?? '';
138*0b3fd2d3SAndreas Gohr
139*0b3fd2d3SAndreas Gohr        if ($options['use_privacy']) {
140*0b3fd2d3SAndreas Gohr            $this->context->set('qop', 'auth-conf');
141*0b3fd2d3SAndreas Gohr        } elseif ($options['use_integrity']) {
142*0b3fd2d3SAndreas Gohr            $this->context->set('qop', 'auth-int');
143*0b3fd2d3SAndreas Gohr        } else {
144*0b3fd2d3SAndreas Gohr            $this->context->set('qop', 'auth');
145*0b3fd2d3SAndreas Gohr        }
146*0b3fd2d3SAndreas Gohr
147*0b3fd2d3SAndreas Gohr        $messageOpts = [
148*0b3fd2d3SAndreas Gohr            'username' => $options['username'] ?? null,
149*0b3fd2d3SAndreas Gohr            'digest-uri' => isset($options['host']) ? ($options['service'] . '/' . $options['host']) : null,
150*0b3fd2d3SAndreas Gohr            'qop' => $this->context->get('qop'),
151*0b3fd2d3SAndreas Gohr            'nonce_size' => $options['nonce_size'],
152*0b3fd2d3SAndreas Gohr            'service' => $options['service']
153*0b3fd2d3SAndreas Gohr        ];
154*0b3fd2d3SAndreas Gohr        if (isset($options['cnonce'])) {
155*0b3fd2d3SAndreas Gohr            $messageOpts['cnonce'] = $options['cnonce'];
156*0b3fd2d3SAndreas Gohr        }
157*0b3fd2d3SAndreas Gohr        if (isset($options['nonce'])) {
158*0b3fd2d3SAndreas Gohr            $messageOpts['nonce'] = $options['nonce'];
159*0b3fd2d3SAndreas Gohr        }
160*0b3fd2d3SAndreas Gohr        if (isset($options['cipher'])) {
161*0b3fd2d3SAndreas Gohr            $messageOpts['cipher'] = $options['cipher'];
162*0b3fd2d3SAndreas Gohr        }
163*0b3fd2d3SAndreas Gohr        $response = $this->factory->create(
164*0b3fd2d3SAndreas Gohr            DigestMD5MessageFactory::MESSAGE_CLIENT_RESPONSE, $messageOpts, $message
165*0b3fd2d3SAndreas Gohr        );
166*0b3fd2d3SAndreas Gohr        $response->set('response', DigestMD5Mechanism::computeResponse(
167*0b3fd2d3SAndreas Gohr            $password,
168*0b3fd2d3SAndreas Gohr            $message,
169*0b3fd2d3SAndreas Gohr            $response,
170*0b3fd2d3SAndreas Gohr            $this->context->isServerMode()
171*0b3fd2d3SAndreas Gohr        ));
172*0b3fd2d3SAndreas Gohr
173*0b3fd2d3SAndreas Gohr        # The verification is used to check the response value returned from the server for authentication.
174*0b3fd2d3SAndreas Gohr        $this->context->set(
175*0b3fd2d3SAndreas Gohr            'verification',
176*0b3fd2d3SAndreas Gohr            DigestMD5Mechanism::computeResponse(
177*0b3fd2d3SAndreas Gohr                $password,
178*0b3fd2d3SAndreas Gohr                $message,
179*0b3fd2d3SAndreas Gohr                $response,
180*0b3fd2d3SAndreas Gohr                !$this->context->isServerMode()
181*0b3fd2d3SAndreas Gohr            )
182*0b3fd2d3SAndreas Gohr        );
183*0b3fd2d3SAndreas Gohr
184*0b3fd2d3SAndreas Gohr        # Pre-compute some stuff in advance. The A1 / cipher value is used in the security layer.
185*0b3fd2d3SAndreas Gohr        if ($options['use_integrity'] || $options['use_privacy']) {
186*0b3fd2d3SAndreas Gohr            $this->context->set('a1', hex2bin(DigestMD5Mechanism::computeA1($password, $message, $response)));
187*0b3fd2d3SAndreas Gohr            $this->context->set('cipher', $response->get('cipher'));
188*0b3fd2d3SAndreas Gohr        }
189*0b3fd2d3SAndreas Gohr
190*0b3fd2d3SAndreas Gohr        return $this->encoder->encode($response, $this->context);
191*0b3fd2d3SAndreas Gohr    }
192*0b3fd2d3SAndreas Gohr
193*0b3fd2d3SAndreas Gohr    protected function generateServerVerification(Message $received, array $options): ?Message
194*0b3fd2d3SAndreas Gohr    {
195*0b3fd2d3SAndreas Gohr        $this->context->setIsComplete(true);
196*0b3fd2d3SAndreas Gohr        # The client sent a response without us sending a challenge...
197*0b3fd2d3SAndreas Gohr        if ($this->challenge === null) {
198*0b3fd2d3SAndreas Gohr            return null;
199*0b3fd2d3SAndreas Gohr        }
200*0b3fd2d3SAndreas Gohr
201*0b3fd2d3SAndreas Gohr        # @todo This should accept some kind of computed value, like the a1. Then it could generate the other values
202*0b3fd2d3SAndreas Gohr        #       using that.
203*0b3fd2d3SAndreas Gohr        $password = $options['password'] ?? null;
204*0b3fd2d3SAndreas Gohr        $qop = $received->get('qop');
205*0b3fd2d3SAndreas Gohr        $cipher = $received->get('cipher');
206*0b3fd2d3SAndreas Gohr        if ($password === null) {
207*0b3fd2d3SAndreas Gohr            return null;
208*0b3fd2d3SAndreas Gohr        }
209*0b3fd2d3SAndreas Gohr        # Client selected a qop we did not advertise...
210*0b3fd2d3SAndreas Gohr        if (!in_array($qop, $this->challenge->get('qop'), true)) {
211*0b3fd2d3SAndreas Gohr            return null;
212*0b3fd2d3SAndreas Gohr        }
213*0b3fd2d3SAndreas Gohr        # Client selected a cipher we did not advertise...
214*0b3fd2d3SAndreas Gohr        if (!in_array($cipher, $this->challenge->get('cipher'), true)) {
215*0b3fd2d3SAndreas Gohr            return null;
216*0b3fd2d3SAndreas Gohr        }
217*0b3fd2d3SAndreas Gohr        # The client sent a nonce without the minimum length from the RFC...
218*0b3fd2d3SAndreas Gohr        if (strlen((string) $received->get('cnonce')) < 12) {
219*0b3fd2d3SAndreas Gohr            return null;
220*0b3fd2d3SAndreas Gohr        }
221*0b3fd2d3SAndreas Gohr        # The client sent back a nonce different than what we sent them...
222*0b3fd2d3SAndreas Gohr        if ($received->get('nonce') !== $this->challenge->get('nonce')) {
223*0b3fd2d3SAndreas Gohr            return null;
224*0b3fd2d3SAndreas Gohr        }
225*0b3fd2d3SAndreas Gohr
226*0b3fd2d3SAndreas Gohr        # Generate our own response to compare against what we received from the client. If they do not match,
227*0b3fd2d3SAndreas Gohr        # then the password was incorrect.
228*0b3fd2d3SAndreas Gohr        $expected = DigestMD5Mechanism::computeResponse($password, $this->challenge, $received);
229*0b3fd2d3SAndreas Gohr        if ($expected !== $received->get('response')) {
230*0b3fd2d3SAndreas Gohr            return null;
231*0b3fd2d3SAndreas Gohr        }
232*0b3fd2d3SAndreas Gohr
233*0b3fd2d3SAndreas Gohr        $response = DigestMD5Mechanism::computeResponse($password, $this->challenge, $received, true);
234*0b3fd2d3SAndreas Gohr        $this->context->setIsAuthenticated(true);
235*0b3fd2d3SAndreas Gohr        if ($qop === 'auth-int' || $qop === 'auth-conf') {
236*0b3fd2d3SAndreas Gohr            $this->context->setHasSecurityLayer(true);
237*0b3fd2d3SAndreas Gohr            $this->context->set('a1', hex2bin(DigestMD5Mechanism::computeA1($password, $this->challenge, $received)));
238*0b3fd2d3SAndreas Gohr            $this->context->set('cipher', $received->get('cipher'));
239*0b3fd2d3SAndreas Gohr            $this->context->set('seqnumsnt', 0);
240*0b3fd2d3SAndreas Gohr            $this->context->set('seqnumrcv', 0);
241*0b3fd2d3SAndreas Gohr        }
242*0b3fd2d3SAndreas Gohr
243*0b3fd2d3SAndreas Gohr        return $this->factory->create(
244*0b3fd2d3SAndreas Gohr            DigestMD5MessageFactory::MESSAGE_SERVER_RESPONSE,
245*0b3fd2d3SAndreas Gohr            ['rspauth' => $response]
246*0b3fd2d3SAndreas Gohr        );
247*0b3fd2d3SAndreas Gohr    }
248*0b3fd2d3SAndreas Gohr
249*0b3fd2d3SAndreas Gohr    protected function generateServerChallenge(array $options): Message
250*0b3fd2d3SAndreas Gohr    {
251*0b3fd2d3SAndreas Gohr        $messageOpts = [];
252*0b3fd2d3SAndreas Gohr        if (isset($options['nonce'])) {
253*0b3fd2d3SAndreas Gohr            $messageOpts['nonce'] = $options['nonce'];
254*0b3fd2d3SAndreas Gohr        }
255*0b3fd2d3SAndreas Gohr        if (isset($options['cipher'])) {
256*0b3fd2d3SAndreas Gohr            $messageOpts['cipher'] = $options['cipher'];
257*0b3fd2d3SAndreas Gohr        }
258*0b3fd2d3SAndreas Gohr        $this->challenge = $this->factory->create(
259*0b3fd2d3SAndreas Gohr            DigestMD5MessageFactory::MESSAGE_SERVER_CHALLENGE, $options
260*0b3fd2d3SAndreas Gohr        );
261*0b3fd2d3SAndreas Gohr
262*0b3fd2d3SAndreas Gohr        return $this->challenge;
263*0b3fd2d3SAndreas Gohr    }
264*0b3fd2d3SAndreas Gohr}
265