Skip to content
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

Merged
merged 6 commits into from
Aug 10, 2020

Conversation

GrabYourPitchforks
Copy link
Member

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 temporary SHA256 instances in cases where you're going to hash a single buffer.

  • Remove references to *CryptoServiceProvider and friends. Use the standard factories like SHA256.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.

@GrabYourPitchforks
Copy link
Member Author

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));
Copy link
Member

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));

Copy link
Member Author

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's fair

Copy link
Member

@davidfowl davidfowl left a 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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implicit span conversion?

Copy link
Member Author

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);
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member

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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

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.

Copy link
Member Author

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:

  1. Validate that the incoming string length is exactly 24 chars.
  2. 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.)
  3. 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.

Copy link
Member

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 - - - -

Copy link
Member

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())
Copy link
Member

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?

Copy link
Member

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>.

@Pilchie Pilchie added the area-auth Includes: Authn, Authz, OAuth, OIDC, Bearer label Aug 10, 2020
@BrennanConroy BrennanConroy added this to the 5.0.0-rc1 milestone Aug 10, 2020
@pranavkm pranavkm merged commit bbf7c87 into dotnet:master Aug 10, 2020
@pranavkm
Copy link
Contributor

Thanks @GrabYourPitchforks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-auth Includes: Authn, Authz, OAuth, OIDC, Bearer
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants