* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace FreeDSx\Sasl\Mechanism; use FreeDSx\Sasl\Challenge\ChallengeInterface; use FreeDSx\Sasl\Challenge\DigestMD5Challenge; use FreeDSx\Sasl\Exception\SaslException; use FreeDSx\Sasl\Message; use FreeDSx\Sasl\Security\DigestMD5SecurityLayer; use FreeDSx\Sasl\Security\SecurityLayerInterface; use FreeDSx\Sasl\SecurityStrength; /** * The Digest-MD5 mechanism. * * @author Chad Sikorra */ class DigestMD5Mechanism implements MechanismInterface { public const NAME = 'DIGEST-MD5'; protected const A2_SERVER = ':'; protected const A2_CLIENT = 'AUTHENTICATE:'; /** * {@inheritDoc} */ public function getName(): string { return self::NAME; } /** * {@inheritDoc} */ public function challenge(): ChallengeInterface { $challenge = new DigestMD5Challenge(); return $challenge; } /** * {@inheritDoc} */ public function securityStrength(): SecurityStrength { return new SecurityStrength( true, true, true, false, 128 ); } /** * {@inheritDoc} */ public function securityLayer(): SecurityLayerInterface { return new DigestMD5SecurityLayer(); } public function __toString() { return self::NAME; } /** * Generates the computed response value. RFC2831 2.1.2.1 * * HEX( KD ( HEX(H(A1)), * { nonce-value, ":" nc-value, ":", * cnonce-value, ":", qop-value, ":", HEX(H(A2)) })) * * If the "qop" directive's value is "auth", then A2 is: * * A2 = { "AUTHENTICATE:", digest-uri-value } * * If the "qop" value is "auth-int" or "auth-conf" then A2 is: * * A2 = { "AUTHENTICATE:", digest-uri-value, * ":00000000000000000000000000000000" } * * If this is the server context, then the beginning of A2 is just a semi-colon. * * @throws SaslException */ public static function computeResponse(string $password, Message $challenge, Message $response, bool $useServerMode = false): string { $a1 = self::computeA1($password, $challenge, $response); $qop = $response->get('qop'); $digestUri = $response->get('digest-uri'); $a2 = $useServerMode ? self::A2_SERVER : self::A2_CLIENT; if ($qop === 'auth') { $a2 .= $digestUri; } elseif ($qop === 'auth-int' || $qop === 'auth-conf') { $a2 .= $digestUri . ':00000000000000000000000000000000'; } else { throw new SaslException('The qop directive must be one of: auth, auth-conf, auth-int.'); } $a2 = hash('md5', $a2); return hash('md5', sprintf( '%s:%s:%s:%s:%s:%s', $a1, $challenge->get('nonce'), str_pad(dechex($response->get('nc')), 8, '0', STR_PAD_LEFT), $response->get('cnonce'), $response->get('qop'), $a2 )); } /** * If authzid is specified, then A1 is * * A1 = { H( { username-value, ":", realm-value, ":", passwd } ), * ":", nonce-value, ":", cnonce-value, ":", authzid-value } * * If authzid is not specified, then A1 is * * A1 = { H( { username-value, ":", realm-value, ":", passwd } ), * ":", nonce-value, ":", cnonce-value } * */ public static function computeA1(string $password, Message $challenge, Message $response): string { $a1 = hash('md5', sprintf( '%s:%s:%s', $response->get('username'), $response->get('realm'), $password ), true); $a1 = sprintf( '%s:%s:%s', $a1, $challenge->get('nonce'), $response->get('cnonce') ); if ($response->has('authzid')) { $a1 .= ':' . $response->get('authzid'); } return hash('md5', $a1); } }