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\Encoder; 13*0b3fd2d3SAndreas Gohr 14*0b3fd2d3SAndreas Gohruse FreeDSx\Sasl\Exception\SaslEncodingException; 15*0b3fd2d3SAndreas Gohruse FreeDSx\Sasl\Message; 16*0b3fd2d3SAndreas Gohruse FreeDSx\Sasl\SaslContext; 17*0b3fd2d3SAndreas Gohruse function dechex, explode, implode, preg_match, sprintf, str_pad, strlen, substr; 18*0b3fd2d3SAndreas Gohr 19*0b3fd2d3SAndreas Gohr/** 20*0b3fd2d3SAndreas Gohr * Responsible for encoding / decoding DIGEST-MD5 messages. 21*0b3fd2d3SAndreas Gohr * 22*0b3fd2d3SAndreas Gohr * @author Chad Sikorra <Chad.Sikorra@gmail.com> 23*0b3fd2d3SAndreas Gohr */ 24*0b3fd2d3SAndreas Gohrclass DigestMD5Encoder implements EncoderInterface 25*0b3fd2d3SAndreas Gohr{ 26*0b3fd2d3SAndreas Gohr protected const MATCH_KEY = '/(([a-zA-Z-]+)=)/'; 27*0b3fd2d3SAndreas Gohr 28*0b3fd2d3SAndreas Gohr protected const MATCH_QD_STR_VAL = '/("((.*?)(?<!\\\))")/'; 29*0b3fd2d3SAndreas Gohr 30*0b3fd2d3SAndreas Gohr protected const MATCH_DIGITS = '/([0-9]+)/'; 31*0b3fd2d3SAndreas Gohr 32*0b3fd2d3SAndreas Gohr protected const MATCH_ALPHA_NUMERIC = '/([A-Za-z0-9-]+)/'; 33*0b3fd2d3SAndreas Gohr 34*0b3fd2d3SAndreas Gohr protected const MATCH_LHEX = '/([0-9a-fA-F]{1,})/'; 35*0b3fd2d3SAndreas Gohr 36*0b3fd2d3SAndreas Gohr protected const ONCE_ONLY = [ 37*0b3fd2d3SAndreas Gohr 'stale', 38*0b3fd2d3SAndreas Gohr 'maxbuf', 39*0b3fd2d3SAndreas Gohr 'charset', 40*0b3fd2d3SAndreas Gohr 'algorithm', 41*0b3fd2d3SAndreas Gohr 'nonce', 42*0b3fd2d3SAndreas Gohr 'cnonce', 43*0b3fd2d3SAndreas Gohr 'nc', 44*0b3fd2d3SAndreas Gohr 'qop', 45*0b3fd2d3SAndreas Gohr 'digest-uri', 46*0b3fd2d3SAndreas Gohr 'response', 47*0b3fd2d3SAndreas Gohr 'cipher', 48*0b3fd2d3SAndreas Gohr ]; 49*0b3fd2d3SAndreas Gohr 50*0b3fd2d3SAndreas Gohr /** 51*0b3fd2d3SAndreas Gohr * @var string 52*0b3fd2d3SAndreas Gohr */ 53*0b3fd2d3SAndreas Gohr protected $binary; 54*0b3fd2d3SAndreas Gohr 55*0b3fd2d3SAndreas Gohr /** 56*0b3fd2d3SAndreas Gohr * @var int 57*0b3fd2d3SAndreas Gohr */ 58*0b3fd2d3SAndreas Gohr protected $pos = 0; 59*0b3fd2d3SAndreas Gohr 60*0b3fd2d3SAndreas Gohr /** 61*0b3fd2d3SAndreas Gohr * @var int 62*0b3fd2d3SAndreas Gohr */ 63*0b3fd2d3SAndreas Gohr protected $length = 0; 64*0b3fd2d3SAndreas Gohr 65*0b3fd2d3SAndreas Gohr /** 66*0b3fd2d3SAndreas Gohr * Tracks the number of times a specific option is encountered during decoding. 67*0b3fd2d3SAndreas Gohr */ 68*0b3fd2d3SAndreas Gohr protected $occurrences = []; 69*0b3fd2d3SAndreas Gohr 70*0b3fd2d3SAndreas Gohr /** 71*0b3fd2d3SAndreas Gohr * {@inheritDoc} 72*0b3fd2d3SAndreas Gohr */ 73*0b3fd2d3SAndreas Gohr public function decode(string $data, SaslContext $context): Message 74*0b3fd2d3SAndreas Gohr { 75*0b3fd2d3SAndreas Gohr return $this->parse($data, !$context->isServerMode()); 76*0b3fd2d3SAndreas Gohr } 77*0b3fd2d3SAndreas Gohr 78*0b3fd2d3SAndreas Gohr /** 79*0b3fd2d3SAndreas Gohr * {@inheritDoc} 80*0b3fd2d3SAndreas Gohr */ 81*0b3fd2d3SAndreas Gohr public function encode(Message $message, SaslContext $context): string 82*0b3fd2d3SAndreas Gohr { 83*0b3fd2d3SAndreas Gohr $response = ''; 84*0b3fd2d3SAndreas Gohr 85*0b3fd2d3SAndreas Gohr foreach ($message->toArray() as $key => $value) { 86*0b3fd2d3SAndreas Gohr if ($response !== '') { 87*0b3fd2d3SAndreas Gohr $response .= ','; 88*0b3fd2d3SAndreas Gohr } 89*0b3fd2d3SAndreas Gohr $response .= $key . '=' . $this->encodeOptValue( 90*0b3fd2d3SAndreas Gohr $key, 91*0b3fd2d3SAndreas Gohr $value, 92*0b3fd2d3SAndreas Gohr $context->isServerMode() 93*0b3fd2d3SAndreas Gohr ); 94*0b3fd2d3SAndreas Gohr } 95*0b3fd2d3SAndreas Gohr 96*0b3fd2d3SAndreas Gohr return $response; 97*0b3fd2d3SAndreas Gohr } 98*0b3fd2d3SAndreas Gohr 99*0b3fd2d3SAndreas Gohr protected function startParsing(string $binary): void 100*0b3fd2d3SAndreas Gohr { 101*0b3fd2d3SAndreas Gohr $this->binary = $binary; 102*0b3fd2d3SAndreas Gohr $this->pos = 0; 103*0b3fd2d3SAndreas Gohr $this->length = strlen($binary); 104*0b3fd2d3SAndreas Gohr $this->occurrences = []; 105*0b3fd2d3SAndreas Gohr } 106*0b3fd2d3SAndreas Gohr 107*0b3fd2d3SAndreas Gohr protected function endParsing(): void 108*0b3fd2d3SAndreas Gohr { 109*0b3fd2d3SAndreas Gohr $this->binary = ''; 110*0b3fd2d3SAndreas Gohr $this->pos = 0; 111*0b3fd2d3SAndreas Gohr $this->length = 0; 112*0b3fd2d3SAndreas Gohr $this->occurrences = []; 113*0b3fd2d3SAndreas Gohr } 114*0b3fd2d3SAndreas Gohr 115*0b3fd2d3SAndreas Gohr /** 116*0b3fd2d3SAndreas Gohr * @throws SaslEncodingException 117*0b3fd2d3SAndreas Gohr */ 118*0b3fd2d3SAndreas Gohr protected function parse(string $digest, bool $isServerMode): Message 119*0b3fd2d3SAndreas Gohr { 120*0b3fd2d3SAndreas Gohr $this->startParsing($digest); 121*0b3fd2d3SAndreas Gohr 122*0b3fd2d3SAndreas Gohr $message = new Message(); 123*0b3fd2d3SAndreas Gohr while ($this->pos < $this->length) { 124*0b3fd2d3SAndreas Gohr $keyMatches = null; 125*0b3fd2d3SAndreas Gohr if (!preg_match(self::MATCH_KEY, substr($this->binary, $this->pos), $keyMatches)) { 126*0b3fd2d3SAndreas Gohr throw new SaslEncodingException('The digest is malformed. Expected a key, but none was found.'); 127*0b3fd2d3SAndreas Gohr } 128*0b3fd2d3SAndreas Gohr $this->pos += strlen($keyMatches[1]); 129*0b3fd2d3SAndreas Gohr if (!isset($this->binary[$this->pos])) { 130*0b3fd2d3SAndreas Gohr throw new SaslEncodingException('Unexpected end of digest. Expected a value following a key.'); 131*0b3fd2d3SAndreas Gohr } 132*0b3fd2d3SAndreas Gohr $message->set($keyMatches[2], $this->parseOptValue($keyMatches[2], $isServerMode)); 133*0b3fd2d3SAndreas Gohr } 134*0b3fd2d3SAndreas Gohr $this->endParsing(); 135*0b3fd2d3SAndreas Gohr 136*0b3fd2d3SAndreas Gohr return $message; 137*0b3fd2d3SAndreas Gohr } 138*0b3fd2d3SAndreas Gohr 139*0b3fd2d3SAndreas Gohr /** 140*0b3fd2d3SAndreas Gohr * @return mixed 141*0b3fd2d3SAndreas Gohr * @throws SaslEncodingException 142*0b3fd2d3SAndreas Gohr */ 143*0b3fd2d3SAndreas Gohr protected function parseOptValue(string $opt, bool $isServerMode) 144*0b3fd2d3SAndreas Gohr { 145*0b3fd2d3SAndreas Gohr $value = null; 146*0b3fd2d3SAndreas Gohr 147*0b3fd2d3SAndreas Gohr switch ($opt) { 148*0b3fd2d3SAndreas Gohr case 'realm': 149*0b3fd2d3SAndreas Gohr case 'nonce': 150*0b3fd2d3SAndreas Gohr case 'username': 151*0b3fd2d3SAndreas Gohr case 'cnonce': 152*0b3fd2d3SAndreas Gohr case 'authzid': 153*0b3fd2d3SAndreas Gohr case 'digest-uri': 154*0b3fd2d3SAndreas Gohr $value = $this->parseQuotedValue(); 155*0b3fd2d3SAndreas Gohr break; 156*0b3fd2d3SAndreas Gohr case 'qop': 157*0b3fd2d3SAndreas Gohr case 'cipher': 158*0b3fd2d3SAndreas Gohr if ($isServerMode) { 159*0b3fd2d3SAndreas Gohr $value = $this->parseQuotedCommaList(); 160*0b3fd2d3SAndreas Gohr } else { 161*0b3fd2d3SAndreas Gohr $value = $this->parseRegex(self::MATCH_ALPHA_NUMERIC, 'The value is malformed.'); 162*0b3fd2d3SAndreas Gohr } 163*0b3fd2d3SAndreas Gohr break; 164*0b3fd2d3SAndreas Gohr case 'stale': 165*0b3fd2d3SAndreas Gohr $value = $this->parseExact('true'); 166*0b3fd2d3SAndreas Gohr break; 167*0b3fd2d3SAndreas Gohr case 'maxbuf': 168*0b3fd2d3SAndreas Gohr $value = $this->parseRegex(self::MATCH_DIGITS, 'Expected a series of digits for a key value.'); 169*0b3fd2d3SAndreas Gohr break; 170*0b3fd2d3SAndreas Gohr case 'algorithm': 171*0b3fd2d3SAndreas Gohr $value = $this->parseExact('md5-sess'); 172*0b3fd2d3SAndreas Gohr break; 173*0b3fd2d3SAndreas Gohr case 'charset': 174*0b3fd2d3SAndreas Gohr $value = $this->parseExact('utf-8'); 175*0b3fd2d3SAndreas Gohr break; 176*0b3fd2d3SAndreas Gohr case 'nc': 177*0b3fd2d3SAndreas Gohr $value = $this->parseLHexValue(8); 178*0b3fd2d3SAndreas Gohr break; 179*0b3fd2d3SAndreas Gohr case 'response': 180*0b3fd2d3SAndreas Gohr case 'rspauth': 181*0b3fd2d3SAndreas Gohr $value = $this->parseLHexValue(32); 182*0b3fd2d3SAndreas Gohr break; 183*0b3fd2d3SAndreas Gohr default: 184*0b3fd2d3SAndreas Gohr throw new SaslEncodingException(sprintf( 185*0b3fd2d3SAndreas Gohr 'Digest option %s is not supported.', 186*0b3fd2d3SAndreas Gohr $opt 187*0b3fd2d3SAndreas Gohr )); 188*0b3fd2d3SAndreas Gohr break; 189*0b3fd2d3SAndreas Gohr } 190*0b3fd2d3SAndreas Gohr 191*0b3fd2d3SAndreas Gohr if (isset($this->binary[$this->pos]) && $this->binary[$this->pos] !== ',') { 192*0b3fd2d3SAndreas Gohr throw new SaslEncodingException(sprintf( 193*0b3fd2d3SAndreas Gohr 'Expected a comma following digest value for %s.', 194*0b3fd2d3SAndreas Gohr $opt 195*0b3fd2d3SAndreas Gohr )); 196*0b3fd2d3SAndreas Gohr } 197*0b3fd2d3SAndreas Gohr if (isset($this->binary[$this->pos]) && $this->binary[$this->pos] === ',') { 198*0b3fd2d3SAndreas Gohr $this->pos++; 199*0b3fd2d3SAndreas Gohr } 200*0b3fd2d3SAndreas Gohr 201*0b3fd2d3SAndreas Gohr if (isset($this->occurrences[$opt]) && in_array($opt, self::ONCE_ONLY, true)) { 202*0b3fd2d3SAndreas Gohr throw new SaslEncodingException(sprintf('The option "%s" may occur only once.', $opt)); 203*0b3fd2d3SAndreas Gohr } elseif (isset($this->occurrences[$opt])) { 204*0b3fd2d3SAndreas Gohr $this->occurrences[$opt]++; 205*0b3fd2d3SAndreas Gohr } else { 206*0b3fd2d3SAndreas Gohr $this->occurrences[$opt] = 1; 207*0b3fd2d3SAndreas Gohr } 208*0b3fd2d3SAndreas Gohr 209*0b3fd2d3SAndreas Gohr return $value; 210*0b3fd2d3SAndreas Gohr } 211*0b3fd2d3SAndreas Gohr 212*0b3fd2d3SAndreas Gohr /** 213*0b3fd2d3SAndreas Gohr * @return mixed 214*0b3fd2d3SAndreas Gohr * @throws SaslEncodingException 215*0b3fd2d3SAndreas Gohr */ 216*0b3fd2d3SAndreas Gohr protected function encodeOptValue(string $name, $value, bool $isServerMode) 217*0b3fd2d3SAndreas Gohr { 218*0b3fd2d3SAndreas Gohr $encoded = null; 219*0b3fd2d3SAndreas Gohr 220*0b3fd2d3SAndreas Gohr switch ($name) { 221*0b3fd2d3SAndreas Gohr case 'realm': 222*0b3fd2d3SAndreas Gohr case 'nonce': 223*0b3fd2d3SAndreas Gohr case 'username': 224*0b3fd2d3SAndreas Gohr case 'cnonce': 225*0b3fd2d3SAndreas Gohr case 'authzid': 226*0b3fd2d3SAndreas Gohr case 'digest-uri': 227*0b3fd2d3SAndreas Gohr $encoded = '"' . str_replace(['\\', '"'], ['\\\\', '\"'], $value) . '"'; 228*0b3fd2d3SAndreas Gohr break; 229*0b3fd2d3SAndreas Gohr case 'qop': 230*0b3fd2d3SAndreas Gohr case 'cipher': 231*0b3fd2d3SAndreas Gohr if ($isServerMode) { 232*0b3fd2d3SAndreas Gohr $encoded = '"' . implode(',', (array) $value) . '"'; 233*0b3fd2d3SAndreas Gohr } else { 234*0b3fd2d3SAndreas Gohr $encoded = (string) $value; 235*0b3fd2d3SAndreas Gohr } 236*0b3fd2d3SAndreas Gohr break; 237*0b3fd2d3SAndreas Gohr case 'stale': 238*0b3fd2d3SAndreas Gohr $encoded = 'true'; 239*0b3fd2d3SAndreas Gohr break; 240*0b3fd2d3SAndreas Gohr case 'maxbuf': 241*0b3fd2d3SAndreas Gohr case 'algorithm': 242*0b3fd2d3SAndreas Gohr case 'charset': 243*0b3fd2d3SAndreas Gohr $encoded = (string) $value; 244*0b3fd2d3SAndreas Gohr break; 245*0b3fd2d3SAndreas Gohr case 'nc': 246*0b3fd2d3SAndreas Gohr $encoded = str_pad(dechex($value), 8, '0', STR_PAD_LEFT); 247*0b3fd2d3SAndreas Gohr break; 248*0b3fd2d3SAndreas Gohr case 'response': 249*0b3fd2d3SAndreas Gohr case 'rspauth': 250*0b3fd2d3SAndreas Gohr $encoded = $this->encodeLHexValue($value, 32); 251*0b3fd2d3SAndreas Gohr break; 252*0b3fd2d3SAndreas Gohr default: 253*0b3fd2d3SAndreas Gohr throw new SaslEncodingException(sprintf( 254*0b3fd2d3SAndreas Gohr 'Digest option %s is not supported.', 255*0b3fd2d3SAndreas Gohr $name 256*0b3fd2d3SAndreas Gohr )); 257*0b3fd2d3SAndreas Gohr break; 258*0b3fd2d3SAndreas Gohr } 259*0b3fd2d3SAndreas Gohr 260*0b3fd2d3SAndreas Gohr return $encoded; 261*0b3fd2d3SAndreas Gohr } 262*0b3fd2d3SAndreas Gohr 263*0b3fd2d3SAndreas Gohr /** 264*0b3fd2d3SAndreas Gohr * @throws SaslEncodingException 265*0b3fd2d3SAndreas Gohr */ 266*0b3fd2d3SAndreas Gohr protected function parseExact(string $expected): string 267*0b3fd2d3SAndreas Gohr { 268*0b3fd2d3SAndreas Gohr $length = strlen($expected); 269*0b3fd2d3SAndreas Gohr if (substr($this->binary, $this->pos, $length) !== $expected) { 270*0b3fd2d3SAndreas Gohr throw new SaslEncodingException(sprintf( 271*0b3fd2d3SAndreas Gohr 'Expected the directive value to be "%s", but it is not.', 272*0b3fd2d3SAndreas Gohr $expected 273*0b3fd2d3SAndreas Gohr )); 274*0b3fd2d3SAndreas Gohr } 275*0b3fd2d3SAndreas Gohr $this->pos += $length; 276*0b3fd2d3SAndreas Gohr 277*0b3fd2d3SAndreas Gohr return $expected; 278*0b3fd2d3SAndreas Gohr } 279*0b3fd2d3SAndreas Gohr 280*0b3fd2d3SAndreas Gohr /** 281*0b3fd2d3SAndreas Gohr * @throws SaslEncodingException 282*0b3fd2d3SAndreas Gohr */ 283*0b3fd2d3SAndreas Gohr protected function parseQuotedValue(): string 284*0b3fd2d3SAndreas Gohr { 285*0b3fd2d3SAndreas Gohr if (!preg_match(self::MATCH_QD_STR_VAL, substr($this->binary, $this->pos), $matches)) { 286*0b3fd2d3SAndreas Gohr throw new SaslEncodingException('The value is malformed. Expected a qdstr-val.'); 287*0b3fd2d3SAndreas Gohr } 288*0b3fd2d3SAndreas Gohr $this->pos += strlen($matches[1]); 289*0b3fd2d3SAndreas Gohr 290*0b3fd2d3SAndreas Gohr return stripslashes($matches[2]); 291*0b3fd2d3SAndreas Gohr } 292*0b3fd2d3SAndreas Gohr 293*0b3fd2d3SAndreas Gohr /** 294*0b3fd2d3SAndreas Gohr * @throws SaslEncodingException 295*0b3fd2d3SAndreas Gohr */ 296*0b3fd2d3SAndreas Gohr protected function parseQuotedCommaList(): array 297*0b3fd2d3SAndreas Gohr { 298*0b3fd2d3SAndreas Gohr $value = $this->parseQuotedValue(); 299*0b3fd2d3SAndreas Gohr 300*0b3fd2d3SAndreas Gohr return explode(',', $value); 301*0b3fd2d3SAndreas Gohr } 302*0b3fd2d3SAndreas Gohr 303*0b3fd2d3SAndreas Gohr /** 304*0b3fd2d3SAndreas Gohr * @throws SaslEncodingException 305*0b3fd2d3SAndreas Gohr */ 306*0b3fd2d3SAndreas Gohr protected function parseLHexValue(int $length): string 307*0b3fd2d3SAndreas Gohr { 308*0b3fd2d3SAndreas Gohr if (!preg_match(self::MATCH_LHEX, substr($this->binary, $this->pos), $matches)) { 309*0b3fd2d3SAndreas Gohr throw new SaslEncodingException('Expected a hex value.'); 310*0b3fd2d3SAndreas Gohr } 311*0b3fd2d3SAndreas Gohr if (strlen($matches[1]) !== $length) { 312*0b3fd2d3SAndreas Gohr throw new SaslEncodingException(sprintf('Expected the hex value to be %s characters long.', $length)); 313*0b3fd2d3SAndreas Gohr } 314*0b3fd2d3SAndreas Gohr $this->pos += strlen($matches[1]); 315*0b3fd2d3SAndreas Gohr 316*0b3fd2d3SAndreas Gohr return $matches[1]; 317*0b3fd2d3SAndreas Gohr } 318*0b3fd2d3SAndreas Gohr 319*0b3fd2d3SAndreas Gohr /** 320*0b3fd2d3SAndreas Gohr * @throws SaslEncodingException 321*0b3fd2d3SAndreas Gohr */ 322*0b3fd2d3SAndreas Gohr protected function parseRegex(string $regex, string $errorMessage) 323*0b3fd2d3SAndreas Gohr { 324*0b3fd2d3SAndreas Gohr if (!preg_match($regex, substr($this->binary, $this->pos), $matches)) { 325*0b3fd2d3SAndreas Gohr throw new SaslEncodingException($errorMessage); 326*0b3fd2d3SAndreas Gohr } 327*0b3fd2d3SAndreas Gohr $this->pos += strlen($matches[1]); 328*0b3fd2d3SAndreas Gohr 329*0b3fd2d3SAndreas Gohr return $matches[1]; 330*0b3fd2d3SAndreas Gohr } 331*0b3fd2d3SAndreas Gohr 332*0b3fd2d3SAndreas Gohr /** 333*0b3fd2d3SAndreas Gohr * @throws SaslEncodingException 334*0b3fd2d3SAndreas Gohr */ 335*0b3fd2d3SAndreas Gohr protected function encodeLHexValue(string $data, int $length): string 336*0b3fd2d3SAndreas Gohr { 337*0b3fd2d3SAndreas Gohr if (strlen($data) !== $length) { 338*0b3fd2d3SAndreas Gohr throw new SaslEncodingException(sprintf('Expected the encoded hex value to be %s characters long.', $length)); 339*0b3fd2d3SAndreas Gohr } 340*0b3fd2d3SAndreas Gohr 341*0b3fd2d3SAndreas Gohr return $data; 342*0b3fd2d3SAndreas Gohr } 343*0b3fd2d3SAndreas Gohr} 344