diff --git a/src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthenticationHandler.cs b/src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthenticationHandler.cs index bc63cd1c8..257c36a15 100644 --- a/src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthenticationHandler.cs +++ b/src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthenticationHandler.cs @@ -66,7 +66,7 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop var nonceGenerator = Options.NonceGenerator; var staleNonce = Context.Items[DigestFields.Stale] as string ?? "false"; AuthenticationHandlerFeature.Set(await HandleAuthenticateOnceSafeAsync().ConfigureAwait(false), Context); // so annoying that Microsoft does not propagate AuthenticateResult properly - other have noticed as well: https://github.com/dotnet/aspnetcore/issues/44100 - Decorator.Enclose(Response.Headers).TryAdd(HeaderNames.WWWAuthenticate, string.Create(CultureInfo.InvariantCulture, $"{DigestAuthorizationHeader.Scheme} realm=\"{Options.Realm}\", qop=\"auth, auth-int\", nonce=\"{nonceGenerator(DateTime.UtcNow, etag, nonceSecret())}\", opaque=\"{opaqueGenerator()}\", stale={staleNonce}, algorithm={DigestAuthenticationMiddleware.ParseAlgorithm(Options.Algorithm)}")); + Decorator.Enclose(Response.Headers).TryAdd(HeaderNames.WWWAuthenticate, string.Create(CultureInfo.InvariantCulture, $"{DigestAuthorizationHeader.Scheme} realm=\"{Options.Realm}\", qop=\"auth, auth-int\", nonce=\"{nonceGenerator(DateTime.UtcNow, etag, nonceSecret())}\", opaque=\"{opaqueGenerator()}\", stale={staleNonce}, algorithm={DigestAuthenticationMiddleware.ParseAlgorithm(Options.DigestAlgorithm)}")); await base.HandleChallengeAsync(properties).ConfigureAwait(false); } } diff --git a/src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthenticationMiddleware.cs b/src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthenticationMiddleware.cs index 3ee46e678..ae91b3444 100644 --- a/src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthenticationMiddleware.cs +++ b/src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthenticationMiddleware.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; using Cuemon.Collections.Generic; using Cuemon.IO; -using Cuemon.Security.Cryptography; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; @@ -61,7 +60,7 @@ await Decorator.Enclose(context).InvokeUnauthorizedExceptionAsync(Options, princ var nonceSecret = Options.NonceSecret; var nonceGenerator = Options.NonceGenerator; var staleNonce = dc.Items[DigestFields.Stale] as string ?? "false"; - Decorator.Enclose(dc.Response.Headers).TryAdd(HeaderNames.WWWAuthenticate, string.Create(CultureInfo.InvariantCulture, $"{DigestAuthorizationHeader.Scheme} realm=\"{Options.Realm}\", qop=\"auth, auth-int\", nonce=\"{nonceGenerator(DateTime.UtcNow, etag, nonceSecret())}\", opaque=\"{opaqueGenerator()}\", stale={staleNonce}, algorithm={ParseAlgorithm(Options.Algorithm)}")); + Decorator.Enclose(dc.Response.Headers).TryAdd(HeaderNames.WWWAuthenticate, string.Create(CultureInfo.InvariantCulture, $"{DigestAuthorizationHeader.Scheme} realm=\"{Options.Realm}\", qop=\"auth, auth-int\", nonce=\"{nonceGenerator(DateTime.UtcNow, etag, nonceSecret())}\", opaque=\"{opaqueGenerator()}\", stale={staleNonce}, algorithm={ParseAlgorithm(Options.DigestAlgorithm)}")); }).ConfigureAwait(false); } @@ -138,14 +137,22 @@ internal static DigestAuthorizationHeader AuthorizationHeaderParser(HttpContext return DigestAuthorizationHeader.Create(authorizationHeader); } - internal static string ParseAlgorithm(UnkeyedCryptoAlgorithm algorithm) + internal static string ParseAlgorithm(DigestCryptoAlgorithm algorithm) { switch (algorithm) { - case UnkeyedCryptoAlgorithm.Sha256: + case DigestCryptoAlgorithm.Md5: + return "MD5"; + case DigestCryptoAlgorithm.Md5Session: + return "MD5-sess"; + case DigestCryptoAlgorithm.Sha256: return "SHA-256"; - case UnkeyedCryptoAlgorithm.Sha512: + case DigestCryptoAlgorithm.Sha256Session: + return "SHA-256-sess"; + case DigestCryptoAlgorithm.Sha512Slash256: return "SHA-512-256"; + case DigestCryptoAlgorithm.Sha512Slash256Session: + return "SHA-512-256-sess"; default: return "MD5"; } diff --git a/src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthenticationOptions.cs b/src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthenticationOptions.cs index 0f638d665..a8a5005b0 100644 --- a/src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthenticationOptions.cs +++ b/src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthenticationOptions.cs @@ -23,14 +23,14 @@ public sealed class DigestAuthenticationOptions : AuthenticationOptions /// Initial Value /// /// - /// - /// - /// - /// /// /// null /// /// + /// + /// + /// + /// /// /// A default implementation of a nonce generator. /// @@ -58,7 +58,7 @@ public sealed class DigestAuthenticationOptions : AuthenticationOptions /// public DigestAuthenticationOptions() { - Algorithm = UnkeyedCryptoAlgorithm.Sha256; + DigestAlgorithm = DigestCryptoAlgorithm.Sha256; OpaqueGenerator = () => Generate.RandomString(32, Alphanumeric.Hexadecimal).ToLowerInvariant(); NonceExpiredParser = (nonce, timeToLive) => { @@ -103,8 +103,14 @@ public DigestAuthenticationOptions() /// /// The algorithm of the HTTP Digest Access Authentication. /// Allowed values are: , and . + [Obsolete("This member is obsolete and will be removed in a future version. Use DigestAlgorithm property instead.")] public UnkeyedCryptoAlgorithm Algorithm { get; set; } + /// + /// Specifies the cryptographic algorithm used in HTTP Digest Access Authentication. Default is . + /// + public DigestCryptoAlgorithm DigestAlgorithm { get; set; } + /// /// Gets the realm that defines the protection space. /// @@ -139,7 +145,7 @@ public DigestAuthenticationOptions() /// Gets or sets a value indicating whether the server should bypass the calculation of HA1 password representation. /// /// true if the server should bypass the calculation of HA1 password representation; otherwise, false. - /// When enabled, the server reads the HA1 value directly from a secured storage. + /// When enabled, the server reads the HA1 value directly from a secured storage, hence this cannot be used in combination with session variants of . public bool UseServerSideHa1Storage { get; set; } /// diff --git a/src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthorizationHeaderBuilder.cs b/src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthorizationHeaderBuilder.cs index de54154b9..73b2f7389 100644 --- a/src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthorizationHeaderBuilder.cs +++ b/src/Cuemon.AspNetCore.Authentication/Digest/DigestAuthorizationHeaderBuilder.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Globalization; using System.Net.Http.Headers; using System.Text; @@ -13,16 +14,46 @@ namespace Cuemon.AspNetCore.Authentication.Digest /// public class DigestAuthorizationHeaderBuilder : AuthorizationHeaderBuilder { + /// + /// Initializes a new instance of the class. + /// + public DigestAuthorizationHeaderBuilder() : this(DigestCryptoAlgorithm.Sha256) + { + } + /// /// Initializes a new instance of the class. /// /// The algorithm to use when computing HA1, HA2 and/or RESPONSE value(s). /// Allowed values for are: , and . - public DigestAuthorizationHeaderBuilder(UnkeyedCryptoAlgorithm algorithm = UnkeyedCryptoAlgorithm.Sha256) : base(DigestAuthorizationHeader.Scheme) + [Obsolete("This constructor is obsolete and will be removed in a future version. Use DigestAlgorithm variant instead.")] + public DigestAuthorizationHeaderBuilder(UnkeyedCryptoAlgorithm algorithm) : this(Validator.CheckParameter( + () => + { + Validator.ThrowIfEqual(algorithm, UnkeyedCryptoAlgorithm.Sha1, nameof(algorithm)); + Validator.ThrowIfEqual(algorithm, UnkeyedCryptoAlgorithm.Sha384, nameof(algorithm)); + switch (algorithm) + { + case UnkeyedCryptoAlgorithm.Md5: + return DigestCryptoAlgorithm.Md5; + case UnkeyedCryptoAlgorithm.Sha256: + return DigestCryptoAlgorithm.Sha256; + case UnkeyedCryptoAlgorithm.Sha512: + return DigestCryptoAlgorithm.Sha512Slash256; + default: + throw new InvalidEnumArgumentException(nameof(algorithm), (int)algorithm, typeof(UnkeyedCryptoAlgorithm)); + } + })) { - Validator.ThrowIfEqual(algorithm, UnkeyedCryptoAlgorithm.Sha1, nameof(algorithm)); - Validator.ThrowIfEqual(algorithm, UnkeyedCryptoAlgorithm.Sha384, nameof(algorithm)); - Algorithm = algorithm; + } + + /// + /// Initializes a new instance of the class. + /// + /// The algorithm to use when computing HA1, HA2 and/or RESPONSE value(s). + public DigestAuthorizationHeaderBuilder(DigestCryptoAlgorithm algorithm) : base(DigestAuthorizationHeader.Scheme) + { + DigestAlgorithm = algorithm; MapRelation(nameof(AddResponse), DigestFields.Response); MapRelation(nameof(AddRealm), DigestFields.Realm); MapRelation(nameof(AddUserName), DigestFields.UserName); @@ -40,7 +71,15 @@ public DigestAuthorizationHeaderBuilder(UnkeyedCryptoAlgorithm algorithm = Unkey /// Gets the algorithm of the HTTP Digest Access Authentication. /// /// The algorithm of the HTTP Digest Access Authentication. - public UnkeyedCryptoAlgorithm Algorithm { get; private set; } + + [Obsolete("This member is obsolete and will be removed in a future version. Use DigestAlgorithm property instead.")] + public UnkeyedCryptoAlgorithm Algorithm { get; } + + /// + /// Gets the algorithm of the HTTP Digest Access Authentication. + /// + /// The algorithm of the HTTP Digest Access Authentication. + public DigestCryptoAlgorithm DigestAlgorithm { get; private set; } /// /// Associates the field with the specified . @@ -161,7 +200,7 @@ private DigestAuthorizationHeaderBuilder AddFromWwwAuthenticateHeader(string www public DigestAuthorizationHeaderBuilder AddFromDigestAuthorizationHeader(DigestAuthorizationHeader header) { Validator.ThrowIfNull(header); - Algorithm = ParseAlgorithm(header.Algorithm); + DigestAlgorithm = ParseAlgorithm(header.Algorithm); AddUserName(header.UserName); AddRealm(header.Realm); AddUri(header.Uri); @@ -172,11 +211,15 @@ public DigestAuthorizationHeaderBuilder AddFromDigestAuthorizationHeader(DigestA return this; } - private static UnkeyedCryptoAlgorithm ParseAlgorithm(string algorithm) + private static DigestCryptoAlgorithm ParseAlgorithm(string algorithm) { - if (algorithm.StartsWith("SHA-512", StringComparison.OrdinalIgnoreCase)) { return UnkeyedCryptoAlgorithm.Sha512; } - if (algorithm.StartsWith("SHA-256", StringComparison.OrdinalIgnoreCase)) { return UnkeyedCryptoAlgorithm.Sha256; } - return UnkeyedCryptoAlgorithm.Md5; + if (algorithm.Equals("MD5", StringComparison.OrdinalIgnoreCase)) { return DigestCryptoAlgorithm.Md5; } + if (algorithm.Equals("SHA-256", StringComparison.OrdinalIgnoreCase)) { return DigestCryptoAlgorithm.Sha256; } + if (algorithm.Equals("SHA-512-256", StringComparison.OrdinalIgnoreCase)) { return DigestCryptoAlgorithm.Sha512Slash256; } + if (algorithm.Equals("MD5-sess", StringComparison.OrdinalIgnoreCase)) { return DigestCryptoAlgorithm.Md5Session; } + if (algorithm.Equals("SHA-256-sess", StringComparison.OrdinalIgnoreCase)) { return DigestCryptoAlgorithm.Sha256Session; } + if (algorithm.Equals("SHA-512-256-sess", StringComparison.OrdinalIgnoreCase)) { return DigestCryptoAlgorithm.Sha512Slash256Session; } + throw new NotSupportedException($"The algorithm '{algorithm}' is not supported."); } /// @@ -198,15 +241,31 @@ public DigestAuthorizationHeaderBuilder AddResponse(string password, string meth /// Computes a by parameter defined hash value of the required values for the HTTP Digest access authentication HA1. /// /// The password to include in the HA1 computed value. - /// A in the format of H(::). H is determined by . + /// A in the format of H(::). H is determined by . public virtual string ComputeHash1(string password) { Validator.ThrowIfNullOrWhitespace(password); ValidateData(DigestFields.UserName, DigestFields.Realm); - return UnkeyedHashFactory.CreateCrypto(Algorithm).ComputeHash(string.Format(CultureInfo.InvariantCulture, "{0}:{1}:{2}", Data[DigestFields.UserName], Data[DigestFields.Realm], password), o => + var crypto = DigestHashFactory.CreateCrypto(DigestAlgorithm); + + var ha1 = crypto.ComputeHash(string.Format(CultureInfo.InvariantCulture, "{0}:{1}:{2}", Data[DigestFields.UserName], Data[DigestFields.Realm], password), o => { o.Encoding = Encoding.UTF8; }).ToHexadecimalString(); + + switch (DigestAlgorithm) + { + case DigestCryptoAlgorithm.Md5Session: + case DigestCryptoAlgorithm.Sha256Session: + case DigestCryptoAlgorithm.Sha512Slash256Session: + ha1 = crypto.ComputeHash(string.Format(CultureInfo.InvariantCulture, "{0}:{1}:{2}", ha1, Data[DigestFields.Nonce], Data[DigestFields.ClientNonce]), o => + { + o.Encoding = Encoding.UTF8; + }).ToHexadecimalString(); + break; + } + + return ha1; } /// @@ -214,7 +273,7 @@ public virtual string ComputeHash1(string password) /// /// The HTTP method to include in the HA2 computed value. /// The entity body to apply in the signature when qop is set to auth-int. - /// A in the format of H(:) OR H(::H()). H is determined by . + /// A in the format of H(:) OR H(::H()). H is determined by . public virtual string ComputeHash2(string method, string entityBody = null) { Validator.ThrowIfNullOrWhitespace(method); @@ -226,8 +285,8 @@ public virtual string ComputeHash2(string method, string entityBody = null) var hashFields = !hasIntegrityProtection ? string.Create(CultureInfo.InvariantCulture, $"{method}:{Data[DigestFields.DigestUri]}") - : string.Create(CultureInfo.InvariantCulture, $"{method}:{Data[DigestFields.DigestUri]}:{UnkeyedHashFactory.CreateCrypto(Algorithm).ComputeHash(entityBody, o => o.Encoding = Encoding.UTF8).ToHexadecimalString()}"); - return UnkeyedHashFactory.CreateCrypto(Algorithm).ComputeHash(hashFields, o => + : string.Create(CultureInfo.InvariantCulture, $"{method}:{Data[DigestFields.DigestUri]}:{DigestHashFactory.CreateCrypto(DigestAlgorithm).ComputeHash(entityBody, o => o.Encoding = Encoding.UTF8).ToHexadecimalString()}"); + return DigestHashFactory.CreateCrypto(DigestAlgorithm).ComputeHash(hashFields, o => { o.Encoding = Encoding.UTF8; }).ToHexadecimalString(); @@ -238,13 +297,13 @@ public virtual string ComputeHash2(string method, string entityBody = null) /// /// The HA1 to include in the RESPONSE computed value. /// The HA2 to include in the RESPONSE computed value. - /// A in the format of H(:::::). H is determined by . + /// A in the format of H(:::::). H is determined by . public virtual string ComputeResponse(string hash1, string hash2) { Validator.ThrowIfNullOrWhitespace(hash1); Validator.ThrowIfNullOrWhitespace(hash2); ValidateData(DigestFields.Nonce, DigestFields.NonceCount, DigestFields.ClientNonce, DigestFields.QualityOfProtection); - return UnkeyedHashFactory.CreateCrypto(Algorithm).ComputeHash(string.Create(CultureInfo.InvariantCulture, $"{hash1}:{Data[DigestFields.Nonce]}:{Data[DigestFields.NonceCount]}:{Data[DigestFields.ClientNonce]}:{Data[DigestFields.QualityOfProtection]}:{hash2}"), o => + return DigestHashFactory.CreateCrypto(DigestAlgorithm).ComputeHash(string.Create(CultureInfo.InvariantCulture, $"{hash1}:{Data[DigestFields.Nonce]}:{Data[DigestFields.NonceCount]}:{Data[DigestFields.ClientNonce]}:{Data[DigestFields.QualityOfProtection]}:{hash2}"), o => { o.Encoding = Encoding.UTF8; }).ToHexadecimalString(); diff --git a/src/Cuemon.AspNetCore.Authentication/Digest/DigestCryptoAlgorithm.cs b/src/Cuemon.AspNetCore.Authentication/Digest/DigestCryptoAlgorithm.cs new file mode 100644 index 000000000..c306c7a54 --- /dev/null +++ b/src/Cuemon.AspNetCore.Authentication/Digest/DigestCryptoAlgorithm.cs @@ -0,0 +1,38 @@ +namespace Cuemon.AspNetCore.Authentication.Digest +{ + /// + /// Specifies the cryptographic algorithms used in Digest authentication. + /// + public enum DigestCryptoAlgorithm + { + /// + /// The Message Digest 5 (MD5) algorithm (128 bits). + /// + Md5 = -2, + + /// + /// The Message Digest 5 (MD5) algorithm (128 bits) session variant. + /// + Md5Session = -1, + + /// + /// The Secure Hashing Algorithm (SHA256) algorithm (256 bits). + /// + Sha256 = 0, + + /// + /// The Secure Hashing Algorithm (SHA256) algorithm (256 bits) session variant. + /// + Sha256Session = 1, + + /// + /// The Secure Hashing Algorithm (SHA512/256) algorithm (256 bits). + /// + Sha512Slash256 = 2, + + /// + /// The Secure Hashing Algorithm (SHA512/256) algorithm (256 bits) session variant. + /// + Sha512Slash256Session = 3 + } +} diff --git a/src/Cuemon.AspNetCore.Authentication/Digest/DigestHashFactory.cs b/src/Cuemon.AspNetCore.Authentication/Digest/DigestHashFactory.cs new file mode 100644 index 000000000..13013c525 --- /dev/null +++ b/src/Cuemon.AspNetCore.Authentication/Digest/DigestHashFactory.cs @@ -0,0 +1,35 @@ +using System; +using Cuemon.Security; +using Cuemon.Security.Cryptography; + +namespace Cuemon.AspNetCore.Authentication.Digest +{ + /// + /// Provides access to factory methods for creating and configuring instances based on . + /// + public static class DigestHashFactory + { + /// + /// Creates an instance of a cryptographic implementation that derives from with the specified . + /// + /// The that defines the cryptographic implementation. Default is . + /// A implementation of the by parameter specified . + public static Hash CreateCrypto(DigestCryptoAlgorithm algorithm = default) + { + switch (algorithm) + { + case DigestCryptoAlgorithm.Md5: + case DigestCryptoAlgorithm.Md5Session: + return UnkeyedHashFactory.CreateCrypto(UnkeyedCryptoAlgorithm.Md5); + case DigestCryptoAlgorithm.Sha256: + case DigestCryptoAlgorithm.Sha256Session: + return UnkeyedHashFactory.CreateCrypto(UnkeyedCryptoAlgorithm.Sha256); + case DigestCryptoAlgorithm.Sha512Slash256: + case DigestCryptoAlgorithm.Sha512Slash256Session: + return UnkeyedHashFactory.CreateCrypto(UnkeyedCryptoAlgorithm.Sha512Slash256); + default: + throw new ArgumentOutOfRangeException(nameof(algorithm), algorithm, $"The specified {nameof(algorithm)} is not supported."); + } + } + } +} diff --git a/src/Cuemon.Security.Cryptography/SHA512256.cs b/src/Cuemon.Security.Cryptography/SHA512256.cs new file mode 100644 index 000000000..498408445 --- /dev/null +++ b/src/Cuemon.Security.Cryptography/SHA512256.cs @@ -0,0 +1,219 @@ +using System; +using System.Security.Cryptography; + +namespace Cuemon.Security.Cryptography +{ + /// + /// Represents the SHA-512/256 cryptographic hash algorithm, which produces a 256-bit hash value using the SHA-512 algorithm as its base. + /// + /// Full disclosure; this class was created in collaboration with OpenAI ChatGPT. Have a look at the prompt here: https://chatgpt.com/share/67fbd1fe-17d8-8010-8144-e94251c2e79d + public sealed class SHA512256 : HashAlgorithm + { + private const int BlockSize = 128; // 1024 bits + private const int DigestLength = 32; // 256 bits + + private static readonly ulong[] K = new ulong[] + { + // Constants used in the SHA-512 algorithm + 0x428a2f98d728ae22, 0x7137449123ef65cd, 0xb5c0fbcfec4d3b2f, 0xe9b5dba58189dbbc, + 0x3956c25bf348b538, 0x59f111f1b605d019, 0x923f82a4af194f9b, 0xab1c5ed5da6d8118, + 0xd807aa98a3030242, 0x12835b0145706fbe, 0x243185be4ee4b28c, 0x550c7dc3d5ffb4e2, + 0x72be5d74f27b896f, 0x80deb1fe3b1696b1, 0x9bdc06a725c71235, 0xc19bf174cf692694, + 0xe49b69c19ef14ad2, 0xefbe4786384f25e3, 0x0fc19dc68b8cd5b5, 0x240ca1cc77ac9c65, + 0x2de92c6f592b0275, 0x4a7484aa6ea6e483, 0x5cb0a9dcbd41fbd4, 0x76f988da831153b5, + 0x983e5152ee66dfab, 0xa831c66d2db43210, 0xb00327c898fb213f, 0xbf597fc7beef0ee4, + 0xc6e00bf33da88fc2, 0xd5a79147930aa725, 0x06ca6351e003826f, 0x142929670a0e6e70, + 0x27b70a8546d22ffc, 0x2e1b21385c26c926, 0x4d2c6dfc5ac42aed, 0x53380d139d95b3df, + 0x650a73548baf63de, 0x766a0abb3c77b2a8, 0x81c2c92e47edaee6, 0x92722c851482353b, + 0xa2bfe8a14cf10364, 0xa81a664bbc423001, 0xc24b8b70d0f89791, 0xc76c51a30654be30, + 0xd192e819d6ef5218, 0xd69906245565a910, 0xf40e35855771202a, 0x106aa07032bbd1b8, + 0x19a4c116b8d2d0c8, 0x1e376c085141ab53, 0x2748774cdf8eeb99, 0x34b0bcb5e19b48a8, + 0x391c0cb3c5c95a63, 0x4ed8aa4ae3418acb, 0x5b9cca4f7763e373, 0x682e6ff3d6b2b8a3, + 0x748f82ee5defb2fc, 0x78a5636f43172f60, 0x84c87814a1f0ab72, 0x8cc702081a6439ec, + 0x90befffa23631e28, 0xa4506cebde82bde9, 0xbef9a3f7b2c67915, 0xc67178f2e372532b, + 0xca273eceea26619c, 0xd186b8c721c0c207, 0xeada7dd6cde0eb1e, 0xf57d4f7fee6ed178, + 0x06f067aa72176fba, 0x0a637dc5a2c898a6, 0x113f9804bef90dae, 0x1b710b35131c471b, + 0x28db77f523047d84, 0x32caab7b40c72493, 0x3c9ebe0a15c9bebc, 0x431d67c49c100d4c, + 0x4cc5d4becb3e42b6, 0x597f299cfc657e2a, 0x5fcb6fab3ad6faec, 0x6c44198c4a475817 + }; + + private static readonly ulong[] IV512_256 = new ulong[] + { + // Initial hash values for SHA-512/256 + 0x22312194FC2BF72C, 0x9F555FA3C84C64C2, + 0x2393B86B6F53B151, 0x963877195940EABD, + 0x96283EE2A88EFFE3, 0xBE5E1E2553863992, + 0x2B0199FC2C85B8AA, 0x0EB72DDC81C52CA2 + }; + + private ulong[] _H = new ulong[8]; + private ulong[] _W = new ulong[80]; + private byte[] _buffer = new byte[BlockSize]; + private int _bufferPos; + private ulong _bitCountHigh; + private ulong _bitCountLow; + + /// + /// Initializes a new instance of the class. + /// + public SHA512256() + { + Initialize(); + HashSizeValue = 256; + } + + /// + /// Initializes the hash algorithm, resetting its state. + /// + public override void Initialize() + { + Array.Copy(IV512_256, _H, 8); + Array.Clear(_buffer, 0, _buffer.Length); + _bufferPos = 0; + _bitCountHigh = 0; + _bitCountLow = 0; + } + + /// + /// Routes data written to the object into the hash algorithm for computing the hash. + /// + /// The input data. + /// The offset into the byte array from which to begin using data. + /// The number of bytes in the array to use as data. + protected override void HashCore(byte[] array, int ibStart, int cbSize) + { + while (cbSize > 0) + { + int toCopy = Math.Min(BlockSize - _bufferPos, cbSize); + Array.Copy(array, ibStart, _buffer, _bufferPos, toCopy); + _bufferPos += toCopy; + ibStart += toCopy; + cbSize -= toCopy; + + AddLength((ulong)(toCopy * 8)); // track total bit count + + if (_bufferPos == BlockSize) + { + ProcessBlock(_buffer, 0); + _bufferPos = 0; + } + } + } + + /// + /// Finalizes the hash computation after the last data is processed. + /// + /// The computed hash code. + protected override byte[] HashFinal() + { + // Padding + _buffer[_bufferPos++] = 0x80; + if (_bufferPos > BlockSize - 16) + { + Array.Clear(_buffer, _bufferPos, BlockSize - _bufferPos); + ProcessBlock(_buffer, 0); + _bufferPos = 0; + } + + Array.Clear(_buffer, _bufferPos, BlockSize - _bufferPos); + + // Append total bit count (128-bit big-endian) + WriteULongBE(_bitCountHigh, _buffer, BlockSize - 16); + WriteULongBE(_bitCountLow, _buffer, BlockSize - 8); + ProcessBlock(_buffer, 0); + + // Produce digest (first 256 bits = first 4 H words) + byte[] output = new byte[DigestLength]; + for (int i = 0; i < 4; i++) + { + WriteULongBE(_H[i], output, i * 8); + } + + return output; + } + + /// + /// Adds the specified number of bits to the total bit count. + /// + /// The number of bits to add. + private void AddLength(ulong bits) + { + _bitCountLow += bits; + if (_bitCountLow < bits) + _bitCountHigh++; + } + + /// + /// Writes a 64-bit unsigned integer to a byte array in big-endian format. + /// + /// The value to write. + /// The buffer to write to. + /// The offset in the buffer to start writing at. + private static void WriteULongBE(ulong value, byte[] buffer, int offset) + { + for (int i = 7; i >= 0; i--) + buffer[offset + 7 - i] = (byte)(value >> (i * 8)); + } + + /// + /// Processes a single 1024-bit block of data. + /// + /// The block of data to process. + /// The offset in the block to start processing at. + private void ProcessBlock(byte[] block, int offset) + { + for (int i = 0; i < 16; i++) + { + _W[i] = + ((ulong)block[offset + i * 8 + 0] << 56) | + ((ulong)block[offset + i * 8 + 1] << 48) | + ((ulong)block[offset + i * 8 + 2] << 40) | + ((ulong)block[offset + i * 8 + 3] << 32) | + ((ulong)block[offset + i * 8 + 4] << 24) | + ((ulong)block[offset + i * 8 + 5] << 16) | + ((ulong)block[offset + i * 8 + 6] << 8) | + ((ulong)block[offset + i * 8 + 7]); + } + + for (int i = 16; i < 80; i++) + { + ulong s0 = RotateRight(_W[i - 15], 1) ^ RotateRight(_W[i - 15], 8) ^ (_W[i - 15] >> 7); + ulong s1 = RotateRight(_W[i - 2], 19) ^ RotateRight(_W[i - 2], 61) ^ (_W[i - 2] >> 6); + _W[i] = _W[i - 16] + s0 + _W[i - 7] + s1; + } + + ulong a = _H[0], b = _H[1], c = _H[2], d = _H[3]; + ulong e = _H[4], f = _H[5], g = _H[6], h = _H[7]; + + for (int i = 0; i < 80; i++) + { + ulong S1 = RotateRight(e, 14) ^ RotateRight(e, 18) ^ RotateRight(e, 41); + ulong ch = (e & f) ^ (~e & g); + ulong temp1 = h + S1 + ch + K[i] + _W[i]; + ulong S0 = RotateRight(a, 28) ^ RotateRight(a, 34) ^ RotateRight(a, 39); + ulong maj = (a & b) ^ (a & c) ^ (b & c); + ulong temp2 = S0 + maj; + + h = g; + g = f; + f = e; + e = d + temp1; + d = c; + c = b; + b = a; + a = temp1 + temp2; + } + + _H[0] += a; _H[1] += b; _H[2] += c; _H[3] += d; + _H[4] += e; _H[5] += f; _H[6] += g; _H[7] += h; + } + + /// + /// Rotates the bits of a 64-bit unsigned integer to the right. + /// + /// The value to rotate. + /// The number of bits to rotate. + /// The rotated value. + private static ulong RotateRight(ulong x, int n) => (x >> n) | (x << (64 - n)); + } +} diff --git a/src/Cuemon.Security.Cryptography/SecureHashAlgorithm512256.cs b/src/Cuemon.Security.Cryptography/SecureHashAlgorithm512256.cs new file mode 100644 index 000000000..836ccb351 --- /dev/null +++ b/src/Cuemon.Security.Cryptography/SecureHashAlgorithm512256.cs @@ -0,0 +1,26 @@ +using System; + +namespace Cuemon.Security.Cryptography +{ + /// + /// Provides a SHA-512-256 implementation of the SHA (Secure Hash Algorithm) cryptographic hashing algorithm for 512-bit hash values. This class cannot be inherited. + /// Implements the + /// + /// + /// + public sealed class SecureHashAlgorithm512256 : UnkeyedCryptoHash + { + /// + /// Produces a 256-bit hash value + /// + public const int BitSize = 256; + + /// + /// Initializes a new instance of the class. + /// + /// The which may be configured. + public SecureHashAlgorithm512256(Action setup) : base(() => new SHA512256(), setup) + { + } + } +} diff --git a/src/Cuemon.Security.Cryptography/UnkeyedCryptoAlgorithm.cs b/src/Cuemon.Security.Cryptography/UnkeyedCryptoAlgorithm.cs index 994a9048e..3c72fc713 100644 --- a/src/Cuemon.Security.Cryptography/UnkeyedCryptoAlgorithm.cs +++ b/src/Cuemon.Security.Cryptography/UnkeyedCryptoAlgorithm.cs @@ -24,6 +24,10 @@ public enum UnkeyedCryptoAlgorithm /// /// The Secure Hashing Algorithm (SHA512) algorithm (512 bits). /// - Sha512 = 2 + Sha512 = 2, + /// + /// The Secure Hashing Algorithm (SHA512/256) algorithm (256 bits). + /// + Sha512Slash256 = 3 } -} \ No newline at end of file +} diff --git a/src/Cuemon.Security.Cryptography/UnkeyedHashFactory.cs b/src/Cuemon.Security.Cryptography/UnkeyedHashFactory.cs index 48a88ece2..9c3855a6b 100644 --- a/src/Cuemon.Security.Cryptography/UnkeyedHashFactory.cs +++ b/src/Cuemon.Security.Cryptography/UnkeyedHashFactory.cs @@ -25,11 +25,23 @@ public static Hash CreateCrypto(UnkeyedCryptoAlgorithm algorithm = default, Acti return CreateCryptoSha384(setup); case UnkeyedCryptoAlgorithm.Sha512: return CreateCryptoSha512(setup); + case UnkeyedCryptoAlgorithm.Sha512Slash256: + return CreateCryptoSha512Slash256(setup); default: return CreateCryptoSha256(setup); } } + /// + /// Creates an instance of . + /// + /// The which may be configured. + /// A implementation of . + public static Hash CreateCryptoSha512Slash256(Action setup = null) + { + return new SecureHashAlgorithm512256(setup); + } + /// /// Creates an instance of . /// diff --git a/test/Cuemon.AspNetCore.Authentication.Tests/Digest/DigestAccessAuthenticationHandlerTest.cs b/test/Cuemon.AspNetCore.Authentication.Tests/Digest/DigestAccessAuthenticationHandlerTest.cs index c71af5f41..ab50fba14 100644 --- a/test/Cuemon.AspNetCore.Authentication.Tests/Digest/DigestAccessAuthenticationHandlerTest.cs +++ b/test/Cuemon.AspNetCore.Authentication.Tests/Digest/DigestAccessAuthenticationHandlerTest.cs @@ -25,8 +25,14 @@ public DigestAccessAuthenticationHandlerTest(ITestOutputHelper output) : base(ou { } - [Fact] - public async Task HandleAuthenticateAsync_ShouldReturnContent_WithQopAuthentication() + [Theory] + [InlineData(DigestCryptoAlgorithm.Sha256)] + [InlineData(DigestCryptoAlgorithm.Sha256Session)] + [InlineData(DigestCryptoAlgorithm.Sha512Slash256)] + [InlineData(DigestCryptoAlgorithm.Sha512Slash256Session)] + [InlineData(DigestCryptoAlgorithm.Md5)] + [InlineData(DigestCryptoAlgorithm.Md5Session)] + public async Task HandleAuthenticateAsync_ShouldReturnContent_WithQopAuthentication(DigestCryptoAlgorithm algorithm) { using (var webApp = WebHostTestFactory.Create(services => { @@ -49,6 +55,7 @@ public async Task HandleAuthenticateAsync_ShouldReturnContent_WithQopAuthenticat return null; }; o.RequireSecureConnection = false; + o.DigestAlgorithm = algorithm; }); }, app => { @@ -72,7 +79,7 @@ public async Task HandleAuthenticateAsync_ShouldReturnContent_WithQopAuthenticat TestOutput.WriteLine("WWW-Authenticate:"); TestOutput.WriteLine(wwwAuthenticate); - var db = new DigestAuthorizationHeaderBuilder(options.Algorithm) + var db = new DigestAuthorizationHeaderBuilder(options.DigestAlgorithm) .AddRealm(options.Realm) .AddUserName("Agent") .AddUri("/") @@ -101,8 +108,14 @@ public async Task HandleAuthenticateAsync_ShouldReturnContent_WithQopAuthenticat } } - [Fact] - public async Task HandleAuthenticateAsync_ShouldReturnContent_QopAuthenticationIntegrity() + [Theory] + [InlineData(DigestCryptoAlgorithm.Sha256)] + [InlineData(DigestCryptoAlgorithm.Sha256Session)] + [InlineData(DigestCryptoAlgorithm.Sha512Slash256)] + [InlineData(DigestCryptoAlgorithm.Sha512Slash256Session)] + [InlineData(DigestCryptoAlgorithm.Md5)] + [InlineData(DigestCryptoAlgorithm.Md5Session)] + public async Task HandleAuthenticateAsync_ShouldReturnContent_QopAuthenticationIntegrity(DigestCryptoAlgorithm algorithm) { using (var webApp = WebHostTestFactory.Create(services => { @@ -125,6 +138,7 @@ public async Task HandleAuthenticateAsync_ShouldReturnContent_QopAuthenticationI return null; }; o.RequireSecureConnection = false; + o.DigestAlgorithm = algorithm; }); }, app => { @@ -148,7 +162,7 @@ public async Task HandleAuthenticateAsync_ShouldReturnContent_QopAuthenticationI TestOutput.WriteLine("WWW-Authenticate:"); TestOutput.WriteLine(wwwAuthenticate); - var db = new DigestAuthorizationHeaderBuilder(options.Algorithm) + var db = new DigestAuthorizationHeaderBuilder(options.DigestAlgorithm) .AddRealm(options.Realm) .AddUserName("Agent") .AddUri("/") @@ -189,13 +203,13 @@ public async Task HandleAuthenticateAsync_ShouldReturnContent_WithServerSideHa1S .AddAuthentication(DigestAuthorizationHeader.Scheme) .AddDigestAccess(o => { - o.Algorithm = UnkeyedCryptoAlgorithm.Sha512; + o.DigestAlgorithm = DigestCryptoAlgorithm.Sha512Slash256; o.UseServerSideHa1Storage = true; o.Authenticator = (string username, out string password) => { if (username == "Agent") { - password = "64d7c739de5dc6b5149de600751c413ef74fab0419e5a656e9f5ead5b98105b8c75ba6e18850ccd7ef2a3a13517520e158181bd34a4b68e2ab3b728acd7d066b"; + password = "7a0adced41ceeaf77c95a4bb382a80303536fd3ee166a3a67a2dc9c100a9d7be"; var cp = new ClaimsPrincipal(); cp.AddIdentity(new ClaimsIdentity(Arguments.Yield(new Claim("Name", "Test Agent")), DigestAuthorizationHeader.Scheme)); return cp; @@ -227,7 +241,7 @@ public async Task HandleAuthenticateAsync_ShouldReturnContent_WithServerSideHa1S TestOutput.WriteLine("WWW-Authenticate:"); TestOutput.WriteLine(wwwAuthenticate); - var db = new DigestAuthorizationHeaderBuilder(options.Algorithm) + var db = new DigestAuthorizationHeaderBuilder(options.DigestAlgorithm) .AddRealm(options.Realm) .AddUserName("Agent") .AddUri("/") @@ -253,7 +267,7 @@ public async Task HandleAuthenticateAsync_ShouldReturnContent_WithServerSideHa1S result = await client.GetAsync("/fake"); - Assert.Equal(UnkeyedCryptoAlgorithm.Sha512, options.Algorithm); + Assert.Equal(DigestCryptoAlgorithm.Sha512Slash256, options.DigestAlgorithm); Assert.True(options.UseServerSideHa1Storage); Assert.False(options.RequireSecureConnection); diff --git a/test/Cuemon.AspNetCore.Authentication.Tests/Digest/DigestAccessAuthenticationMiddlewareTest.cs b/test/Cuemon.AspNetCore.Authentication.Tests/Digest/DigestAccessAuthenticationMiddlewareTest.cs index f9bf7b1b9..1190fb80c 100644 --- a/test/Cuemon.AspNetCore.Authentication.Tests/Digest/DigestAccessAuthenticationMiddlewareTest.cs +++ b/test/Cuemon.AspNetCore.Authentication.Tests/Digest/DigestAccessAuthenticationMiddlewareTest.cs @@ -117,7 +117,7 @@ public async Task InvokeAsync_ShouldAuthenticateWhenApplyingAuthorizationHeader( TestOutput.WriteLine(wwwAuthenticate); - var db = new DigestAuthorizationHeaderBuilder(options.Value.Algorithm) + var db = new DigestAuthorizationHeaderBuilder(options.Value.DigestAlgorithm) .AddRealm(options.Value.Realm) .AddUserName("Agent") .AddUri("/") @@ -193,7 +193,7 @@ public async Task InvokeAsync_ShouldAuthenticateWhenApplyingAuthorizationHeaderN TestOutput.WriteLine(wwwAuthenticate); - var db = new DigestAuthorizationHeaderBuilder(options.Value.Algorithm) + var db = new DigestAuthorizationHeaderBuilder(options.Value.DigestAlgorithm) .AddRealm(options.Value.Realm) .AddUserName("Agent") .AddUri("/") @@ -268,7 +268,7 @@ public async Task InvokeAsync_ShouldAuthenticateWhenApplyingAuthorizationHeaderW TestOutput.WriteLine(wwwAuthenticate); - var db = new DigestAuthorizationHeaderBuilder(options.Value.Algorithm) + var db = new DigestAuthorizationHeaderBuilder(options.Value.DigestAlgorithm) .AddRealm(options.Value.Realm) .AddUserName("Agent") .AddUri("/") diff --git a/test/Cuemon.AspNetCore.Authentication.Tests/DigestAuthenticationOptionsTest.cs b/test/Cuemon.AspNetCore.Authentication.Tests/DigestAuthenticationOptionsTest.cs index c1a58f67c..0b6ae5ffd 100644 --- a/test/Cuemon.AspNetCore.Authentication.Tests/DigestAuthenticationOptionsTest.cs +++ b/test/Cuemon.AspNetCore.Authentication.Tests/DigestAuthenticationOptionsTest.cs @@ -179,7 +179,7 @@ public void DigestAuthenticationOptions_ShouldHaveDefaultValues() { var sut = new DigestAuthenticationOptions(); - Assert.Equal(UnkeyedCryptoAlgorithm.Sha256, sut.Algorithm); + Assert.Equal(DigestCryptoAlgorithm.Sha256, sut.DigestAlgorithm); Assert.NotNull(sut.OpaqueGenerator); Assert.NotNull(sut.NonceExpiredParser); Assert.NotNull(sut.NonceGenerator); diff --git a/test/Cuemon.Extensions.AspNetCore.Authentication.Tests/AuthorizationResponseHandlerTest.cs b/test/Cuemon.Extensions.AspNetCore.Authentication.Tests/AuthorizationResponseHandlerTest.cs index 07e76299e..8cf7a34af 100644 --- a/test/Cuemon.Extensions.AspNetCore.Authentication.Tests/AuthorizationResponseHandlerTest.cs +++ b/test/Cuemon.Extensions.AspNetCore.Authentication.Tests/AuthorizationResponseHandlerTest.cs @@ -546,7 +546,7 @@ public async Task AuthorizationResponseHandler_DigestScheme_ShouldAuthorizeWithT var result = await client.GetAsync("/"); - var db = new DigestAuthorizationHeaderBuilder(options.Algorithm) + var db = new DigestAuthorizationHeaderBuilder(options.DigestAlgorithm) .AddRealm(options.Realm) .AddUserName("Agent") .AddUri("/") @@ -594,7 +594,7 @@ public async Task AuthorizationResponseHandler_DigestScheme_ShouldAuthorizeWithT password = null; return null; }; - o.Algorithm = UnkeyedCryptoAlgorithm.Sha512; + o.DigestAlgorithm = DigestCryptoAlgorithm.Sha512Slash256; }); services.AddAuthorization(o => { @@ -623,7 +623,7 @@ public async Task AuthorizationResponseHandler_DigestScheme_ShouldAuthorizeWithT var result = await client.GetAsync("/"); - var db = new DigestAuthorizationHeaderBuilder(options.Algorithm) + var db = new DigestAuthorizationHeaderBuilder(options.DigestAlgorithm) .AddRealm(options.Realm) .AddUserName("Agent") .AddUri("/") @@ -702,7 +702,7 @@ public async Task AuthorizationResponseHandler_DigestScheme_ShouldRenderResponse var result = await client.GetAsync("/"); var options = startup.Host.Services.GetRequiredScopedService>().Get(DigestAuthorizationHeader.Scheme); - var db = new DigestAuthorizationHeaderBuilder(options.Algorithm) + var db = new DigestAuthorizationHeaderBuilder(options.DigestAlgorithm) .AddRealm(options.Realm) .AddUserName("Agent") .AddUri("/") diff --git a/test/Cuemon.Security.Cryptography.Tests/UnkeyedHashFactoryTest.cs b/test/Cuemon.Security.Cryptography.Tests/UnkeyedHashFactoryTest.cs index aa72f9f5a..ac8a7c532 100644 --- a/test/Cuemon.Security.Cryptography.Tests/UnkeyedHashFactoryTest.cs +++ b/test/Cuemon.Security.Cryptography.Tests/UnkeyedHashFactoryTest.cs @@ -11,6 +11,16 @@ public UnkeyedHashFactoryTest(ITestOutputHelper output) : base(output) { } + [Fact] + public void CreateCryptoSha512256_ShouldBeValidHashResult() + { + var h = UnkeyedHashFactory.CreateCryptoSha512Slash256(); + Assert.Equal("cdf1cc0effe26ecc0c13758f7b4a48e000615df241284185c39eb05d355bb9c8", h.ComputeHash(Alphanumeric.LettersAndNumbers).ToHexadecimalString()); + Assert.Equal("d48b2aa4a50d1c3e324a1a762d3b2165244661ef80e004dd3669a77e02c489d8", h.ComputeHash(Alphanumeric.Numbers).ToHexadecimalString()); + Assert.Equal("e41c9660b04714cdf7249f0fd6e6c5556f54a7e04d299958b69a877e0fada2fb", h.ComputeHash(Guid.Empty.ToByteArray()).ToHexadecimalString()); + Assert.Equal("0ac561fac838104e3f2e4ad107b4bee3e938bf15f2b15f009ccccd61a913f017", h.ComputeHash("hello world").ToHexadecimalString()); + } + [Fact] public void CreateCryptoSha512_ShouldBeValidHashResult() {