xref: /plugin/pureldap/vendor/freedsx/sasl/src/FreeDSx/Sasl/Challenge/CramMD5Challenge.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\CramMD5Encoder;
15*0b3fd2d3SAndreas Gohruse FreeDSx\Sasl\Encoder\EncoderInterface;
16*0b3fd2d3SAndreas Gohruse FreeDSx\Sasl\Exception\SaslException;
17*0b3fd2d3SAndreas Gohruse FreeDSx\Sasl\Factory\NonceTrait;
18*0b3fd2d3SAndreas Gohruse FreeDSx\Sasl\Message;
19*0b3fd2d3SAndreas Gohruse FreeDSx\Sasl\SaslContext;
20*0b3fd2d3SAndreas Gohr
21*0b3fd2d3SAndreas Gohr/**
22*0b3fd2d3SAndreas Gohr * The CRAM-MD5 challenge / response class.
23*0b3fd2d3SAndreas Gohr *
24*0b3fd2d3SAndreas Gohr * @author Chad Sikorra <Chad.Sikorra@gmail.com>
25*0b3fd2d3SAndreas Gohr */
26*0b3fd2d3SAndreas Gohrclass CramMD5Challenge implements ChallengeInterface
27*0b3fd2d3SAndreas Gohr{
28*0b3fd2d3SAndreas Gohr    use NonceTrait;
29*0b3fd2d3SAndreas Gohr
30*0b3fd2d3SAndreas Gohr    /**
31*0b3fd2d3SAndreas Gohr     * @var SaslContext
32*0b3fd2d3SAndreas Gohr     */
33*0b3fd2d3SAndreas Gohr    protected $context;
34*0b3fd2d3SAndreas Gohr
35*0b3fd2d3SAndreas Gohr    /**
36*0b3fd2d3SAndreas Gohr     * @var EncoderInterface
37*0b3fd2d3SAndreas Gohr     */
38*0b3fd2d3SAndreas Gohr    protected $encoder;
39*0b3fd2d3SAndreas Gohr
40*0b3fd2d3SAndreas Gohr    public function __construct(bool $isServerMode = false)
41*0b3fd2d3SAndreas Gohr    {
42*0b3fd2d3SAndreas Gohr        $this->encoder = new CramMD5Encoder();
43*0b3fd2d3SAndreas Gohr        $this->context = new SaslContext();
44*0b3fd2d3SAndreas Gohr        $this->context->setIsServerMode($isServerMode);
45*0b3fd2d3SAndreas Gohr    }
46*0b3fd2d3SAndreas Gohr
47*0b3fd2d3SAndreas Gohr    /**
48*0b3fd2d3SAndreas Gohr     * {@inheritDoc}
49*0b3fd2d3SAndreas Gohr     */
50*0b3fd2d3SAndreas Gohr    public function challenge(?string $received = null, array $options = []): SaslContext
51*0b3fd2d3SAndreas Gohr    {
52*0b3fd2d3SAndreas Gohr        $received = ($received === null) ? null : $this->encoder->decode($received, $this->context);
53*0b3fd2d3SAndreas Gohr
54*0b3fd2d3SAndreas Gohr        if ($received === null) {
55*0b3fd2d3SAndreas Gohr            !$this->context->isServerMode() ? $this->context : $this->generateServerChallenge($options);
56*0b3fd2d3SAndreas Gohr
57*0b3fd2d3SAndreas Gohr            return $this->context;
58*0b3fd2d3SAndreas Gohr        }
59*0b3fd2d3SAndreas Gohr
60*0b3fd2d3SAndreas Gohr        if ($this->context->isServerMode()) {
61*0b3fd2d3SAndreas Gohr            $this->validateClientResponse($received, $options);
62*0b3fd2d3SAndreas Gohr        } else {
63*0b3fd2d3SAndreas Gohr            $this->generateClientResponse($received, $options);
64*0b3fd2d3SAndreas Gohr        }
65*0b3fd2d3SAndreas Gohr
66*0b3fd2d3SAndreas Gohr        return $this->context;
67*0b3fd2d3SAndreas Gohr    }
68*0b3fd2d3SAndreas Gohr
69*0b3fd2d3SAndreas Gohr    protected function generateServerChallenge(array $options): SaslContext
70*0b3fd2d3SAndreas Gohr    {
71*0b3fd2d3SAndreas Gohr        $nonce = $options['challenge'] ?? $this->generateNonce(32);
72*0b3fd2d3SAndreas Gohr        $challenge = new Message(['challenge' => $nonce]);
73*0b3fd2d3SAndreas Gohr        $this->context->setResponse($this->encoder->encode($challenge, $this->context));
74*0b3fd2d3SAndreas Gohr        $this->context->set('challenge', $challenge->get('challenge'));
75*0b3fd2d3SAndreas Gohr
76*0b3fd2d3SAndreas Gohr        return $this->context;
77*0b3fd2d3SAndreas Gohr    }
78*0b3fd2d3SAndreas Gohr
79*0b3fd2d3SAndreas Gohr    protected function generateClientResponse(Message $received, array $options): void
80*0b3fd2d3SAndreas Gohr    {
81*0b3fd2d3SAndreas Gohr        if (!$received->has('challenge')) {
82*0b3fd2d3SAndreas Gohr            throw new SaslException('Expected a server challenge to generate a client response.');
83*0b3fd2d3SAndreas Gohr        }
84*0b3fd2d3SAndreas Gohr        if (!(isset($options['username']) && isset($options['password']))) {
85*0b3fd2d3SAndreas Gohr            throw new SaslException('A username and password is required for a client response.');
86*0b3fd2d3SAndreas Gohr        }
87*0b3fd2d3SAndreas Gohr        $response = new Message([
88*0b3fd2d3SAndreas Gohr            'username' => $options['username'],
89*0b3fd2d3SAndreas Gohr            'digest' => $this->generateDigest($received->get('challenge'), $options['password']),
90*0b3fd2d3SAndreas Gohr        ]);
91*0b3fd2d3SAndreas Gohr        $this->context->setResponse($this->encoder->encode($response, $this->context));
92*0b3fd2d3SAndreas Gohr        $this->context->setIsComplete(true);
93*0b3fd2d3SAndreas Gohr    }
94*0b3fd2d3SAndreas Gohr
95*0b3fd2d3SAndreas Gohr    protected function validateClientResponse(Message $received, array $options): void
96*0b3fd2d3SAndreas Gohr    {
97*0b3fd2d3SAndreas Gohr        if (!$received->has('username')) {
98*0b3fd2d3SAndreas Gohr            throw new SaslException('The client response must have a username.');
99*0b3fd2d3SAndreas Gohr        }
100*0b3fd2d3SAndreas Gohr        if (!$received->has('digest')) {
101*0b3fd2d3SAndreas Gohr            throw new SaslException('The client response must have a digest.');
102*0b3fd2d3SAndreas Gohr        }
103*0b3fd2d3SAndreas Gohr        if (!isset($options['password'])) {
104*0b3fd2d3SAndreas Gohr            throw new SaslException('To validate the client response you must supply the password option.');
105*0b3fd2d3SAndreas Gohr        }
106*0b3fd2d3SAndreas Gohr        $username = $received->get('username');
107*0b3fd2d3SAndreas Gohr        $digest = $received->get('digest');
108*0b3fd2d3SAndreas Gohr
109*0b3fd2d3SAndreas Gohr        $password = $options['password'];
110*0b3fd2d3SAndreas Gohr        if (!is_callable($password)) {
111*0b3fd2d3SAndreas Gohr            throw new SaslException('The password option must be callable. It will be passed the username and challenge');
112*0b3fd2d3SAndreas Gohr        }
113*0b3fd2d3SAndreas Gohr        $expectedDigest = $password($username, $this->context->get('challenge'));
114*0b3fd2d3SAndreas Gohr
115*0b3fd2d3SAndreas Gohr        $this->context->setIsAuthenticated($expectedDigest === $digest);
116*0b3fd2d3SAndreas Gohr        $this->context->setIsComplete(true);
117*0b3fd2d3SAndreas Gohr    }
118*0b3fd2d3SAndreas Gohr
119*0b3fd2d3SAndreas Gohr    protected function generateDigest(string $challenge, string $key): string
120*0b3fd2d3SAndreas Gohr    {
121*0b3fd2d3SAndreas Gohr        return hash_hmac(
122*0b3fd2d3SAndreas Gohr            'md5',
123*0b3fd2d3SAndreas Gohr            $challenge,
124*0b3fd2d3SAndreas Gohr            $key
125*0b3fd2d3SAndreas Gohr        );
126*0b3fd2d3SAndreas Gohr    }
127*0b3fd2d3SAndreas Gohr}
128