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