-
Notifications
You must be signed in to change notification settings - Fork 10.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Clean up some usage of SHA1 and SHA256 in the code base #24696
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code is called exactly once at app start. Wasn't worth complicating the code for so little gain.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that's fair
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, small nits.
throw new InvalidOperationException(); | ||
} | ||
|
||
var bytes = SHA256.HashData(buffer); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implicit span conversion?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does SHA1.HashData
allocate any objects or is it truly static?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@BrennanConroy did you commit those performance tests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
@@ -97,7 +98,7 @@ public string AddFileVersionToPath(PathString requestPathBase, string path) | |||
|
|||
private static string GetHashForFile(IFileInfo fileInfo) | |||
{ | |||
using (var sha256 = CryptographyAlgorithms.CreateSHA256()) | |||
using (var sha256 = SHA256.Create()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why isn't this one using the static method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.HashData
method where applicable, as it's going to use the most optimal implementation available on the current platform. It also avoids allocating temporarySHA256
instances in cases where you're going to hash a single buffer.Remove references to
*CryptoServiceProvider
and 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.