5555class SCRAM extends AbstractAuthentication implements ChallengeAuthenticationInterface, VerificationInterface
5656{
5757 private string $ hashAlgo ;
58-
5958 private ?string $ gs2Header = null ;
6059 private ?string $ cnonce = null ;
6160 private ?string $ firstMessageBare = null ;
@@ -80,7 +79,9 @@ public function __construct(Options $options, string $hash)
8079 // For instance "sha1" is accepted, while the registered hash name should be "SHA-1".
8180 $ replaced = preg_replace ('#^sha-(\d+)#i ' , 'sha\1 ' , $ hash );
8281 if ($ replaced === null ) {
82+ // @codeCoverageIgnoreStart
8383 throw new InvalidArgumentException ("Could not normalize hash ' $ hash' " );
84+ // @codeCoverageIgnoreEnd
8485 }
8586 $ normalizedHash = strtolower ($ replaced );
8687
@@ -176,22 +177,32 @@ private function generateResponse(
176177 /* @psalm-var array<int,string> $matches */
177178 $ matches = [];
178179
180+ $ downGradeProtectionRegexp = '(?:[A-Za-z0-9/+]{4})*(?:[A-Za-z0-9/+]{3}=|[A-Za-z0-9/+]{2}==) ' ;
181+
179182 $ serverMessageRegexp = "#^r=(?<nonce>[ \x21- \x2B\x2D- \x7E/]+) "
180183 . ",s=(?<salt>(?:[A-Za-z0-9/+]{4})*(?:[A-Za-z0-9/+]{3}=|[A-Za-z0-9/+]{2}==)?) "
181184 . ",i=(?<iteration>[0-9]*) "
182- . "(?:,d=(?<downgradeProtection>(?:[A-Za-z0-9/+]{4})*(?:[A-Za-z0-9/+]{3}=|[A-Za-z0-9/+]{2}==)))? "
183- . "(,[A-Za-z]=[^,])*$# " ;
185+ . "(?:,d=(?<downgradeProtection> $ downGradeProtectionRegexp))? "
186+ . "(?:,h=(?<downgradeProtectionSecure> $ downGradeProtectionRegexp))? "
187+ . "(?<additionalAttr>(?:,[A-Za-z]=[^,]+)*)$# " ;
184188
185189 if ($ this ->cnonce === null ||
186190 $ this ->gs2Header === null ||
187191 $ this ->firstMessageBare === null ||
188- !preg_match ($ serverMessageRegexp , $ challenge , $ matches )) {
192+ ! preg_match ($ serverMessageRegexp , $ challenge , $ matches )) {
193+ return false ;
194+ }
195+
196+ $ additionalAttribute = $ this ->parseAdditionalAttributes ($ matches ['additionalAttr ' ]);
197+
198+ if (isset ($ additionalAttribute ['m ' ])) {
189199 return false ;
190200 }
191201
192202 $ nonce = $ matches ['nonce ' ];
193203 $ salt = base64_decode ($ matches ['salt ' ]);
194- if (!$ salt ) {
204+
205+ if (! $ salt ) {
195206 // Invalid Base64.
196207 return false ;
197208 }
@@ -203,8 +214,14 @@ private function generateResponse(
203214 return false ;
204215 }
205216
206- if ($ matches ['downgradeProtection ' ] !== '' ) {
207- if (!$ this ->downgradeProtection ($ matches ['downgradeProtection ' ])) {
217+ if (! empty ($ matches ['downgradeProtectionSecure ' ])) {
218+ if (! $ this ->downgradeProtection ($ matches ['downgradeProtectionSecure ' ], "\x1f" , "\x1e" )) {
219+ return false ;
220+ }
221+ }
222+
223+ if (! empty ($ matches ['downgradeProtection ' ])) {
224+ if (! $ this ->downgradeProtection ($ matches ['downgradeProtection ' ], '| ' , ', ' )) {
208225 return false ;
209226 }
210227 }
@@ -224,13 +241,20 @@ private function generateResponse(
224241 return $ finalMessage . $ proof ;
225242 }
226243
227- private function downgradeProtection (string $ expectedDowngradeProtectionHash ): bool
228- {
244+ private function downgradeProtection (
245+ string $ expectedDowngradeProtectionHash ,
246+ string $ groupDelimiter ,
247+ string $ delimiter
248+ ): bool {
229249 if ($ this ->options ->getDowngradeProtection () === null ) {
230250 return true ;
231251 }
232252
233- $ actualDgPHash = base64_encode ($ this ->hash ($ this ->generateDowngradeProtectionVerification ()));
253+ $ actualDgPHash = base64_encode (
254+ $ this ->hash (
255+ $ this ->generateDowngradeProtectionVerification ($ groupDelimiter , $ delimiter )
256+ )
257+ );
234258 return $ expectedDowngradeProtectionHash === $ actualDgPHash ;
235259 }
236260
@@ -268,16 +292,23 @@ private function hi(
268292 */
269293 public function verify (string $ data ): bool
270294 {
271- $ verifierRegexp = '#^v=(?<verifier>(?:[A-Za-z0-9/+]{4})*(?:[A-Za-z0-9/+]{3}=|[A-Za-z0-9/+]{2}==)?)$# ' ;
295+ $ verifierRegexp = '#^v=(?<verifier>(?:[A-Za-z0-9/+]{4})*(?:[A-Za-z0-9/+]{3}=|[A-Za-z0-9/+]{2}==)?) '
296+ . '(?<additionalAttr>(?:,[A-Za-z]=[^,]+)*)$# ' ;
272297
273298 $ matches = [];
274299 if (empty ($ this ->saltedSecret ) ||
275300 $ this ->authMessage === null ||
276- !preg_match ($ verifierRegexp , $ data , $ matches )) {
301+ ! preg_match ($ verifierRegexp , $ data , $ matches )) {
277302 // This cannot be an outcome, you never sent the challenge's response.
278303 return false ;
279304 }
280305
306+ $ additionalAttribute = $ this ->parseAdditionalAttributes ($ matches ['additionalAttr ' ]);
307+
308+ if (isset ($ additionalAttribute ['m ' ])) {
309+ return false ;
310+ }
311+
281312 $ saltedSecret = $ this ->saltedSecret ->getValue ();
282313
283314 $ verifier = $ matches ['verifier ' ];
@@ -288,6 +319,18 @@ public function verify(string $data): bool
288319 return $ proposedServerSignature === $ serverSignature ;
289320 }
290321
322+ private function parseAdditionalAttributes (string $ addAttr ): array
323+ {
324+ return array_column (
325+ array_map (
326+ fn ($ v ) => explode ('= ' , trim ($ v ), 2 ),
327+ array_filter (explode (', ' , $ addAttr ))
328+ ),
329+ 1 ,
330+ 0
331+ );
332+ }
333+
291334 private function hash (string $ data ): string
292335 {
293336 return hash ($ this ->hashAlgo , $ data , true );
0 commit comments