Conversation
…length fix: CA1305
…gardless (just hiding the complexity) feat: move full span compatible code into a separate file as its a pain to flip back and forth feat: set Char64 MethodImplOptions.AggressiveInlining
feat: sort benchmarks
chore: swap out NET8_0_OR_GREATER
chore: drop fwk from benchmark run
feat: EncodeBase64 stackalloc and return via `Buffer.Memmove(ref MemoryMarshal.GetArrayDataReference(destination), ref _reference, (uint)_length);` alloc (toarray > span)
vs alloc to heap and return span; added benchmark
…ements) Clone BCryptExtendedV2 to BCryptExtendedV2.Std as we can't use the newer span wiring
* Add experimental support for an extension to allow easy SecureString usage; I don't agree with the usage but its still a thing in UI apps * Net5+ BCryptSafeString tries to maintain as much of the call in memory as possible using newer features * Net Standard 2 version makes an attempt * Add tests; automatically covers both * Tidy SecureEquals; no need for null check on ReadOnlySpan as it returns default; add variable for length. This is correct vs the .net version which uses SUB instead of XOR * Zero out input bytes when we're done with them as its our buffer. * Change fwk to use EnhancedHashDelegate instead of func; keeps the definition in one place * Update SonarAnalyzer * Add beginning of doc
…re if temporarily or permanently
…ow me to simplify the existing benchmarks project
Split out functionality from base into core as it makes my life easier working out which junk is still needed in netfwk standard and core and will also make deleting things like fwk easier later on
``` BenchmarkDotNet v0.15.6, Windows 11 (10.0.26100.6899/24H2/2024Update/HudsonValley) AMD Ryzen 9 9900X 4.40GHz, 1 CPU, 24 logical and 12 physical cores .NET SDK 10.0.100 [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4 Job-XJDVJN : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4 Job-QQRERM : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4 Job-TDICCM : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4 Job-DYUUZC : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4 Job-JIERNF : .NET 8.0.22 (8.0.22, 8.0.2225.52707), X64 RyuJIT x86-64-v4 Job-QBQHGG : .NET 8.0.22 (8.0.22, 8.0.2225.52707), X64 RyuJIT x86-64-v4 Job-NOZKUG : .NET 8.0.22 (8.0.22, 8.0.2225.52707), X64 RyuJIT x86-64-v4 Job-RDWRBK : .NET 8.0.22 (8.0.22, 8.0.2225.52707), X64 RyuJIT x86-64-v4 Job-RDXXZX : .NET Framework 4.8.1 (4.8.9310.0), X64 RyuJIT VectorSize=256 Job-RLXPNS : .NET Framework 4.8.1 (4.8.9310.0), X64 RyuJIT VectorSize=256 Job-XEJGGT : .NET Framework 4.8.1 (4.8.9310.0), X64 RyuJIT VectorSize=256 Job-URTIUT : .NET Framework 4.8.1 (4.8.9310.0), X64 RyuJIT VectorSize=256 Job-TUHZZR : .NET Framework 4.8.1 (4.8.9310.0), X64 RyuJIT VectorSize=256 Job-BXPPFA : .NET Framework 4.8.1 (4.8.9310.0), X64 RyuJIT VectorSize=256 Job-BQPXRF : .NET Framework 4.8.1 (4.8.9310.0), X64 RyuJIT VectorSize=256 Job-RIVHIS : .NET Framework 4.8.1 (4.8.9310.0), X64 RyuJIT VectorSize=256 Job-OVEKXS : .NET Framework 4.8.1 (4.8.9310.0), X64 RyuJIT VectorSize=256 Job-ULWAAK : .NET Framework 4.8.1 (4.8.9310.0), X64 RyuJIT VectorSize=256 Server=True ``` | Method | Runtime | Arguments | key | salt | hash | Mean | Error | StdDev | Rank | Allocated | |----------------- |--------------------- |---------------------------------------------- |--------------------- |--------------------- |--------------------- |----------:|---------:|---------:|-----:|----------:| | **TestHashValidate** | **.NET 10.0** | **/p:BCryptVersion=2.1.4** | **~!@#$(...)NBFRD [34]** | **$2a$1(...)rOvHe [29]** | **$2a$1(...)JYlfS [60]** | **40.43 ms** | **0.689 ms** | **0.644 ms** | **1** | **69.76 KB** | | TestHashValidate | .NET 10.0 | /p:BCryptVersion=3.5.0 | ~!@#$(...)NBFRD [34] | $2a$1(...)rOvHe [29] | $2a$1(...)JYlfS [60] | 40.40 ms | 0.396 ms | 0.370 ms | 1 | 5.01 KB | | TestHashValidate | .NET 10.0 | /p:BCryptVersion=4.0.3 | ~!@#$(...)NBFRD [34] | $2a$1(...)rOvHe [29] | $2a$1(...)JYlfS [60] | 40.64 ms | 0.620 ms | 0.580 ms | 1 | 5.01 KB | | TestHashValidate | .NET 10.0 | /p:BCryptVersion=5.0.0-prerelease.g5300270033 | ~!@#$(...)NBFRD [34] | $2a$1(...)rOvHe [29] | $2a$1(...)JYlfS [60] | 40.15 ms | 0.278 ms | 0.260 ms | 1 | 4.45 KB | | TestHashValidate | .NET 8.0 | /p:BCryptVersion=2.1.4 | ~!@#$(...)NBFRD [34] | $2a$1(...)rOvHe [29] | $2a$1(...)JYlfS [60] | 39.95 ms | 0.228 ms | 0.213 ms | 1 | 69.76 KB | | TestHashValidate | .NET 8.0 | /p:BCryptVersion=3.5.0 | ~!@#$(...)NBFRD [34] | $2a$1(...)rOvHe [29] | $2a$1(...)JYlfS [60] | 40.42 ms | 0.471 ms | 0.441 ms | 1 | 5.01 KB | | TestHashValidate | .NET 8.0 | /p:BCryptVersion=4.0.3 | ~!@#$(...)NBFRD [34] | $2a$1(...)rOvHe [29] | $2a$1(...)JYlfS [60] | 40.50 ms | 0.432 ms | 0.404 ms | 1 | 5.01 KB | | TestHashValidate | .NET 8.0 | /p:BCryptVersion=5.0.0-prerelease.g5300270033 | ~!@#$(...)NBFRD [34] | $2a$1(...)rOvHe [29] | $2a$1(...)JYlfS [60] | 40.19 ms | 0.292 ms | 0.273 ms | 1 | 4.45 KB | | TestHashValidate | .NET Framework 4.6.2 | /p:BCryptVersion=2.0.0 | ~!@#$(...)NBFRD [34] | $2a$1(...)rOvHe [29] | $2a$1(...)JYlfS [60] | 42.33 ms | 0.481 ms | 0.427 ms | 1 | 70 KB | | TestHashValidate | .NET Framework 4.6.2 | /p:BCryptVersion=2.1.4 | ~!@#$(...)NBFRD [34] | $2a$1(...)rOvHe [29] | $2a$1(...)JYlfS [60] | 42.26 ms | 0.336 ms | 0.297 ms | 1 | 69.66 KB | | TestHashValidate | .NET Framework 4.6.2 | /p:BCryptVersion=3.5.0 | ~!@#$(...)NBFRD [34] | $2a$1(...)rOvHe [29] | $2a$1(...)JYlfS [60] | 42.39 ms | 0.428 ms | 0.401 ms | 1 | 69.33 KB | | TestHashValidate | .NET Framework 4.6.2 | /p:BCryptVersion=4.0.3 | ~!@#$(...)NBFRD [34] | $2a$1(...)rOvHe [29] | $2a$1(...)JYlfS [60] | 42.50 ms | 0.373 ms | 0.349 ms | 1 | 69.33 KB | | TestHashValidate | .NET Framework 4.6.2 | /p:BCryptVersion=5.0.0-prerelease.g5300270033 | ~!@#$(...)NBFRD [34] | $2a$1(...)rOvHe [29] | $2a$1(...)JYlfS [60] | 42.56 ms | 0.557 ms | 0.521 ms | 1 | 68.99 KB | | TestHashValidate | .NET Framework 4.8.1 | /p:BCryptVersion=2.0.0 | ~!@#$(...)NBFRD [34] | $2a$1(...)rOvHe [29] | $2a$1(...)JYlfS [60] | 43.54 ms | 0.845 ms | 0.940 ms | 1 | 70 KB | | TestHashValidate | .NET Framework 4.8.1 | /p:BCryptVersion=2.1.4 | ~!@#$(...)NBFRD [34] | $2a$1(...)rOvHe [29] | $2a$1(...)JYlfS [60] | 44.21 ms | 0.512 ms | 0.478 ms | 1 | 70 KB | | TestHashValidate | .NET Framework 4.8.1 | /p:BCryptVersion=3.5.0 | ~!@#$(...)NBFRD [34] | $2a$1(...)rOvHe [29] | $2a$1(...)JYlfS [60] | 44.64 ms | 0.865 ms | 0.997 ms | 1 | 4.67 KB | | TestHashValidate | .NET Framework 4.8.1 | /p:BCryptVersion=4.0.3 | ~!@#$(...)NBFRD [34] | $2a$1(...)rOvHe [29] | $2a$1(...)JYlfS [60] | 42.64 ms | 0.314 ms | 0.293 ms | 1 | 4.67 KB | | TestHashValidate | .NET Framework 4.8.1 | /p:BCryptVersion=5.0.0-prerelease.g5300270033 | ~!@#$(...)NBFRD [34] | $2a$1(...)rOvHe [29] | $2a$1(...)JYlfS [60] | 42.59 ms | 0.292 ms | 0.259 ms | 1 | 4.67 KB | | **TestHashValidate** | **.NET 10.0** | **/p:BCryptVersion=2.1.4** | **~!@#$(...)NBFRD [34]** | **$2a$1(...)nkrPO [29]** | **$2a$1(...)eyhgC [60]** | **160.46 ms** | **1.088 ms** | **0.964 ms** | **2** | **261.76 KB** | | TestHashValidate | .NET 10.0 | /p:BCryptVersion=3.5.0 | ~!@#$(...)NBFRD [34] | $2a$1(...)nkrPO [29] | $2a$1(...)eyhgC [60] | 160.22 ms | 0.962 ms | 0.900 ms | 2 | 5.01 KB | | TestHashValidate | .NET 10.0 | /p:BCryptVersion=4.0.3 | ~!@#$(...)NBFRD [34] | $2a$1(...)nkrPO [29] | $2a$1(...)eyhgC [60] | 160.37 ms | 0.645 ms | 0.572 ms | 2 | 5.01 KB | | TestHashValidate | .NET 10.0 | /p:BCryptVersion=5.0.0-prerelease.g5300270033 | ~!@#$(...)NBFRD [34] | $2a$1(...)nkrPO [29] | $2a$1(...)eyhgC [60] | 160.95 ms | 1.437 ms | 1.274 ms | 2 | 4.45 KB | | TestHashValidate | .NET 8.0 | /p:BCryptVersion=2.1.4 | ~!@#$(...)NBFRD [34] | $2a$1(...)nkrPO [29] | $2a$1(...)eyhgC [60] | 162.36 ms | 3.214 ms | 3.439 ms | 2 | 261.76 KB | | TestHashValidate | .NET 8.0 | /p:BCryptVersion=3.5.0 | ~!@#$(...)NBFRD [34] | $2a$1(...)nkrPO [29] | $2a$1(...)eyhgC [60] | 160.75 ms | 0.941 ms | 0.786 ms | 2 | 5.01 KB | | TestHashValidate | .NET 8.0 | /p:BCryptVersion=4.0.3 | ~!@#$(...)NBFRD [34] | $2a$1(...)nkrPO [29] | $2a$1(...)eyhgC [60] | 160.83 ms | 1.753 ms | 1.554 ms | 2 | 5.01 KB | | TestHashValidate | .NET 8.0 | /p:BCryptVersion=5.0.0-prerelease.g5300270033 | ~!@#$(...)NBFRD [34] | $2a$1(...)nkrPO [29] | $2a$1(...)eyhgC [60] | 160.47 ms | 1.143 ms | 0.955 ms | 2 | 4.45 KB | | TestHashValidate | .NET Framework 4.6.2 | /p:BCryptVersion=2.0.0 | ~!@#$(...)NBFRD [34] | $2a$1(...)nkrPO [29] | $2a$1(...)eyhgC [60] | 170.28 ms | 2.024 ms | 1.893 ms | 2 | 261.33 KB | | TestHashValidate | .NET Framework 4.6.2 | /p:BCryptVersion=2.1.4 | ~!@#$(...)NBFRD [34] | $2a$1(...)nkrPO [29] | $2a$1(...)eyhgC [60] | 169.68 ms | 1.650 ms | 1.543 ms | 2 | 261.33 KB | | TestHashValidate | .NET Framework 4.6.2 | /p:BCryptVersion=3.5.0 | ~!@#$(...)NBFRD [34] | $2a$1(...)nkrPO [29] | $2a$1(...)eyhgC [60] | 168.60 ms | 1.189 ms | 1.112 ms | 2 | 262.21 KB | | TestHashValidate | .NET Framework 4.6.2 | /p:BCryptVersion=4.0.3 | ~!@#$(...)NBFRD [34] | $2a$1(...)nkrPO [29] | $2a$1(...)eyhgC [60] | 169.15 ms | 1.267 ms | 1.185 ms | 2 | 261.33 KB | | TestHashValidate | .NET Framework 4.6.2 | /p:BCryptVersion=5.0.0-prerelease.g5300270033 | ~!@#$(...)NBFRD [34] | $2a$1(...)nkrPO [29] | $2a$1(...)eyhgC [60] | 169.73 ms | 2.048 ms | 1.916 ms | 2 | 261.33 KB | | TestHashValidate | .NET Framework 4.8.1 | /p:BCryptVersion=2.0.0 | ~!@#$(...)NBFRD [34] | $2a$1(...)nkrPO [29] | $2a$1(...)eyhgC [60] | 169.54 ms | 2.082 ms | 1.948 ms | 2 | 261.33 KB | | TestHashValidate | .NET Framework 4.8.1 | /p:BCryptVersion=2.1.4 | ~!@#$(...)NBFRD [34] | $2a$1(...)nkrPO [29] | $2a$1(...)eyhgC [60] | 169.22 ms | 1.691 ms | 1.499 ms | 2 | 261.33 KB | | TestHashValidate | .NET Framework 4.8.1 | /p:BCryptVersion=3.5.0 | ~!@#$(...)NBFRD [34] | $2a$1(...)nkrPO [29] | $2a$1(...)eyhgC [60] | 169.25 ms | 1.372 ms | 1.217 ms | 2 | 5.33 KB | | TestHashValidate | .NET Framework 4.8.1 | /p:BCryptVersion=4.0.3 | ~!@#$(...)NBFRD [34] | $2a$1(...)nkrPO [29] | $2a$1(...)eyhgC [60] | 172.02 ms | 3.346 ms | 3.436 ms | 2 | 5.33 KB | | TestHashValidate | .NET Framework 4.8.1 | /p:BCryptVersion=5.0.0-prerelease.g5300270033 | ~!@#$(...)NBFRD [34] | $2a$1(...)nkrPO [29] | $2a$1(...)eyhgC [60] | 173.11 ms | 3.154 ms | 3.375 ms | 2 | 5.33 KB |
* remove coverlet * install msft extensions bits
# Conflicts: # .github/workflows/ci-build.yml # .github/workflows/ci-manual-build-test-sign.yml # .github/workflows/codeql-analysis.yml # .github/workflows/dependency-review.yml # .github/workflows/devskim.yml # .github/workflows/generate-publish-docs.yml # .github/workflows/markdown-link-check.yml # .github/workflows/upload-coverage-report.yml # Directory.Packages.props
|
This pull request sets up GitHub code scanning for this repository. Once the scans have completed and the checks have passed, the analysis results for this pull request branch will appear on this overview. Once you merge this pull request, the 'Security' tab will show more code scanning analysis results (for example, for the default branch). Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results. For more information about GitHub code scanning, check out the documentation. |
There was a problem hiding this comment.
Pull request overview
This PR adds comprehensive readonly span support to BCrypt.Net, targeting .NET 10 and introducing several major enhancements including span-based APIs, buffer zeroing for security, input length validation, Enhanced Hashing V3 with HMAC, and experimental SecureString support. The PR represents a significant modernization effort with breaking changes.
Changes:
- Adds span-based APIs for reduced allocations and improved performance
- Introduces input length validation (breaking change - throws on >72 bytes)
- Implements Enhanced Hashing V3 with keyed HMAC for additional security
- Updates target frameworks to .NET 10, .NET 8, and various .NET Framework versions
Reviewed changes
Copilot reviewed 138 out of 170 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| version.json | Updates schema URL and adds assembly version precision, duplicate key issue |
| tests/UnitTests/ThreadLocalStressTests.cs | New comprehensive stress tests for concurrent ThreadLocal usage |
| tests/UnitTests/Base64Tests.cs | Adds NETCOREAPP-specific span-based Base64 encoding tests |
| tests/UnitTests/BCryptTestsExtendedv3.cs | Updates copyright year, test refactoring with renamed variables |
| tests/UnitTests/BCrypt.Net.UnitTests.csproj | Updates to .NET 10, adds configurations and defines |
| tests/Directory.Build.props | Changes signing configuration and updates test packages |
| tests/BCrypt.Net.IdentityExtensions.Tests/* | New test project for Identity Extensions |
| stryker-config.json | New mutation testing configuration |
| src/create-preview-release.cmd | New command file for preview releases |
| src/Directory.Build.props | Updates package configuration, removes commented code |
| src/BCrypt.Net/SaltParseException.cs | Changes conditional compilation, updates documentation |
| src/BCrypt.Net/SafeStringExtension.cs | New SecureString support for .NET Core |
| src/BCrypt.Net/Properties/AssemblyInfo.cs | Updates InternalsVisibleTo with public keys |
| src/BCrypt.Net/HashType.cs | Reformats to file-scoped namespace |
| src/BCrypt.Net/HashParser.cs | Reformats, adds StringComparison.Ordinal |
| src/BCrypt.Net/Hash*.cs | Various refactorings and formatting changes |
| src/BCrypt.Net/BCryptExtendedV2*.cs | Refactors enhanced hashing, adds span support |
| src/BCrypt.Net/BCrypt.Net.csproj | Updates target frameworks, adds conditional defines |
| src/BCrypt.Net.IdentityExtensions/* | Updates for StringComparison, culture-aware operations |
| global.json | Updates SDK to 10.0.0 |
| examples/* | Multiple new example projects and updates to .NET 10 |
| docs/* | Documentation updates |
| benchmarks/* | Comprehensive benchmark refactoring and new projects |
| .github/workflows/* | CI/CD updates for .NET 10 and dependency updates |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Changes
Spans buffers and spans
Adds new methods that allow passing in a buffer
Adds in buffer zeroing when no longer needed to reduce time in memory.
Changed targeting and csproj defined constants to ensure net8 still gains the available support for span that came with the memory package and netstandard support. This could potentially start at 4.7.2 but the netstandard integration was iffy.
Validate input length (Breaking)
If the usage doesn't make use of the enhanced hash generation to retain entropy (aka the standard use) the library will throw when the input to hash length is greater than 72 bytes.
This isn't standard behaviour in most bcrypt libraries where truncating silently is the default.
But I believe this is necessary, should always have been in place, and will force developers to decide deliberately to truncate input at 72 if they wish to retain existing behaviour.
Enhanced Hashing V3
Along with switching the func to a bog standard delegate, splitting the enhanced versions into separate classes simplified maintaining the previous versions and adding in the new version.
The v3 enhanced hash adds in keyed HMAC which apart from protecting against theorical risks of shucking adds a further uniqueness to the produced key material.
Experimental SafeString support
Hate SafeString, it's filled with lies and was really only a useful feature in Windows. In an API it should never be touched but there are some cases in Windows apps where people still require it to tick a box so I've added a sort of basic implementation
This code requires unsafe enabled so will probably be disabled initially.
SecureEquals check
This basically needed to be made for the span handling but also allowed a few minor optimizations which mimic the code in the .net codebase.
Net 10
We basically try to tag against the lts releases; the code may be updated to target sts between releases to assertain if there is any performance improvement worth having but otherwise its lts.
Examples
The api uses the identity extension to enable bcrypt passwords; chucked in a maui to test performance on android as we've been asked before by people doing the hashing at the device level
Release Benchmarks
Separate project that tests previous nuget package versions; its mostly as a simple way to test for regressions in performance while the other benchmark project is for testing components in a more targeted way.