xref: /plugin/pureldap/vendor/freedsx/sasl/src/FreeDSx/Sasl/Security/DigestMD5SecurityLayer.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\Security;
13*0b3fd2d3SAndreas Gohr
14*0b3fd2d3SAndreas Gohruse FreeDSx\Sasl\Exception\SaslException;
15*0b3fd2d3SAndreas Gohruse FreeDSx\Sasl\SaslContext;
16*0b3fd2d3SAndreas Gohr
17*0b3fd2d3SAndreas Gohr/**
18*0b3fd2d3SAndreas Gohr * The DIGEST-MD5 security layer.
19*0b3fd2d3SAndreas Gohr *
20*0b3fd2d3SAndreas Gohr * @author Chad Sikorra <Chad.Sikorra@gmail.com>
21*0b3fd2d3SAndreas Gohr */
22*0b3fd2d3SAndreas Gohrclass DigestMD5SecurityLayer implements SecurityLayerInterface
23*0b3fd2d3SAndreas Gohr{
24*0b3fd2d3SAndreas Gohr    protected const MAXBUF = 65536;
25*0b3fd2d3SAndreas Gohr
26*0b3fd2d3SAndreas Gohr    protected const KCC_MC = 'Digest H(A1) to client-to-server sealing key magic constant';
27*0b3fd2d3SAndreas Gohr
28*0b3fd2d3SAndreas Gohr    protected const KCS_MC = 'Digest H(A1) to server-to-client sealing key magic constant';
29*0b3fd2d3SAndreas Gohr
30*0b3fd2d3SAndreas Gohr    protected const KIC_MC = 'Digest session key to client-to-server signing key magic constant';
31*0b3fd2d3SAndreas Gohr
32*0b3fd2d3SAndreas Gohr    protected const KIS_MC = 'Digest session key to server-to-client signing key magic constant';
33*0b3fd2d3SAndreas Gohr
34*0b3fd2d3SAndreas Gohr    /**
35*0b3fd2d3SAndreas Gohr     * RFC2831 Section 2.3 / 2.4
36*0b3fd2d3SAndreas Gohr     */
37*0b3fd2d3SAndreas Gohr    protected const MESSAGE_TYPE = 1;
38*0b3fd2d3SAndreas Gohr
39*0b3fd2d3SAndreas Gohr    /**
40*0b3fd2d3SAndreas Gohr     * Cipher specific details related to the encryption / decryption process.
41*0b3fd2d3SAndreas Gohr     */
42*0b3fd2d3SAndreas Gohr    protected const CIPHERS = [
43*0b3fd2d3SAndreas Gohr        '3des' => [
44*0b3fd2d3SAndreas Gohr            'block_size' => 8,
45*0b3fd2d3SAndreas Gohr            'kcn' => 16,
46*0b3fd2d3SAndreas Gohr            'cipher' => 'des-ede3-cbc',
47*0b3fd2d3SAndreas Gohr        ],
48*0b3fd2d3SAndreas Gohr        'des' => [
49*0b3fd2d3SAndreas Gohr            'block_size' => 8,
50*0b3fd2d3SAndreas Gohr            'kcn' => 16,
51*0b3fd2d3SAndreas Gohr            'cipher' => 'des-ede-cbc'
52*0b3fd2d3SAndreas Gohr        ],
53*0b3fd2d3SAndreas Gohr        'rc4' => [
54*0b3fd2d3SAndreas Gohr            'block_size' => 1,
55*0b3fd2d3SAndreas Gohr            'kcn' => 16,
56*0b3fd2d3SAndreas Gohr            'cipher' => 'rc4',
57*0b3fd2d3SAndreas Gohr        ],
58*0b3fd2d3SAndreas Gohr        'rc4-40' => [
59*0b3fd2d3SAndreas Gohr            'block_size' => 1,
60*0b3fd2d3SAndreas Gohr            'kcn' => 5,
61*0b3fd2d3SAndreas Gohr            'cipher' => 'rc4-40',
62*0b3fd2d3SAndreas Gohr        ],
63*0b3fd2d3SAndreas Gohr        'rc4-56' => [
64*0b3fd2d3SAndreas Gohr            'block_size' => 1,
65*0b3fd2d3SAndreas Gohr            'kcn' => 7,
66*0b3fd2d3SAndreas Gohr            'cipher' => 'rc4-56',
67*0b3fd2d3SAndreas Gohr        ],
68*0b3fd2d3SAndreas Gohr    ];
69*0b3fd2d3SAndreas Gohr
70*0b3fd2d3SAndreas Gohr    /**
71*0b3fd2d3SAndreas Gohr     * {@inheritDoc}
72*0b3fd2d3SAndreas Gohr     */
73*0b3fd2d3SAndreas Gohr    public function wrap(string $data, SaslContext $context): string
74*0b3fd2d3SAndreas Gohr    {
75*0b3fd2d3SAndreas Gohr        $qop = $context->get('qop');
76*0b3fd2d3SAndreas Gohr
77*0b3fd2d3SAndreas Gohr        if ($qop === 'auth-conf') {
78*0b3fd2d3SAndreas Gohr            $wrapped = $this->encrypt($data, $context);
79*0b3fd2d3SAndreas Gohr        } elseif ($qop === 'auth-int') {
80*0b3fd2d3SAndreas Gohr            $wrapped = $this->sign($data, $context);
81*0b3fd2d3SAndreas Gohr        } else {
82*0b3fd2d3SAndreas Gohr            throw new SaslException(sprintf('The qop option "%s" is not recognized as a security layer.', $qop));
83*0b3fd2d3SAndreas Gohr        }
84*0b3fd2d3SAndreas Gohr        $this->validateBufferLength($wrapped, $context);
85*0b3fd2d3SAndreas Gohr        $context->set('seqnumsnt', $context->get('seqnumsnt') + 1);
86*0b3fd2d3SAndreas Gohr
87*0b3fd2d3SAndreas Gohr        return $wrapped;
88*0b3fd2d3SAndreas Gohr    }
89*0b3fd2d3SAndreas Gohr
90*0b3fd2d3SAndreas Gohr    /**
91*0b3fd2d3SAndreas Gohr     * {@inheritDoc}
92*0b3fd2d3SAndreas Gohr     */
93*0b3fd2d3SAndreas Gohr    public function unwrap(string $data, SaslContext $context): string
94*0b3fd2d3SAndreas Gohr    {
95*0b3fd2d3SAndreas Gohr        $qop = $context->get('qop');
96*0b3fd2d3SAndreas Gohr        $this->validateBufferLength($data, $context);
97*0b3fd2d3SAndreas Gohr
98*0b3fd2d3SAndreas Gohr        if ($qop === 'auth-conf') {
99*0b3fd2d3SAndreas Gohr            $unwrapped = $this->decrypt($data, $context);
100*0b3fd2d3SAndreas Gohr        } elseif ($qop === 'auth-int') {
101*0b3fd2d3SAndreas Gohr            $unwrapped = $this->verify($data, $context);
102*0b3fd2d3SAndreas Gohr        } else {
103*0b3fd2d3SAndreas Gohr            throw new SaslException(sprintf('The qop option "%s" is not recognized as a security layer.', $qop));
104*0b3fd2d3SAndreas Gohr        }
105*0b3fd2d3SAndreas Gohr        $context->set('seqnumrcv', $context->get('seqnumrcv') + 1);
106*0b3fd2d3SAndreas Gohr
107*0b3fd2d3SAndreas Gohr        return $unwrapped;
108*0b3fd2d3SAndreas Gohr    }
109*0b3fd2d3SAndreas Gohr
110*0b3fd2d3SAndreas Gohr    /**
111*0b3fd2d3SAndreas Gohr     * @throws SaslException
112*0b3fd2d3SAndreas Gohr     */
113*0b3fd2d3SAndreas Gohr    protected function decrypt(string $data, SaslContext $context): string
114*0b3fd2d3SAndreas Gohr    {
115*0b3fd2d3SAndreas Gohr        # At the very least we are expect 16 bytes. 10 for the actual MAC, 4 for the seqnum, 2 for the msgtype.
116*0b3fd2d3SAndreas Gohr        if (strlen($data) < 16) {
117*0b3fd2d3SAndreas Gohr            throw new SaslException('The data to decrypt must be at least 16 bytes.');
118*0b3fd2d3SAndreas Gohr        }
119*0b3fd2d3SAndreas Gohr        $receivedMsgType = hexdec(bin2hex(substr($data, -6, 2)));
120*0b3fd2d3SAndreas Gohr        $receivedSeqNum = hexdec(bin2hex(substr($data, -4)));
121*0b3fd2d3SAndreas Gohr        if (self::MESSAGE_TYPE !== $receivedMsgType) {
122*0b3fd2d3SAndreas Gohr            throw new SaslException(sprintf(
123*0b3fd2d3SAndreas Gohr                'The received message type of "%s" was unexpected.',
124*0b3fd2d3SAndreas Gohr                $receivedMsgType
125*0b3fd2d3SAndreas Gohr            ));
126*0b3fd2d3SAndreas Gohr        }
127*0b3fd2d3SAndreas Gohr        $seqnum = $context->get('seqnumrcv');
128*0b3fd2d3SAndreas Gohr        if (!is_int($seqnum) || $seqnum !== $receivedSeqNum) {
129*0b3fd2d3SAndreas Gohr            throw new SaslException(sprintf(
130*0b3fd2d3SAndreas Gohr                'The received sequence number was unexpected. Expected %s, but got %s.',
131*0b3fd2d3SAndreas Gohr                $seqnum,
132*0b3fd2d3SAndreas Gohr                $receivedSeqNum
133*0b3fd2d3SAndreas Gohr            ));
134*0b3fd2d3SAndreas Gohr        }
135*0b3fd2d3SAndreas Gohr
136*0b3fd2d3SAndreas Gohr        $cipher = $context->get('cipher');
137*0b3fd2d3SAndreas Gohr        $a1 = $context->get('a1');
138*0b3fd2d3SAndreas Gohr        $isServerMode = $context->isServerMode();
139*0b3fd2d3SAndreas Gohr        $this->validateCipher($cipher);
140*0b3fd2d3SAndreas Gohr        $encrypted = substr($data, 0, -6);
141*0b3fd2d3SAndreas Gohr
142*0b3fd2d3SAndreas Gohr        # Inverted selection of constants here and for $mcKi, as this would be the receiving end.
143*0b3fd2d3SAndreas Gohr        $mcKc = $isServerMode ? self::KCC_MC : self::KCS_MC;
144*0b3fd2d3SAndreas Gohr        $kc = $this->generateKeyKc($a1, $cipher, $mcKc);
145*0b3fd2d3SAndreas Gohr        [$iv, $key] = $this->generateKeyAndIV($cipher, $kc);
146*0b3fd2d3SAndreas Gohr        $data = openssl_decrypt($encrypted, self::CIPHERS[$cipher]['cipher'], $key, OPENSSL_NO_PADDING | OPENSSL_RAW_DATA, $iv);
147*0b3fd2d3SAndreas Gohr        if ($data === false) {
148*0b3fd2d3SAndreas Gohr            throw new SaslException('Failed the decrypt the message.');
149*0b3fd2d3SAndreas Gohr        }
150*0b3fd2d3SAndreas Gohr        $message = substr($data, 0, -10);
151*0b3fd2d3SAndreas Gohr        if (self::CIPHERS[$cipher]['block_size'] > 1) {
152*0b3fd2d3SAndreas Gohr            $message = $this->removePadding($message, self::CIPHERS[$cipher]['block_size']);
153*0b3fd2d3SAndreas Gohr        }
154*0b3fd2d3SAndreas Gohr
155*0b3fd2d3SAndreas Gohr        $receivedMac = substr($data, -10);
156*0b3fd2d3SAndreas Gohr        $mcKi = $isServerMode ? self::KIC_MC : self::KIS_MC;
157*0b3fd2d3SAndreas Gohr        $ki = $this->generateKeyKi($a1, $mcKi);
158*0b3fd2d3SAndreas Gohr        $expectedMac = substr($this->generateMACBlock($ki, $message, $seqnum), 0, 10);
159*0b3fd2d3SAndreas Gohr
160*0b3fd2d3SAndreas Gohr        if ($receivedMac !== $expectedMac) {
161*0b3fd2d3SAndreas Gohr            throw new SaslException('The received MAC does not match the expected MAC.');
162*0b3fd2d3SAndreas Gohr        }
163*0b3fd2d3SAndreas Gohr
164*0b3fd2d3SAndreas Gohr        return $message;
165*0b3fd2d3SAndreas Gohr    }
166*0b3fd2d3SAndreas Gohr
167*0b3fd2d3SAndreas Gohr    /**
168*0b3fd2d3SAndreas Gohr     * SEAL(Ki, Kc, SeqNum, msg) = {CIPHER(Kc, {msg, pad, HMAC(Ki, {SeqNum, msg})[0..9])}), 0x0001, SeqNum}
169*0b3fd2d3SAndreas Gohr     *
170*0b3fd2d3SAndreas Gohr     * @throws SaslException
171*0b3fd2d3SAndreas Gohr     */
172*0b3fd2d3SAndreas Gohr    protected function encrypt(string $data, SaslContext $context): string
173*0b3fd2d3SAndreas Gohr    {
174*0b3fd2d3SAndreas Gohr        $cipher = $context->get('cipher');
175*0b3fd2d3SAndreas Gohr        $a1 = $context->get('a1');
176*0b3fd2d3SAndreas Gohr        $isServerMode = $context->isServerMode();
177*0b3fd2d3SAndreas Gohr        $seqnum = $context->get('seqnumsnt');
178*0b3fd2d3SAndreas Gohr        $this->validateCipher($cipher);
179*0b3fd2d3SAndreas Gohr
180*0b3fd2d3SAndreas Gohr        $mcKc = $isServerMode ? self::KCS_MC : self::KCC_MC;
181*0b3fd2d3SAndreas Gohr        $kc = $this->generateKeyKc($a1, $cipher, $mcKc);
182*0b3fd2d3SAndreas Gohr
183*0b3fd2d3SAndreas Gohr        $mcKi = $isServerMode ? self::KIS_MC : self::KIC_MC;
184*0b3fd2d3SAndreas Gohr        $ki = $this->generateKeyKi($a1, $mcKi);
185*0b3fd2d3SAndreas Gohr
186*0b3fd2d3SAndreas Gohr        # The first 10 bytes of the MAC block is used. Extract the last 6 bytes, as that gets tacked onto the end.
187*0b3fd2d3SAndreas Gohr        $macBlock = $this->generateMACBlock($ki, $data, $seqnum);
188*0b3fd2d3SAndreas Gohr        $ending = substr($macBlock, 10);
189*0b3fd2d3SAndreas Gohr        $macBlock = substr($macBlock, 0, 10);
190*0b3fd2d3SAndreas Gohr
191*0b3fd2d3SAndreas Gohr        $padding = $this->generatePadding($data, self::CIPHERS[$cipher]['block_size']);
192*0b3fd2d3SAndreas Gohr        [$iv, $key] = $this->generateKeyAndIV($cipher, $kc);
193*0b3fd2d3SAndreas Gohr        $encrypted = openssl_encrypt($data . $padding . $macBlock, self::CIPHERS[$cipher]['cipher'], $key, OPENSSL_NO_PADDING | OPENSSL_RAW_DATA, $iv);
194*0b3fd2d3SAndreas Gohr
195*0b3fd2d3SAndreas Gohr        return $encrypted . $ending;
196*0b3fd2d3SAndreas Gohr    }
197*0b3fd2d3SAndreas Gohr
198*0b3fd2d3SAndreas Gohr    /**
199*0b3fd2d3SAndreas Gohr     * @throws SaslException
200*0b3fd2d3SAndreas Gohr     */
201*0b3fd2d3SAndreas Gohr    protected function removePadding(string $message, int $blockSize): string
202*0b3fd2d3SAndreas Gohr    {
203*0b3fd2d3SAndreas Gohr        $padOrd = isset($message[-1]) ? ord($message[-1]) : 0;
204*0b3fd2d3SAndreas Gohr        $padRaw = $message[-1] ?? '';
205*0b3fd2d3SAndreas Gohr
206*0b3fd2d3SAndreas Gohr        # The padding size should only ever be between these values...
207*0b3fd2d3SAndreas Gohr        if ($padOrd < 1 || $padOrd > $blockSize) {
208*0b3fd2d3SAndreas Gohr            throw new SaslException('The padding size is not correct.');
209*0b3fd2d3SAndreas Gohr        }
210*0b3fd2d3SAndreas Gohr
211*0b3fd2d3SAndreas Gohr        $msgLength = strlen($message);
212*0b3fd2d3SAndreas Gohr        for ($i = ($msgLength - $padOrd); $i < ($msgLength - 1); $i++) {
213*0b3fd2d3SAndreas Gohr            if ($message[$i] !== $padRaw) {
214*0b3fd2d3SAndreas Gohr                throw new SaslException('The padding does not match the expected value.');
215*0b3fd2d3SAndreas Gohr            }
216*0b3fd2d3SAndreas Gohr        }
217*0b3fd2d3SAndreas Gohr
218*0b3fd2d3SAndreas Gohr        return  substr($message, 0, strlen($message) - $padOrd);
219*0b3fd2d3SAndreas Gohr    }
220*0b3fd2d3SAndreas Gohr
221*0b3fd2d3SAndreas Gohr    /**
222*0b3fd2d3SAndreas Gohr     * @throws SaslException
223*0b3fd2d3SAndreas Gohr     */
224*0b3fd2d3SAndreas Gohr    protected function validateCipher(string $cipher): void
225*0b3fd2d3SAndreas Gohr    {
226*0b3fd2d3SAndreas Gohr        if (!isset(self::CIPHERS[$cipher])) {
227*0b3fd2d3SAndreas Gohr            throw new SaslException(sprintf(
228*0b3fd2d3SAndreas Gohr                'The cipher "%s" is not supported.',
229*0b3fd2d3SAndreas Gohr                $cipher
230*0b3fd2d3SAndreas Gohr            ));
231*0b3fd2d3SAndreas Gohr        }
232*0b3fd2d3SAndreas Gohr    }
233*0b3fd2d3SAndreas Gohr
234*0b3fd2d3SAndreas Gohr    /**
235*0b3fd2d3SAndreas Gohr     * Append a signed MAC to the message.
236*0b3fd2d3SAndreas Gohr     */
237*0b3fd2d3SAndreas Gohr    protected function sign(string $message, SaslContext $context): string
238*0b3fd2d3SAndreas Gohr    {
239*0b3fd2d3SAndreas Gohr        $seqnum = $context->get('seqnumsnt');
240*0b3fd2d3SAndreas Gohr        $mc = $context->isServerMode() ? self::KIS_MC : self::KIC_MC;
241*0b3fd2d3SAndreas Gohr        $ki = $this->generateKeyKi($context->get('a1'), $mc);
242*0b3fd2d3SAndreas Gohr        $macBlock = $this->generateMACBlock($ki, $message, $seqnum);
243*0b3fd2d3SAndreas Gohr
244*0b3fd2d3SAndreas Gohr        return $message . $macBlock;
245*0b3fd2d3SAndreas Gohr    }
246*0b3fd2d3SAndreas Gohr
247*0b3fd2d3SAndreas Gohr    /**
248*0b3fd2d3SAndreas Gohr     * Verify a signed message. Return the unsigned message without the MAC.
249*0b3fd2d3SAndreas Gohr     *
250*0b3fd2d3SAndreas Gohr     * @throws SaslException
251*0b3fd2d3SAndreas Gohr     */
252*0b3fd2d3SAndreas Gohr    protected function verify(string $data, SaslContext $context): string
253*0b3fd2d3SAndreas Gohr    {
254*0b3fd2d3SAndreas Gohr        $receivedMac = substr($data, -16);
255*0b3fd2d3SAndreas Gohr        if (strlen($receivedMac) !== 16) {
256*0b3fd2d3SAndreas Gohr            throw new SaslException('Expected at least 16 bytes of data for the MAC.');
257*0b3fd2d3SAndreas Gohr        }
258*0b3fd2d3SAndreas Gohr
259*0b3fd2d3SAndreas Gohr        $seqnum = $context->get('seqnumrcv');
260*0b3fd2d3SAndreas Gohr        $message = substr($data, 0, -16);
261*0b3fd2d3SAndreas Gohr        # Inverted selection of constant here, as this would be the receiving end.
262*0b3fd2d3SAndreas Gohr        $mc = $context->isServerMode() ? self::KIC_MC : self::KIS_MC;
263*0b3fd2d3SAndreas Gohr        $ki = $this->generateKeyKi($context->get('a1'), $mc);
264*0b3fd2d3SAndreas Gohr        $expectedMac = $this->generateMACBlock($ki, $message, $seqnum);
265*0b3fd2d3SAndreas Gohr
266*0b3fd2d3SAndreas Gohr        if ($receivedMac !== $expectedMac) {
267*0b3fd2d3SAndreas Gohr            throw new SaslException('The received MAC is invalid.');
268*0b3fd2d3SAndreas Gohr        }
269*0b3fd2d3SAndreas Gohr
270*0b3fd2d3SAndreas Gohr        return $message;
271*0b3fd2d3SAndreas Gohr    }
272*0b3fd2d3SAndreas Gohr
273*0b3fd2d3SAndreas Gohr    /**
274*0b3fd2d3SAndreas Gohr     * Per the RFC:
275*0b3fd2d3SAndreas Gohr     *
276*0b3fd2d3SAndreas Gohr     *   If the blocksize of the chosen cipher is not 1 byte, the padding prefix is one or more octets each containing the
277*0b3fd2d3SAndreas Gohr     *   number of padding bytes, such that total length of the encrypted part of the message is a multiple of the
278*0b3fd2d3SAndreas Gohr     *   blocksize.
279*0b3fd2d3SAndreas Gohr     */
280*0b3fd2d3SAndreas Gohr    protected function generatePadding(string $data, int $blockSize): string
281*0b3fd2d3SAndreas Gohr    {
282*0b3fd2d3SAndreas Gohr        if ($blockSize === 1) {
283*0b3fd2d3SAndreas Gohr            return '';
284*0b3fd2d3SAndreas Gohr        }
285*0b3fd2d3SAndreas Gohr        $pad = $blockSize - (strlen($data) + 10) & ($blockSize - 1);
286*0b3fd2d3SAndreas Gohr
287*0b3fd2d3SAndreas Gohr        return str_repeat(chr($pad), $pad);
288*0b3fd2d3SAndreas Gohr    }
289*0b3fd2d3SAndreas Gohr
290*0b3fd2d3SAndreas Gohr    /**
291*0b3fd2d3SAndreas Gohr     * RFC2831 Section 2.3
292*0b3fd2d3SAndreas Gohr     *
293*0b3fd2d3SAndreas Gohr     * The MAC block is 16 bytes: the first 10 bytes of the HMAC-MD5 [RFC2104] of the message, a 2-byte message type
294*0b3fd2d3SAndreas Gohr     * number in network byte order with value 1, and the 4-byte sequence number in network byte order. The message type
295*0b3fd2d3SAndreas Gohr     * is to allow for future extensions such as rekeying.
296*0b3fd2d3SAndreas Gohr     *
297*0b3fd2d3SAndreas Gohr     *   MAC(Ki, SeqNum, msg) = (HMAC(Ki, {SeqNum, msg})[0..9], 0x0001, SeqNum)
298*0b3fd2d3SAndreas Gohr     */
299*0b3fd2d3SAndreas Gohr    protected function generateMACBlock(string $key, string $message, int $seqNum): string
300*0b3fd2d3SAndreas Gohr    {
301*0b3fd2d3SAndreas Gohr        /** 4-byte sequence number in network byte order. */
302*0b3fd2d3SAndreas Gohr        $seqNum = pack('N', $seqNum);
303*0b3fd2d3SAndreas Gohr        $macBlock = substr(hash_hmac('md5', $seqNum . $message, $key, true), 0, 10);
304*0b3fd2d3SAndreas Gohr        /** a 2-byte message type number in network byte order with value 1 */
305*0b3fd2d3SAndreas Gohr        $macBlock .= "\x00\x01";
306*0b3fd2d3SAndreas Gohr        $macBlock .= $seqNum;
307*0b3fd2d3SAndreas Gohr
308*0b3fd2d3SAndreas Gohr        return $macBlock;
309*0b3fd2d3SAndreas Gohr    }
310*0b3fd2d3SAndreas Gohr
311*0b3fd2d3SAndreas Gohr    /**
312*0b3fd2d3SAndreas Gohr     * The keys for integrity protecting messages from client to server / server to client:
313*0b3fd2d3SAndreas Gohr     *
314*0b3fd2d3SAndreas Gohr     *   Kic = MD5({H(A1), "Digest session key to client-to-server signing key magic constant"})
315*0b3fd2d3SAndreas Gohr     *   Kis = MD5({H(A1), "Digest session key to server-to-client signing key magic constant"})
316*0b3fd2d3SAndreas Gohr     *
317*0b3fd2d3SAndreas Gohr     */
318*0b3fd2d3SAndreas Gohr    protected function generateKeyKi(string $a1, string $mc): string
319*0b3fd2d3SAndreas Gohr    {
320*0b3fd2d3SAndreas Gohr        return hash('md5', $a1 . $mc, true);
321*0b3fd2d3SAndreas Gohr    }
322*0b3fd2d3SAndreas Gohr
323*0b3fd2d3SAndreas Gohr    /**
324*0b3fd2d3SAndreas Gohr     * The key for encrypting messages from client to server / server to client:
325*0b3fd2d3SAndreas Gohr     *
326*0b3fd2d3SAndreas Gohr     *   Kcc = MD5({H(A1)[0..n], "Digest H(A1) to client-to-server sealing key magic constant"})
327*0b3fd2d3SAndreas Gohr     *   Kcs = MD5({H(A1)[0..n], "Digest H(A1) to server-to-client sealing key magic constant"})
328*0b3fd2d3SAndreas Gohr     *
329*0b3fd2d3SAndreas Gohr     * Where the key size is determined by "n" above.
330*0b3fd2d3SAndreas Gohr     */
331*0b3fd2d3SAndreas Gohr    protected function generateKeyKc(string $a1, string $cipher, string $mc): string
332*0b3fd2d3SAndreas Gohr    {
333*0b3fd2d3SAndreas Gohr        return hash(
334*0b3fd2d3SAndreas Gohr            'md5',
335*0b3fd2d3SAndreas Gohr            substr($a1, 0, self::CIPHERS[$cipher]['kcn']) . $mc,
336*0b3fd2d3SAndreas Gohr            true
337*0b3fd2d3SAndreas Gohr        );
338*0b3fd2d3SAndreas Gohr    }
339*0b3fd2d3SAndreas Gohr
340*0b3fd2d3SAndreas Gohr    protected function generateKeyAndIV(string $cipher, string $kc): array
341*0b3fd2d3SAndreas Gohr    {
342*0b3fd2d3SAndreas Gohr        # No IV and all of the kc for the key with RC4 types
343*0b3fd2d3SAndreas Gohr        if ($cipher === 'rc4' || $cipher === 'rc4-40' || $cipher === 'rc4-56') {
344*0b3fd2d3SAndreas Gohr            return ['', $kc];
345*0b3fd2d3SAndreas Gohr        }
346*0b3fd2d3SAndreas Gohr
347*0b3fd2d3SAndreas Gohr        $iv = substr($kc, 8, 8);
348*0b3fd2d3SAndreas Gohr        if ($cipher === 'des') {
349*0b3fd2d3SAndreas Gohr            $key = $this->expandDesKey(substr($kc, 0, 7));
350*0b3fd2d3SAndreas Gohr        } else {
351*0b3fd2d3SAndreas Gohr            $key1 = substr($kc, 0, 7);
352*0b3fd2d3SAndreas Gohr            $key2 = substr($kc, 7, 7);
353*0b3fd2d3SAndreas Gohr
354*0b3fd2d3SAndreas Gohr            $key = '';
355*0b3fd2d3SAndreas Gohr            foreach ([$key1, $key2, $key1] as $desKey) {
356*0b3fd2d3SAndreas Gohr                $key .= $this->expandDesKey($desKey);
357*0b3fd2d3SAndreas Gohr            }
358*0b3fd2d3SAndreas Gohr        }
359*0b3fd2d3SAndreas Gohr
360*0b3fd2d3SAndreas Gohr        return [$iv, $key];
361*0b3fd2d3SAndreas Gohr    }
362*0b3fd2d3SAndreas Gohr
363*0b3fd2d3SAndreas Gohr    /**
364*0b3fd2d3SAndreas Gohr     * We need to manually expand the 7-byte DES keys to 8-bytes. This shifts the first 7 bytes into the high seven bits.
365*0b3fd2d3SAndreas Gohr     * This also ignores parity, as it should not be strictly necessary and just adds additional complexity here.
366*0b3fd2d3SAndreas Gohr     */
367*0b3fd2d3SAndreas Gohr    protected function expandDesKey(string $key): string
368*0b3fd2d3SAndreas Gohr    {
369*0b3fd2d3SAndreas Gohr        $bytes = [];
370*0b3fd2d3SAndreas Gohr
371*0b3fd2d3SAndreas Gohr        for ($i = 0; $i < 7; $i++) {
372*0b3fd2d3SAndreas Gohr            $bytes[$i] = ord($key[$i]);
373*0b3fd2d3SAndreas Gohr        }
374*0b3fd2d3SAndreas Gohr
375*0b3fd2d3SAndreas Gohr        return
376*0b3fd2d3SAndreas Gohr            chr($bytes[0] & 0xfe) .
377*0b3fd2d3SAndreas Gohr            chr(($bytes[0] << 7) | ($bytes[1] >> 1)) .
378*0b3fd2d3SAndreas Gohr            chr(($bytes[1] << 6) | ($bytes[2] >> 2)) .
379*0b3fd2d3SAndreas Gohr            chr(($bytes[2] << 5) | ($bytes[3] >> 3)) .
380*0b3fd2d3SAndreas Gohr            chr(($bytes[3] << 4) | ($bytes[4] >> 4)) .
381*0b3fd2d3SAndreas Gohr            chr(($bytes[4] << 3) | ($bytes[5] >> 5)) .
382*0b3fd2d3SAndreas Gohr            chr(($bytes[5] << 2) | ($bytes[6] >> 6)) .
383*0b3fd2d3SAndreas Gohr            chr($bytes[6] << 1);
384*0b3fd2d3SAndreas Gohr    }
385*0b3fd2d3SAndreas Gohr
386*0b3fd2d3SAndreas Gohr    /**
387*0b3fd2d3SAndreas Gohr     * @throws SaslException
388*0b3fd2d3SAndreas Gohr     */
389*0b3fd2d3SAndreas Gohr    protected function validateBufferLength(string $data, SaslContext $context): void
390*0b3fd2d3SAndreas Gohr    {
391*0b3fd2d3SAndreas Gohr        $maxbuf = $context->has('maxbuf') ? (int) $context->get('maxbuf') : self::MAXBUF;
392*0b3fd2d3SAndreas Gohr        if (strlen($data) > $maxbuf) {
393*0b3fd2d3SAndreas Gohr            throw new SaslException(sprintf(
394*0b3fd2d3SAndreas Gohr                'The wrapped buffer exceeds the maxbuf length of %s',
395*0b3fd2d3SAndreas Gohr                $maxbuf
396*0b3fd2d3SAndreas Gohr            ));
397*0b3fd2d3SAndreas Gohr        }
398*0b3fd2d3SAndreas Gohr    }
399*0b3fd2d3SAndreas Gohr}
400