Clean up some usage of SHA1 and SHA256 in the code base#24696
Clean up some usage of SHA1 and SHA256 in the code base#24696pranavkm merged 6 commits intodotnet:masterfrom
Conversation
|
Note to reviewers: I recommend hiding whitespace changes. Removing some using blocks affected indentation. |
| var subHash = hash.Take(8).ToArray(); | ||
| return WebEncoders.Base64UrlEncode(subHash); | ||
| } | ||
| byte[] fullHash = SHA256.HashData(Encoding.UTF8.GetBytes(applicationId)); |
There was a problem hiding this comment.
Since you're here, may as well improve the allocations here.
var source = Encoding.UTF8.GetBytes(applicationId);
Span<byte> target = stackalloc byte[32];
SHA256.HashData(source, target);
WebEncoders.Base64UrlEncode(target.Slice(0, 8));There was a problem hiding this comment.
This code is called exactly once at app start. Wasn't worth complicating the code for so little gain.
| throw new InvalidOperationException(); | ||
| } | ||
|
|
||
| var bytes = SHA256.HashData(buffer); |
There was a problem hiding this comment.
Yes. ArraySegment<byte> is implicitly convertible to ROS<byte>.
|
|
||
| return Convert.ToBase64String(hashedBytes); | ||
| Span<byte> hashedBytes = stackalloc byte[20]; | ||
| var written = SHA1.HashData(mergedBytes, hashedBytes); |
There was a problem hiding this comment.
Does SHA1.HashData allocate any objects or is it truly static?
There was a problem hiding this comment.
I believe it only allocates on Win7 & Win8. But it's still faster than alternatives: see dotnet/runtime#40510.
There was a problem hiding this comment.
I care more about linux? Is it allocating? This change had a measurable impact on websocket upgrades when it was made. I want to make sure this isn't regressing that.
There was a problem hiding this comment.
@BrennanConroy did you commit those performance tests?
There was a problem hiding this comment.
There was a problem hiding this comment.
Last I checked Linux was neither Win7 nor Win8, so it should be non-allocating. :)
Here's the Linux implementation, for reference:
https://github.com/dotnet/runtime/blob/46acbaa5f13b501dc2e750bc2023d4a30eded9c7/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/HashProviderDispenser.Unix.cs#L43-L62
There was a problem hiding this comment.
I don't have a suitable environment for running the perf tests, sorry. If you have a shared environment where I can do so, along with clear instructions for it (copy + paste these exact commands), I can give it a spin.
There was a problem hiding this comment.
Unrelated to this PR, if IsRequestKeyValid is a hot method I think you can get significant perf gains in it from rolling your own logic. The framework method you're calling performs base64 decoding, but you don't actually need to decode it. You only care that it is valid base64 and that it decodes to 16 bytes. So a rough outline of the logic would be:
- Validate that the incoming string length is exactly 24 chars.
- Perform vectorized logic to validate that the first 22 chars of the string are within
[a-zA-Z0-9+/]. (Your vectorized logic will consume 8 or 16 chars at a time, depending on the current OS.) - Validate that the last 2 chars are both
'='.
If any of these checks fail, you can fall back to the framework's Convert.TryFromBsae64String string for extra validation if you need to handle things like whitespace chars.
There was a problem hiding this comment.
Linux
Before:
| Method | Mean | Error | StdDev | Op/s | Gen 0 | Gen 1 | Gen 2 | Allocated |
|---|---|---|---|---|---|---|---|---|
| CreateResponseKey | 2,292.6 ns | 22.81 ns | 21.33 ns | 436,191.8 | 0.0031 | - | - | 200 B |
| IsRequestKeyValid | 143.3 ns | 1.13 ns | 1.00 ns | 6,979,003.0 | - | - | - | - |
After:
| Method | Mean | Error | StdDev | Op/s | Gen 0 | Gen 1 | Gen 2 | Allocated |
|---|---|---|---|---|---|---|---|---|
| CreateResponseKey | 1,406.8 ns | 12.49 ns | 11.07 ns | 710,842.7 | 0.0015 | - | - | 80 B |
| IsRequestKeyValid | 147.2 ns | 1.12 ns | 0.99 ns | 6,792,335.8 | - | - | - | - |
There was a problem hiding this comment.
if IsRequestKeyValid is a hot method I think you can get significant perf gains in it from rolling your own logic
We had a try of this in #13196
| private static string GetHashForFile(IFileInfo fileInfo) | ||
| { | ||
| using (var sha256 = CryptographyAlgorithms.CreateSHA256()) | ||
| using (var sha256 = SHA256.Create()) |
There was a problem hiding this comment.
Why isn't this one using the static method?
There was a problem hiding this comment.
It's hashing a stream and the one-shot doesn't have a stream overload, only byte[] and ReadOnlySpan<byte>.
|
Thanks @GrabYourPitchforks |
Two main changes:
Prefer static
SHA256.HashDatamethod where applicable, as it's going to use the most optimal implementation available on the current platform. It also avoids allocating temporarySHA256instances in cases where you're going to hash a single buffer.Remove references to
*CryptoServiceProviderand friends. Use the standard factories likeSHA256.Create. This also reduces slightly the total number of types referenced by the application, which could help with linker trimming scenarios.I did not change any projects which targeted netstandard2.0 or any other pre-5.0 target framework.