< Summary

Information
Class: System.Net.Http.StringBuilderExtensions
Assembly: System.Net.Http
File(s): D:\runner\runtime\src\libraries\System.Net.Http\src\System\Net\Http\SocketsHttpHandler\AuthenticationHelper.Digest.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 28
Coverable lines: 28
Total lines: 453
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 6
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
AppendKeyValue(...)0%660%

File(s)

D:\runner\runtime\src\libraries\System.Net.Http\src\System\Net\Http\SocketsHttpHandler\AuthenticationHelper.Digest.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System.Collections.Generic;
 5using System.Diagnostics;
 6using System.IO;
 7using System.Net;
 8using System.Net.Http.Headers;
 9using System.Security.Cryptography;
 10using System.Text;
 11using System.Threading.Tasks;
 12
 13namespace System.Net.Http
 14{
 15    internal static partial class AuthenticationHelper
 16    {
 17        // Define digest constants
 18        private const string Qop = "qop";
 19        private const string Auth = "auth";
 20        private const string AuthInt = "auth-int";
 21        private const string Domain = "domain";
 22        private const string Nonce = "nonce";
 23        private const string NC = "nc";
 24        private const string Realm = "realm";
 25        private const string UserHash = "userhash";
 26        private const string Username = "username";
 27        private const string UsernameStar = "username*";
 28        private const string Algorithm = "algorithm";
 29        private const string Uri = "uri";
 30        private const string Sha256 = "SHA-256";
 31        private const string Md5 = "MD5";
 32        private const string Sha256Sess = "SHA-256-sess";
 33        private const string MD5Sess = "MD5-sess";
 34        private const string CNonce = "cnonce";
 35        private const string Opaque = "opaque";
 36        private const string Response = "response";
 37        private const string Stale = "stale";
 38
 39        public static async Task<string?> GetDigestTokenForCredential(NetworkCredential credential, HttpRequestMessage r
 40        {
 41            StringBuilder sb = StringBuilderCache.Acquire();
 42
 43            // It is mandatory for servers to implement sha-256 per RFC 7616
 44            // Keep MD5 for backward compatibility.
 45            string? algorithm;
 46            bool isAlgorithmSpecified = digestResponse.Parameters.TryGetValue(Algorithm, out algorithm);
 47            if (isAlgorithmSpecified)
 48            {
 49                if (!algorithm!.Equals(Sha256, StringComparison.OrdinalIgnoreCase) &&
 50                    !algorithm.Equals(Md5, StringComparison.OrdinalIgnoreCase) &&
 51                    !algorithm.Equals(Sha256Sess, StringComparison.OrdinalIgnoreCase) &&
 52                    !algorithm.Equals(MD5Sess, StringComparison.OrdinalIgnoreCase))
 53                {
 54                    if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(digestResponse, $"Algorithm not supported: 
 55                    return null;
 56                }
 57            }
 58            else
 59            {
 60                algorithm = Md5;
 61            }
 62
 63            // Check if nonce is there in challenge
 64            string? nonce;
 65            if (!digestResponse.Parameters.TryGetValue(Nonce, out nonce))
 66            {
 67                if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(digestResponse, "Nonce missing");
 68                return null;
 69            }
 70
 71            // opaque token may or may not exist
 72            string? opaque;
 73            digestResponse.Parameters.TryGetValue(Opaque, out opaque);
 74
 75            string? realm;
 76            if (!digestResponse.Parameters.TryGetValue(Realm, out realm))
 77            {
 78                if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(digestResponse, "Realm missing");
 79                return null;
 80            }
 81
 82            // Add username
 83            string? userhash;
 84            if (digestResponse.Parameters.TryGetValue(UserHash, out userhash) && userhash == "true")
 85            {
 86                sb.AppendKeyValue(Username, ComputeHash(credential.UserName + ":" + realm, algorithm));
 87                sb.AppendKeyValue(UserHash, userhash, includeQuotes: false);
 88            }
 89            else
 90            {
 91                if (!Ascii.IsValid(credential.UserName))
 92                {
 93                    string usernameStar = HeaderUtilities.Encode5987(credential.UserName);
 94                    sb.AppendKeyValue(UsernameStar, usernameStar, includeQuotes: false);
 95                }
 96                else
 97                {
 98                    sb.AppendKeyValue(Username, credential.UserName);
 99                }
 100            }
 101
 102            // Add realm
 103            sb.AppendKeyValue(Realm, realm);
 104
 105            // Add nonce
 106            sb.AppendKeyValue(Nonce, nonce);
 107
 108            Debug.Assert(request.RequestUri != null);
 109            // Add uri
 110            sb.AppendKeyValue(Uri, request.RequestUri.PathAndQuery);
 111
 112            // Set qop, default is auth
 113            string qop = Auth;
 114            bool isQopSpecified = digestResponse.Parameters.ContainsKey(Qop);
 115            if (isQopSpecified)
 116            {
 117                // Check if auth-int present in qop string
 118                int index1 = digestResponse.Parameters[Qop].IndexOf(AuthInt, StringComparison.Ordinal);
 119                if (index1 != -1)
 120                {
 121                    // Get index of auth if present in qop string
 122                    int index2 = digestResponse.Parameters[Qop].IndexOf(Auth, StringComparison.Ordinal);
 123
 124                    // If index2 < index1, auth option is available
 125                    // If index2 == index1, check if auth option available later in string after auth-int.
 126                    if (index2 == index1)
 127                    {
 128                        index2 = digestResponse.Parameters[Qop].IndexOf(Auth, index1 + AuthInt.Length, StringComparison.
 129                        if (index2 == -1)
 130                        {
 131                            qop = AuthInt;
 132                        }
 133                    }
 134                }
 135            }
 136
 137            // Set cnonce
 138            string cnonce = GetRandomAlphaNumericString();
 139
 140            // Calculate response
 141            string a1 = credential.UserName + ":" + realm + ":" + credential.Password;
 142            if (algorithm.EndsWith("sess", StringComparison.OrdinalIgnoreCase))
 143            {
 144                a1 = ComputeHash(a1, algorithm) + ":" + nonce + ":" + cnonce;
 145            }
 146
 147            string a2 = request.Method.Method + ":" + request.RequestUri.PathAndQuery;
 148            if (qop == AuthInt)
 149            {
 150                string content = request.Content == null ? string.Empty : await request.Content.ReadAsStringAsync().Conf
 151                a2 = a2 + ":" + ComputeHash(content, algorithm);
 152            }
 153
 154            string response;
 155            if (isQopSpecified)
 156            {
 157                response = ComputeHash(ComputeHash(a1, algorithm) + ":" +
 158                                            nonce + ":" +
 159                                            DigestResponse.NonceCount + ":" +
 160                                            cnonce + ":" +
 161                                            qop + ":" +
 162                                            ComputeHash(a2, algorithm), algorithm);
 163            }
 164            else
 165            {
 166                response = ComputeHash(ComputeHash(a1, algorithm) + ":" +
 167                            nonce + ":" +
 168                            ComputeHash(a2, algorithm), algorithm);
 169            }
 170
 171            // Add response
 172            sb.AppendKeyValue(Response, response, includeComma: opaque != null || isAlgorithmSpecified || isQopSpecified
 173
 174            // Add opaque
 175            if (opaque != null)
 176            {
 177                sb.AppendKeyValue(Opaque, opaque, includeComma: isAlgorithmSpecified || isQopSpecified);
 178            }
 179
 180            if (isAlgorithmSpecified)
 181            {
 182                // Add algorithm
 183                sb.AppendKeyValue(Algorithm, algorithm, includeQuotes: false, includeComma: isQopSpecified);
 184            }
 185
 186            if (isQopSpecified)
 187            {
 188                // Add qop
 189                sb.AppendKeyValue(Qop, qop, includeQuotes: false);
 190
 191                // Add nc
 192                sb.AppendKeyValue(NC, DigestResponse.NonceCount, includeQuotes: false);
 193
 194                // Add cnonce
 195                sb.AppendKeyValue(CNonce, cnonce, includeComma: false);
 196            }
 197
 198            return StringBuilderCache.GetStringAndRelease(sb);
 199        }
 200
 201        public static bool IsServerNonceStale(DigestResponse digestResponse)
 202        {
 203            return digestResponse.Parameters.TryGetValue(Stale, out string? stale) && stale == "true";
 204        }
 205
 206        private static string GetRandomAlphaNumericString()
 207        {
 208            const int Length = 16;
 209            const string CharacterSet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
 210            return RandomNumberGenerator.GetString(CharacterSet, Length);
 211        }
 212
 213        private static string ComputeHash(string data, string algorithm)
 214        {
 215            Span<byte> hashBuffer = stackalloc byte[SHA256.HashSizeInBytes]; // SHA256 is the largest hash produced
 216            byte[] dataBytes = Encoding.UTF8.GetBytes(data);
 217            int written;
 218
 219            if (algorithm.StartsWith(Sha256, StringComparison.OrdinalIgnoreCase))
 220            {
 221                written = SHA256.HashData(dataBytes, hashBuffer);
 222                Debug.Assert(written == SHA256.HashSizeInBytes);
 223            }
 224            else
 225            {
 226                // Disable MD5 insecure warning.
 227#pragma warning disable CA5351
 228                written = MD5.HashData(dataBytes, hashBuffer);
 229                Debug.Assert(written == MD5.HashSizeInBytes);
 230#pragma warning restore CA5351
 231            }
 232
 233            return Convert.ToHexStringLower(hashBuffer.Slice(0, written));
 234        }
 235
 236        internal sealed class DigestResponse
 237        {
 238            internal readonly Dictionary<string, string> Parameters = new Dictionary<string, string>(StringComparer.Ordi
 239            internal const string NonceCount = "00000001";
 240
 241            internal DigestResponse(string? challenge)
 242            {
 243                if (!string.IsNullOrEmpty(challenge))
 244                    Parse(challenge);
 245            }
 246
 247            private static bool CharIsSpaceOrTab(char ch)
 248            {
 249                return ch == ' ' || ch == '\t';
 250            }
 251
 252            private static bool MustValueBeQuoted(string key)
 253            {
 254                // As per the RFC, these string must be quoted for historical reasons.
 255                return key.Equals(Realm, StringComparison.OrdinalIgnoreCase) || key.Equals(Nonce, StringComparison.Ordin
 256                    key.Equals(Opaque, StringComparison.OrdinalIgnoreCase) || key.Equals(Qop, StringComparison.OrdinalIg
 257            }
 258
 259            private static string? GetNextKey(string data, int currentIndex, out int parsedIndex)
 260            {
 261                // Skip leading space or tab.
 262                while (currentIndex < data.Length && CharIsSpaceOrTab(data[currentIndex]))
 263                {
 264                    currentIndex++;
 265                }
 266
 267                // Start parsing key
 268                int start = currentIndex;
 269
 270                // Parse till '=' is encountered marking end of key.
 271                // Key cannot contain space or tab, break if either is found.
 272                while (currentIndex < data.Length && data[currentIndex] != '=' && !CharIsSpaceOrTab(data[currentIndex]))
 273                {
 274                    currentIndex++;
 275                }
 276
 277                if (currentIndex == data.Length)
 278                {
 279                    // Key didn't terminate with '='
 280                    parsedIndex = currentIndex;
 281                    return null;
 282                }
 283
 284                // Record end of key.
 285                int length = currentIndex - start;
 286                if (CharIsSpaceOrTab(data[currentIndex]))
 287                {
 288                    // Key parsing terminated due to ' ' or '\t'.
 289                    // Parse till '=' is found.
 290                    while (currentIndex < data.Length && CharIsSpaceOrTab(data[currentIndex]))
 291                    {
 292                        currentIndex++;
 293                    }
 294
 295                    if (currentIndex == data.Length || data[currentIndex] != '=')
 296                    {
 297                        // Key is invalid.
 298                        parsedIndex = currentIndex;
 299                        return null;
 300                    }
 301                }
 302
 303                // Skip trailing space and tab and '='
 304                while (currentIndex < data.Length && (CharIsSpaceOrTab(data[currentIndex]) || data[currentIndex] == '=')
 305                {
 306                    currentIndex++;
 307                }
 308
 309                // Set the parsedIndex to current valid char.
 310                parsedIndex = currentIndex;
 311                return data.Substring(start, length);
 312            }
 313
 314            private static string? GetNextValue(string data, int currentIndex, bool expectQuotes, out int parsedIndex)
 315            {
 316                Debug.Assert(currentIndex < data.Length && !CharIsSpaceOrTab(data[currentIndex]));
 317
 318                // If quoted value, skip first quote.
 319                bool quotedValue = false;
 320                if (data[currentIndex] == '"')
 321                {
 322                    quotedValue = true;
 323                    currentIndex++;
 324                }
 325
 326                if (expectQuotes && !quotedValue)
 327                {
 328                    parsedIndex = currentIndex;
 329                    return null;
 330                }
 331
 332                StringBuilder sb = StringBuilderCache.Acquire();
 333                while (currentIndex < data.Length && ((quotedValue && data[currentIndex] != '"') || (!quotedValue && dat
 334                {
 335                    sb.Append(data[currentIndex]);
 336                    currentIndex++;
 337
 338                    if (currentIndex == data.Length)
 339                        break;
 340
 341                    if (!quotedValue && CharIsSpaceOrTab(data[currentIndex]))
 342                        break;
 343
 344                    if (quotedValue && data[currentIndex] == '"' && data[currentIndex - 1] == '\\')
 345                    {
 346                        // Include the escaped quote.
 347                        sb.Append(data[currentIndex]);
 348                        currentIndex++;
 349                    }
 350                }
 351
 352                // Skip the quote.
 353                if (quotedValue)
 354                    currentIndex++;
 355
 356                // Skip any whitespace.
 357                while (currentIndex < data.Length && CharIsSpaceOrTab(data[currentIndex]))
 358                    currentIndex++;
 359
 360                // Return if this is last value.
 361                if (currentIndex == data.Length)
 362                {
 363                    parsedIndex = currentIndex;
 364                    return StringBuilderCache.GetStringAndRelease(sb);
 365                }
 366
 367                // A key-value pair should end with ','
 368                if (data[currentIndex++] != ',')
 369                {
 370                    parsedIndex = currentIndex;
 371                    return null;
 372                }
 373
 374                // Skip space and tab
 375                while (currentIndex < data.Length && CharIsSpaceOrTab(data[currentIndex]))
 376                {
 377                    currentIndex++;
 378                }
 379
 380                // Set parsedIndex to current valid char.
 381                parsedIndex = currentIndex;
 382                return StringBuilderCache.GetStringAndRelease(sb);
 383            }
 384
 385            private void Parse(string challenge)
 386            {
 387                int parsedIndex = 0;
 388                while (parsedIndex < challenge.Length)
 389                {
 390                    // Get the key.
 391                    string? key = GetNextKey(challenge, parsedIndex, out parsedIndex);
 392                    // Ensure key is not empty and parsedIndex is still in range.
 393                    if (string.IsNullOrEmpty(key) || parsedIndex >= challenge.Length)
 394                        break;
 395
 396                    // Get the value.
 397                    string? value = GetNextValue(challenge, parsedIndex, MustValueBeQuoted(key), out parsedIndex);
 398                    if (value == null)
 399                        break;
 400
 401                    // Ensure value is valid.
 402                    // Opaque, Domain and Realm can have empty string
 403                    if (value == string.Empty &&
 404                        !key.Equals(Opaque, StringComparison.OrdinalIgnoreCase) &&
 405                        !key.Equals(Domain, StringComparison.OrdinalIgnoreCase) &&
 406                        !key.Equals(Realm, StringComparison.OrdinalIgnoreCase))
 407                        break;
 408
 409                    // Add the key-value pair to Parameters.
 410                    Parameters.Add(key, value);
 411                }
 412            }
 413        }
 414    }
 415
 416    internal static class StringBuilderExtensions
 417    {
 418        public static void AppendKeyValue(this StringBuilder sb, string key, string value, bool includeQuotes = true, bo
 0419        {
 0420            sb.Append(key).Append('=');
 421
 0422            if (includeQuotes)
 0423            {
 0424                ReadOnlySpan<char> valueSpan = value;
 0425                sb.Append('"');
 0426                while (true)
 0427                {
 0428                    int i = valueSpan.IndexOfAny('"', '\\'); // Characters that require escaping in quoted string
 0429                    if (i >= 0)
 0430                    {
 0431                        sb.Append(valueSpan.Slice(0, i)).Append('\\').Append(valueSpan[i]);
 0432                        valueSpan = valueSpan.Slice(i + 1);
 0433                    }
 434                    else
 0435                    {
 0436                        sb.Append(valueSpan);
 0437                        break;
 438                    }
 0439                }
 0440                sb.Append('"');
 0441            }
 442            else
 0443            {
 0444                sb.Append(value);
 0445            }
 446
 0447            if (includeComma)
 0448            {
 0449                sb.Append(',').Append(' ');
 0450            }
 0451        }
 452    }
 453}