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