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\Mechanism;
13
14use FreeDSx\Sasl\Challenge\ChallengeInterface;
15use FreeDSx\Sasl\Challenge\DigestMD5Challenge;
16use FreeDSx\Sasl\Exception\SaslException;
17use FreeDSx\Sasl\Message;
18use FreeDSx\Sasl\Security\DigestMD5SecurityLayer;
19use FreeDSx\Sasl\Security\SecurityLayerInterface;
20use FreeDSx\Sasl\SecurityStrength;
21
22/**
23 * The Digest-MD5 mechanism.
24 *
25 * @author Chad Sikorra <Chad.Sikorra@gmail.com>
26 */
27class DigestMD5Mechanism implements MechanismInterface
28{
29    public const NAME = 'DIGEST-MD5';
30
31    protected const A2_SERVER = ':';
32
33    protected const A2_CLIENT = 'AUTHENTICATE:';
34
35    /**
36     * {@inheritDoc}
37     */
38    public function getName(): string
39    {
40        return self::NAME;
41    }
42
43    /**
44     * {@inheritDoc}
45     */
46    public function challenge(): ChallengeInterface
47    {
48        $challenge = new DigestMD5Challenge();
49
50        return $challenge;
51    }
52
53    /**
54     * {@inheritDoc}
55     */
56    public function securityStrength(): SecurityStrength
57    {
58        return new SecurityStrength(
59            true,
60            true,
61            true,
62            false,
63            128
64        );
65    }
66
67    /**
68     * {@inheritDoc}
69     */
70    public function securityLayer(): SecurityLayerInterface
71    {
72        return new DigestMD5SecurityLayer();
73    }
74
75    public function __toString()
76    {
77        return self::NAME;
78    }
79
80    /**
81     * Generates the computed response value. RFC2831 2.1.2.1
82     *
83     *  HEX( KD ( HEX(H(A1)),
84     *      { nonce-value, ":" nc-value, ":",
85     *        cnonce-value, ":", qop-value, ":", HEX(H(A2)) }))
86     *
87     * If the "qop" directive's value is "auth", then A2 is:
88     *
89     *   A2 = { "AUTHENTICATE:", digest-uri-value }
90     *
91     * If the "qop" value is "auth-int" or "auth-conf" then A2 is:
92     *
93     *   A2 = { "AUTHENTICATE:", digest-uri-value,
94     *      ":00000000000000000000000000000000" }
95     *
96     * If this is the server context, then the beginning of A2 is just a semi-colon.
97     *
98     * @throws SaslException
99     */
100    public static function computeResponse(string $password, Message $challenge, Message $response, bool $useServerMode = false): string
101    {
102        $a1 = self::computeA1($password, $challenge, $response);
103
104        $qop = $response->get('qop');
105        $digestUri = $response->get('digest-uri');
106        $a2 = $useServerMode ? self::A2_SERVER : self::A2_CLIENT;
107
108        if ($qop === 'auth') {
109            $a2 .= $digestUri;
110        } elseif ($qop === 'auth-int' || $qop === 'auth-conf') {
111            $a2 .= $digestUri . ':00000000000000000000000000000000';
112        } else {
113            throw new SaslException('The qop directive must be one of: auth, auth-conf, auth-int.');
114        }
115        $a2 = hash('md5', $a2);
116
117        return hash('md5', sprintf(
118            '%s:%s:%s:%s:%s:%s',
119            $a1,
120            $challenge->get('nonce'),
121            str_pad(dechex($response->get('nc')), 8, '0', STR_PAD_LEFT),
122            $response->get('cnonce'),
123            $response->get('qop'),
124            $a2
125        ));
126    }
127
128    /**
129     * If authzid is specified, then A1 is
130     *
131     *   A1 = { H( { username-value, ":", realm-value, ":", passwd } ),
132     *        ":", nonce-value, ":", cnonce-value, ":", authzid-value }
133     *
134     * If authzid is not specified, then A1 is
135     *
136     *   A1 = { H( { username-value, ":", realm-value, ":", passwd } ),
137     *        ":", nonce-value, ":", cnonce-value }
138     *
139     */
140    public static function computeA1(string $password, Message $challenge, Message $response): string
141    {
142        $a1 = hash('md5', sprintf(
143            '%s:%s:%s',
144            $response->get('username'),
145            $response->get('realm'),
146            $password
147        ), true);
148        $a1 = sprintf(
149            '%s:%s:%s',
150            $a1,
151            $challenge->get('nonce'),
152            $response->get('cnonce')
153        );
154        if ($response->has('authzid')) {
155            $a1 .= ':' . $response->get('authzid');
156        }
157
158        return hash('md5', $a1);
159    }
160}
161