From 85f65ed2fd4b106d38c7fc2e8166056ce0b97af1 Mon Sep 17 00:00:00 2001 From: Chris McKee Date: Tue, 1 Apr 2025 12:27:25 +0100 Subject: [PATCH 01/27] feat: validate input set; don't allow passwords exceeding max bCrypt length fix: CA1305 --- src/BCrypt.Net/BCrypt.cs | 5 +---- src/BCrypt.Net/BCryptBase.cs | 30 ++++++++++++++++++++---------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/BCrypt.Net/BCrypt.cs b/src/BCrypt.Net/BCrypt.cs index 2cd1180..13480e8 100644 --- a/src/BCrypt.Net/BCrypt.cs +++ b/src/BCrypt.Net/BCrypt.cs @@ -17,8 +17,6 @@ The above copyright notice and this permission notice shall be included in all c IN THE SOFTWARE. */ -using System; - namespace BCryptNet { /// BCrypt implementation. @@ -83,8 +81,7 @@ public sealed class BCrypt : BCryptCore /// returned if the hash is invalid /// returned if the user hash is null /// New hash of new password - public static string ValidateAndUpgradeHash(string currentKey, string currentHash, string newKey, - int workFactor = DefaultRounds, bool forceWorkFactor = false) + public static string ValidateAndUpgradeHash(string currentKey, string currentHash, string newKey, int workFactor = DefaultRounds, bool forceWorkFactor = false) { if (currentKey == null) throw new ArgumentNullException(nameof(currentKey)); diff --git a/src/BCrypt.Net/BCryptBase.cs b/src/BCrypt.Net/BCryptBase.cs index 5020d09..ac9c287 100644 --- a/src/BCrypt.Net/BCryptBase.cs +++ b/src/BCrypt.Net/BCryptBase.cs @@ -17,6 +17,7 @@ The above copyright notice and this permission notice shall be included in all c IN THE SOFTWARE. */ +using System.Globalization; using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Text; @@ -221,12 +222,11 @@ public class BCryptCore /// /// /// - internal static string CreatePasswordHash(string inputKey, string salt, HashType hashType = HashType.None, - Func enhancedHashKeyGen = null) + internal static string CreatePasswordHash(string inputKey, string salt, HashType hashType = HashType.None, Func enhancedHashKeyGen = null) { - if (inputKey == null) + if (string.IsNullOrEmpty(inputKey)) { - throw new ArgumentNullException(nameof(inputKey)); + throw new ArgumentException("Invalid input key: input key cannot be null or empty", nameof(inputKey)); } if (string.IsNullOrEmpty(salt)) @@ -234,10 +234,15 @@ internal static string CreatePasswordHash(string inputKey, string salt, HashType throw new ArgumentException("Invalid salt: salt cannot be null or empty", nameof(salt)); } + if (hashType == HashType.None && inputKey.Length > 72) + { + throw new ArgumentException("Invalid input key: input key cannot exceed 72 characters for bCrypt", nameof(inputKey)); + } + if (enhancedHashKeyGen == null && hashType != HashType.None) - throw new ArgumentException( - "Invalid HashType, You can't have an enhanced hash without an implementation of the key generator.", - nameof(hashType)); + { + throw new ArgumentException("Invalid HashType, You can't have an enhanced hash without an implementation of the key generator.", nameof(hashType)); + } // Determine the starting offset and validate the salt int startingOffset; @@ -270,7 +275,7 @@ internal static string CreatePasswordHash(string inputKey, string salt, HashType } // Extract details from salt - int workFactor = Convert.ToInt16(salt.Substring(startingOffset, 2)); + int workFactor = Convert.ToInt16(salt.Substring(startingOffset, 2), CultureInfo.InvariantCulture); // Throw if log rounds are out of range on hash, deals with custom salts if (workFactor < 1 || workFactor > 31) @@ -285,8 +290,10 @@ internal static string CreatePasswordHash(string inputKey, string salt, HashType inputBytes = SafeUTF8.GetBytes(inputKey + (bcryptMinorRevision >= 'a' ? Nul : EmptyString)); break; default: - if(enhancedHashKeyGen == null) + if (enhancedHashKeyGen == null) + { throw new ArgumentException("Invalid HashType, You can't have an enhanced hash without an implementation of the key generator.", nameof(hashType)); + } inputBytes = enhancedHashKeyGen(inputKey, hashType, bcryptMinorRevision); break; } @@ -302,7 +309,11 @@ internal static string CreatePasswordHash(string inputKey, string salt, HashType /// /// /// +#if NET5_0_OR_GREATER + internal static string HashBytes(ReadOnlySpan inputBytes, string extractedSalt, char bcryptMinorRevision, int workFactor) +#else internal static string HashBytes(byte[] inputBytes, string extractedSalt, char bcryptMinorRevision, int workFactor) +#endif { byte[] saltBytes = DecodeBase64(extractedSalt, BCryptSaltLen); @@ -376,7 +387,6 @@ internal static bool SecureEquals(byte[] a, byte[] b) return diff == 0; } - /// /// Encode a byte array using BCrypt's slightly-modified base64 encoding scheme. Note that this /// is *not* compatible with the standard MIME-base64 encoding. From d3112f1e22fbb9e8ca06b8c224048d0a1a847ece Mon Sep 17 00:00:00 2001 From: Chris McKee Date: Tue, 1 Apr 2025 16:52:20 +0100 Subject: [PATCH 02/27] feat: Add/Update overloads so we're utilising the allocated buffer regardless (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 --- benchmark/TestBcrypt_Hashing.cs | 14 +- src/BCrypt.Net/BCrypt.cs | 19 +- src/BCrypt.Net/BCryptBase.Core.cs | 420 +++++++++++++++++++++++ src/BCrypt.Net/BCryptBase.cs | 52 ++- tests/UnitTests/BCryptTests.cs | 101 +++++- tests/UnitTests/BCryptTestsExtendedv3.cs | 5 +- tests/UnitTests/Base64Tests.cs | 25 ++ 7 files changed, 597 insertions(+), 39 deletions(-) create mode 100644 src/BCrypt.Net/BCryptBase.Core.cs diff --git a/benchmark/TestBcrypt_Hashing.cs b/benchmark/TestBcrypt_Hashing.cs index 888724b..3483dc5 100644 --- a/benchmark/TestBcrypt_Hashing.cs +++ b/benchmark/TestBcrypt_Hashing.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using BCryptNet.BenchMarks._3._2._1; using BCryptNet.BenchMarks._3._5.perfmerge_1; using BCryptNet.BenchMarks._4._0._0; @@ -73,5 +74,16 @@ public string TestHashValidateCurrent(string key, string salt, string hash) return hashed + validateHashCheck.ToString(); } +#if NETCOREAPP + [Benchmark] + [ArgumentsSource(nameof(Data))] + public string TestHashValidateCurrentSpan(string key, string salt, string hash) + { + string hashed = BCrypt.HashPassword(key.AsSpan(), salt.AsSpan()); + var validateHashCheck = BCrypt.Verify(key, hashed); + return hashed + validateHashCheck.ToString(); + } +#endif + } } diff --git a/src/BCrypt.Net/BCrypt.cs b/src/BCrypt.Net/BCrypt.cs index 13480e8..f212c9c 100644 --- a/src/BCrypt.Net/BCrypt.cs +++ b/src/BCrypt.Net/BCrypt.cs @@ -169,8 +169,25 @@ public static string HashPassword(string inputKey, int workFactor = DefaultRound /// factor therefore increases as 2^workFactor. Default is 11 /// The hashed password. /// Thrown when the salt could not be parsed. - public static string HashPassword(string inputKey, string salt) => CreatePasswordHash(inputKey, salt); + public static string HashPassword(string inputKey, string salt) + { +#if NETCOREAPP + return HashPassword(inputKey.AsSpan(), salt.AsSpan()); +#else + return CreatePasswordHash(inputKey, salt); +#endif + } + +#if NETCOREAPP + public static string HashPassword(ReadOnlySpan inputKey, ReadOnlySpan salt) + { + Span outputBuffer = stackalloc char[60]; + BCrypt.HashPassword(inputKey, salt, outputBuffer, out var outputBufferWritten); + return new string(outputBuffer[..outputBufferWritten]); + } + public static void HashPassword(ReadOnlySpan inputKey, ReadOnlySpan salt, Span outputBuffer, out int outputBufferWritten) => CreatePasswordHash(inputKey, salt, outputBuffer, out outputBufferWritten); +#endif /// /// Based on password_needs_rehash in PHP this method will return true diff --git a/src/BCrypt.Net/BCryptBase.Core.cs b/src/BCrypt.Net/BCryptBase.Core.cs new file mode 100644 index 0000000..9937efa --- /dev/null +++ b/src/BCrypt.Net/BCryptBase.Core.cs @@ -0,0 +1,420 @@ +// /* +// The MIT License (MIT) +// Copyright (c) 2006 Damien Miller djm@mindrot.org (jBCrypt) +// Copyright (c) 2013 Ryan D. Emerle (.Net port) +// Copyright (c) 2016/2025 Chris McKee (.Net-core port / patches / new features) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +// (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, +// merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished +// to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// */ + +using System.Diagnostics; +using System.Globalization; +using System.Text; + +namespace BCryptNet; + +public partial class BCryptCore +{ +#if NETCOREAPP + /// + /// Create Password Hash Base + /// + /// + /// + /// + /// + /// + /// + /// + /// + internal static string CreatePasswordHash(ReadOnlySpan inputKey, ReadOnlySpan salt, HashType hashType = HashType.None, Func enhancedHashKeyGen = null) + { + Span outputBuffer = stackalloc char[60]; + CreatePasswordHash(inputKey, salt, outputBuffer, out var outputBufferWritten, hashType, enhancedHashKeyGen); + return new string(outputBuffer[..outputBufferWritten]); + } + + /// + /// Create Password Hash Base + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + internal static void CreatePasswordHash(ReadOnlySpan inputKey, ReadOnlySpan salt, Span outputBuffer, out int outputBufferWritten, HashType hashType = HashType.None, Func enhancedHashKeyGen = null) + { + if (salt.IsEmpty) + { + throw new ArgumentException("Invalid salt: salt cannot be empty", nameof(salt)); + } + + if (hashType == HashType.None && inputKey.Length > 72) + { + throw new ArgumentException("Invalid input key: input key cannot exceed 72 characters for bCrypt", nameof(inputKey)); + } + + if (enhancedHashKeyGen == null && hashType != HashType.None) + { + throw new ArgumentException("Invalid HashType, You can't have an enhanced hash without an implementation of the key generator.", nameof(hashType)); + } + + if(outputBuffer.Length != 60) + { + throw new ArgumentException("Output buffer must be 60 characters long", nameof(outputBuffer)); + } + + // Determine the starting offset and validate the salt + int startingOffset; + char bcryptMinorRevision = (char)0; + + if (salt[0] != '$' || salt[1] != '2') + { + throw new SaltParseException("Invalid salt version"); + } + + if (salt[2] == '$') + { + startingOffset = 3; + } + else + { + bcryptMinorRevision = salt[2]; + if (bcryptMinorRevision != 'a' && bcryptMinorRevision != 'b' && bcryptMinorRevision != 'x' && + bcryptMinorRevision != 'y' || salt[3] != '$') + { + throw new SaltParseException("Invalid salt revision"); + } + + startingOffset = 4; + } + + // Extract number of rounds + // Extract details from salt + if (!int.TryParse(salt.Slice(startingOffset, 2), NumberStyles.None, CultureInfo.InvariantCulture, out int workFactor)) + { + throw new SaltParseException("Missing salt rounds"); + } + + // Throw if log rounds are out of range on hash, deals with custom salts + if (workFactor < 1 || workFactor > 31) + { + throw new SaltParseException("Salt rounds out of range"); + } + + Span inputBytes; + switch (hashType) + { + case HashType.None: + bool appendNul = bcryptMinorRevision >= 'a'; + Span utf8Buffer = stackalloc byte[Encoding.UTF8.GetMaxByteCount(inputKey.Length + (appendNul ? 1 : 0))]; + int bytesWritten = Encoding.UTF8.GetBytes(inputKey, utf8Buffer); + if (appendNul) utf8Buffer[bytesWritten++] = 0; + inputBytes = utf8Buffer[..bytesWritten].ToArray(); + break; + default: + if (enhancedHashKeyGen == null) + { + throw new ArgumentException("Invalid HashType, You can't have an enhanced hash without an implementation of the key generator.", nameof(hashType)); + } + + inputBytes = enhancedHashKeyGen(new string(inputKey), hashType, bcryptMinorRevision); + break; + } + + if (!HashBytes(inputBytes, salt.Slice(startingOffset + 3, 22), bcryptMinorRevision, workFactor, outputBuffer, out int written)) + throw new BcryptAuthenticationException("Couldn't hash input"); + + outputBufferWritten = written; + } + + /// + /// + /// + /// + /// + /// + /// + /// + internal static bool HashBytes( + ReadOnlySpan inputBytes, + ReadOnlySpan extractedSalt, + char bcryptMinorRevision, + int workFactor, + Span destination, + out int charsWritten) + { + charsWritten = 0; + var bCrypt = new BCrypt(); + + Span saltBuffer = stackalloc byte[BCryptSaltLen]; + int written = DecodeBase64(extractedSalt, saltBuffer); + var saltBytes = saltBuffer[..written]; + + Span hashBuffer = stackalloc byte[BfCryptCiphertext.Length * 4]; + var hashBytes = bCrypt.CryptRaw(inputBytes, saltBytes, workFactor, hashBuffer); + + // Ensure the destination is large enough + // "$2x$10$" + base64(16 bytes) + base64(23 bytes) = 60 characters + if (destination.Length < 60) + return false; + + int pos = 0; + destination[pos++] = '$'; + destination[pos++] = '2'; + destination[pos++] = bcryptMinorRevision; + destination[pos++] = '$'; + + // Write work factor as 2-digit number + if (!workFactor.TryFormat(destination.Slice(pos), out int wfChars, "D2")) + return false; + pos += wfChars; + + destination[pos++] = '$'; + + // Write base64-encoded salt + if (!TryEncodeBase64(saltBytes, saltBytes.Length, destination.Slice(pos), out int saltChars)) + return false; + pos += saltChars; + + // Write base64-encoded hash + if (!TryEncodeBase64(hashBytes, (BfCryptCiphertextLength * 4) - 1, destination.Slice(pos), out int hashChars)) + return false; + pos += hashChars; + + charsWritten = pos; + return true; + } + + internal static string GenerateSalt(int workFactor = DefaultRounds, char bcryptMinorRevision = DefaultHashVersion) + { + if (workFactor < MinRounds || workFactor > MaxRounds) + { + throw new ArgumentOutOfRangeException(nameof(workFactor), workFactor, + $"The work factor must be between {MinRounds} and {MaxRounds} (inclusive)"); + } + + if (bcryptMinorRevision != 'a' && bcryptMinorRevision != 'b' && bcryptMinorRevision != 'x' && + bcryptMinorRevision != 'y') + { + throw new ArgumentException("BCrypt Revision should be a, b, x or y", nameof(bcryptMinorRevision)); + } + + byte[] saltBytes = new byte[BCryptSaltLen]; + + RngCsp.GetBytes(saltBytes); + + var result = new StringBuilder(29); + result.Append('$').Append('2').Append(bcryptMinorRevision).Append('$').Append(workFactor.ToString("D2", CultureInfo.InvariantCulture)).Append('$'); + + // Base65 encoded salt + result.Append(EncodeBase64(saltBytes, saltBytes.Length)); + + return result.ToString(); + } + + internal static bool TryEncodeBase64(ReadOnlySpan byteArray, int length, Span destination, out int charsWritten) + { + charsWritten = 0; + + if (length <= 0 || length > byteArray.Length) + return false; + + int encodedSize = (int)Math.Ceiling((length * 4D) / 3); + if (destination.Length < encodedSize) + return false; + + int pos = 0; + int off = 0; + while (off < length) + { + int c1 = byteArray[off++] & 0xff; + destination[pos++] = Base64Code[(c1 >> 2) & 0x3f]; + c1 = (c1 & 0x03) << 4; + + if (off >= length) + { + destination[pos++] = Base64Code[c1 & 0x3f]; + break; + } + + int c2 = byteArray[off++] & 0xff; + c1 |= (c2 >> 4) & 0x0f; + destination[pos++] = Base64Code[c1 & 0x3f]; + c1 = (c2 & 0x0f) << 2; + + if (off >= length) + { + destination[pos++] = Base64Code[c1 & 0x3f]; + break; + } + + c2 = byteArray[off++] & 0xff; + c1 |= (c2 >> 6) & 0x03; + destination[pos++] = Base64Code[c1 & 0x3f]; + destination[pos++] = Base64Code[c2 & 0x3f]; + } + + charsWritten = pos; + return true; + } + + internal static Span EncodeBase64(ReadOnlySpan byteArray, int length) + { + if (length <= 0 || length > byteArray.Length) + { + throw new ArgumentException("Invalid length", nameof(length)); + } + + int encodedSize = (int)Math.Ceiling((length * 4D) / 3); + char[] encoded = new char[encodedSize]; + + int pos = 0; + int off = 0; + while (off < length) + { + //Process first byte in group + int c1 = byteArray[off++] & 0xff; + encoded[pos++] = Base64Code[(c1 >> 2) & 0x3f]; + c1 = (c1 & 0x03) << 4; + if (off >= length) + { + encoded[pos++] = Base64Code[c1 & 0x3f]; + break; + } + + // second byte of group + int c2 = byteArray[off++] & 0xff; + c1 |= (c2 >> 4) & 0x0f; + encoded[pos++] = Base64Code[c1 & 0x3f]; + c1 = (c2 & 0x0f) << 2; + if (off >= length) + { + encoded[pos++] = Base64Code[c1 & 0x3f]; + break; + } + + // third byte of group + c2 = byteArray[off++] & 0xff; + c1 |= (c2 >> 6) & 0x03; + encoded[pos++] = Base64Code[c1 & 0x3f]; + encoded[pos++] = Base64Code[c2 & 0x3f]; + } + + return encoded; + } + + /// + /// Decode a string encoded using BCrypt's base64 scheme to a byte array. + /// Note that this is *not* compatible with the standard MIME-base64 encoding. + /// + /// Thrown when one or more arguments have unsupported or + /// illegal values. + /// The string to decode. + /// + /// The decoded byte array. + public static int DecodeBase64(ReadOnlySpan encodedSpan, Span destination) + { + int outputLength = 0; + int position = 0; + + while (position < encodedSpan.Length - 1 && outputLength < destination.Length) + { + int c1 = Char64(encodedSpan[position++]); + int c2 = Char64(encodedSpan[position++]); + if (c1 == -1 || c2 == -1) break; + + destination[outputLength] = (byte)((c1 << 2) | ((c2 & 0x30) >> 4)); + if (++outputLength >= destination.Length || position >= encodedSpan.Length) break; + + int c3 = Char64(encodedSpan[position++]); + if (c3 == -1) break; + + destination[outputLength] = (byte)(((c2 & 0x0F) << 4) | ((c3 & 0x3C) >> 2)); + if (++outputLength >= destination.Length || position >= encodedSpan.Length) break; + + int c4 = Char64(encodedSpan[position++]); + if (c4 == -1) break; + + destination[outputLength] = (byte)(((c3 & 0x03) << 6) | c4); + ++outputLength; + } + + return outputLength; + } + + internal ReadOnlySpan CryptRaw(ReadOnlySpan inputBytes, ReadOnlySpan saltBytes, int workFactor, Span destination) + { + int i; + int j; + + Span cdata = stackalloc uint[BfCryptCiphertext.Length]; + BfCryptCiphertext.CopyTo(cdata); + int clen = cdata.Length; + + if (workFactor < MinRounds || workFactor > MaxRounds) + { + throw new ArgumentException("Bad number of rounds", nameof(workFactor)); + } + + if (saltBytes.Length != BCryptSaltLen) + { + throw new ArgumentException("Bad salt Length", nameof(saltBytes)); + } + + uint rounds = 1u << workFactor; + + // We overflowed rounds at 31 - added safety check + if (rounds < 1) + { + throw new ArgumentException("Bad number of rounds", nameof(workFactor)); + } + + InitializeKey(); + EKSKey(saltBytes, inputBytes); + + for (i = 0; i != rounds; i++) + { + Key(inputBytes); + Key(saltBytes); + } + + for (i = 0; i < 64; i++) + { + for (j = 0; j < (clen >> 1); j++) + { + Encipher(cdata, j << 1); + } + } + + // Convert ciphertext to output byte-array + for (i = 0, j = 0; i < clen; i++) + { + // per-line extract first byte by shifting cdata word at index right 24 bits + // using >> op then isolate the least significant byte using mask 0xff + destination[j++] = (byte)((cdata[i] >> 24) & 0xff); + destination[j++] = (byte)((cdata[i] >> 16) & 0xff); + destination[j++] = (byte)((cdata[i] >> 8) & 0xff); + destination[j++] = (byte)(cdata[i] & 0xff); + } + + return destination; + } + +#endif +} diff --git a/src/BCrypt.Net/BCryptBase.cs b/src/BCrypt.Net/BCryptBase.cs index ac9c287..116c5e3 100644 --- a/src/BCrypt.Net/BCryptBase.cs +++ b/src/BCrypt.Net/BCryptBase.cs @@ -17,6 +17,7 @@ The above copyright notice and this permission notice shall be included in all c IN THE SOFTWARE. */ +using System.Diagnostics; using System.Globalization; using System.Runtime.CompilerServices; using System.Security.Cryptography; @@ -27,7 +28,7 @@ namespace BCryptNet; /// /// Base class of the bcrypt api /// -public class BCryptCore +public partial class BCryptCore { // BCrypt parameters /// @@ -211,8 +212,9 @@ public class BCryptCore private uint[] _p; private uint[] _s; +#if !NETCOREAPP /// - /// + /// Create Password Hash Base /// /// /// @@ -224,11 +226,6 @@ public class BCryptCore /// internal static string CreatePasswordHash(string inputKey, string salt, HashType hashType = HashType.None, Func enhancedHashKeyGen = null) { - if (string.IsNullOrEmpty(inputKey)) - { - throw new ArgumentException("Invalid input key: input key cannot be null or empty", nameof(inputKey)); - } - if (string.IsNullOrEmpty(salt)) { throw new ArgumentException("Invalid salt: salt cannot be null or empty", nameof(salt)); @@ -298,22 +295,14 @@ internal static string CreatePasswordHash(string inputKey, string salt, HashType break; } + return HashBytes(inputBytes, salt.Substring(startingOffset + 3, 22), bcryptMinorRevision, workFactor); + } +#endif - /// - /// - /// - /// - /// - /// - /// - /// -#if NET5_0_OR_GREATER - internal static string HashBytes(ReadOnlySpan inputBytes, string extractedSalt, char bcryptMinorRevision, int workFactor) -#else +#if !NETCOREAPP internal static string HashBytes(byte[] inputBytes, string extractedSalt, char bcryptMinorRevision, int workFactor) -#endif { byte[] saltBytes = DecodeBase64(extractedSalt, BCryptSaltLen); @@ -323,13 +312,16 @@ internal static string HashBytes(byte[] inputBytes, string extractedSalt, char b // Generate result string var result = new StringBuilder(60); - result.Append("$2").Append(bcryptMinorRevision).Append('$').Append(workFactor.ToString("D2")).Append('$'); + result.Append('$').Append('2').Append(bcryptMinorRevision).Append('$').Append(workFactor.ToString("D2", CultureInfo.InvariantCulture)).Append('$'); result.Append(EncodeBase64(saltBytes, saltBytes.Length)); result.Append(EncodeBase64(hashed, (BfCryptCiphertextLength * 4) - 1)); return result.ToString(); } +#endif + +#if !NETCOREAPP /// /// Generate a salt for use with the method. /// @@ -363,6 +355,7 @@ internal static string GenerateSalt(int workFactor = DefaultRounds, char bcryptM return result.ToString(); } +#endif // Compares two byte arrays for equality. The method is specifically written so that the loop is not optimised. [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] @@ -387,6 +380,7 @@ internal static bool SecureEquals(byte[] a, byte[] b) return diff == 0; } + #if !NETCOREAPP /// /// Encode a byte array using BCrypt's slightly-modified base64 encoding scheme. Note that this /// is *not* compatible with the standard MIME-base64 encoding. @@ -440,15 +434,17 @@ internal static char[] EncodeBase64(byte[] byteArray, int length) return encoded; } + #endif +#if !NETCOREAPP /// /// Decode a string encoded using BCrypt's base64 scheme to a byte array. /// Note that this is *not* compatible with the standard MIME-base64 encoding. /// /// Thrown when one or more arguments have unsupported or /// illegal values. - /// The string to decode. + /// The string to decode. /// The maximum bytes to decode. /// The decoded byte array. internal static byte[] DecodeBase64(string encodedString, int maximumBytes) @@ -499,6 +495,7 @@ internal static byte[] DecodeBase64(string encodedString, int maximumBytes) return result; } +#endif /// /// Look up the 3 bits base64-encoded by the specified character, range-checking against @@ -506,6 +503,7 @@ internal static byte[] DecodeBase64(string encodedString, int maximumBytes) /// /// The base64-encoded value. /// The decoded value of x. + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int Char64(char character) { return character < 0 || character > Index64.Length ? -1 : Index64[character]; @@ -665,6 +663,7 @@ private void EKSKey(byte[] saltBytes, byte[] inputBytes) } } +#if !NETCOREAPP /// Perform the central hashing step in the BCrypt scheme. /// Thrown when one or more arguments have unsupported or /// illegal values. @@ -672,22 +671,14 @@ private void EKSKey(byte[] saltBytes, byte[] inputBytes) /// The salt byte array to hash with. /// The binary logarithm of the number of rounds of hashing to apply. /// A byte array containing the hashed result. -#if NETCOREAPP - internal byte[] CryptRaw(ReadOnlySpan inputBytes, ReadOnlySpan saltBytes, int workFactor) -#else internal byte[] CryptRaw(byte[] inputBytes, byte[] saltBytes, int workFactor) -#endif + { int i; int j; -#if NETCOREAPP - Span cdata = stackalloc uint[BfCryptCiphertext.Length]; - BfCryptCiphertext.CopyTo(cdata); -#else uint[] cdata = new uint[BfCryptCiphertext.Length]; Array.Copy(BfCryptCiphertext, cdata, BfCryptCiphertext.Length); -#endif int clen = cdata.Length; @@ -740,4 +731,5 @@ internal byte[] CryptRaw(byte[] inputBytes, byte[] saltBytes, int workFactor) return ret; } + #endif } diff --git a/tests/UnitTests/BCryptTests.cs b/tests/UnitTests/BCryptTests.cs index 301fc1f..03a7582 100644 --- a/tests/UnitTests/BCryptTests.cs +++ b/tests/UnitTests/BCryptTests.cs @@ -184,8 +184,7 @@ public void BCryptMaintainsLengthRestrictionsFromPaper() var inBounds = "testtdsdddddddddddddddddddddddddddddddddddddddddddddddsddddddddddddddddd"; //72char var exceedsBounds = "testtdsdddddddddddddddddddddddddddddddddddddddddddddddsdddddddddddddddddd"; //73char var hashPassword = BCrypt.HashPassword(inBounds); - var exceedsBoundsShouldValidate = BCrypt.Verify(exceedsBounds, hashPassword); - Assert.True(exceedsBoundsShouldValidate); + Assert.Throws(() => BCrypt.HashPassword(exceedsBounds)); } @@ -231,6 +230,89 @@ public void TestHashPassword() Trace.WriteLine(""); } + /** + * Test method for 'BCrypt.HashPassword(string, string)' + */ + [Fact()] + public void TestHashPasswordSpanToString() + { + Trace.Write("BCrypt.HashPassword(): "); + var sw = Stopwatch.StartNew(); + for (var r = 0; r < _revisions.Length; r++) + { + for (int i = 0; i < _testVectors.Length / 3; i++) + { + string plain = _testVectors[i, 0]; + string salt; + if (r > 0) + { + //Check hash that goes in one end comes out the next the same + salt = _testVectors[i, 1].Replace("2a", "2" + _revisions[r]); + + string hashed = BCrypt.HashPassword(plain.AsSpan(), salt.AsSpan()); + + Assert.StartsWith("$2" + _revisions[r], hashed); + Trace.WriteLine(hashed); + } + else + { + salt = _testVectors[i, 1]; + var expected = _testVectors[i, 2]; + + string hashed = BCrypt.HashPassword(plain.AsSpan(), salt.AsSpan()); + Assert.Equal(expected, hashed); + } + + Trace.Write("."); + } + } + + Trace.WriteLine(sw.ElapsedMilliseconds); + Trace.WriteLine(""); + } + + [Fact()] + public void TestHashPasswordSpanBuffer() + { + Trace.Write("BCrypt.HashPassword(): "); + var sw = Stopwatch.StartNew(); + + Span outputBuffer = stackalloc char[60]; + + for (var r = 0; r < _revisions.Length; r++) + { + for (int i = 0; i < _testVectors.Length / 3; i++) + { + string plain = _testVectors[i, 0]; + string salt; + if (r > 0) + { + //Check hash that goes in one end comes out the next the same + salt = _testVectors[i, 1].Replace("2a", "2" + _revisions[r]); + BCrypt.HashPassword(plain.AsSpan(), salt.AsSpan(), outputBuffer, out var outputBufferWritten); + var hashed = new string(outputBuffer.Slice(0, outputBufferWritten)); + Assert.StartsWith("$2" + _revisions[r], hashed); + Trace.WriteLine(hashed); + } + else + { + salt = _testVectors[i, 1]; + var expected = _testVectors[i, 2]; + + BCrypt.HashPassword(plain.AsSpan(), salt.AsSpan(), outputBuffer, out var outputBufferWritten); + var hashed = new string(outputBuffer.Slice(0, outputBufferWritten)); + Assert.Equal(expected, hashed); + } + + + Trace.Write("."); + } + } + + Trace.WriteLine(sw.ElapsedMilliseconds); + Trace.WriteLine(""); + } + /** * Test method for 'BCrypt.HashPassword(string, string)' @@ -550,10 +632,10 @@ public void TestInternationalChars(string pw1) [Theory()] - [InlineData("RwiKnN>9xg3*C)1AZl.)y8f_:GCz,vt3T]PIV)[7kktZZQ)z1HI(gyrqgn6;gyb]eIP>r1f:")] + [InlineData("RwiKnN>9xg3*C)1AZl.)y8f_:GCz,vt3T]PIV)[7kktZ")] + [InlineData("")] [InlineData("ππππππππ")] - [InlineData("ЁЂЃЄЅІЇЈЉЊЋЌЍЎЏАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюя")] + [InlineData("ЁЂЃЄЅІЇЈЉЊЋЌЍЎЏАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬ")] [InlineData("ÅÍÎÏ˝ÓÔÒÚÆ☃")] [InlineData("사회과학원 어학연구소")] [InlineData("゚・✿ヾ╲(。◕‿◕。)╱✿・゚")] @@ -607,6 +689,9 @@ public void VerificationTestForVariousOtherLibGeneratedHashes(string password, s Assert.Equal(libHash, hash); } + #if NETCOREAPP + + #else [Fact] public void LeadingByteDoesntTruncateHash() { @@ -632,7 +717,11 @@ public void LeadingByteDoesntTruncateHash() Assert.False(Convert.ToBase64String(hashA) == Convert.ToBase64String(hashB), "These shouldn't match as we hash the whole strings bytes, including the null byte"); } + #endif +#if NETCOREAPP + +#else [Fact] public void LeadingByteDoesntTruncateHashSHA() { @@ -661,7 +750,7 @@ public void LeadingByteDoesntTruncateHashSHA() Assert.False(Convert.ToBase64String(hashA) == Convert.ToBase64String(hashB), "These shouldnt match as we hash the whole strings bytes, including the null byte"); } - +#endif private bool ContainsNoNullBytes(byte[] bytes) { if (bytes == null) return false; diff --git a/tests/UnitTests/BCryptTestsExtendedv3.cs b/tests/UnitTests/BCryptTestsExtendedv3.cs index 6b6c467..493faad 100644 --- a/tests/UnitTests/BCryptTestsExtendedv3.cs +++ b/tests/UnitTests/BCryptTestsExtendedv3.cs @@ -245,6 +245,9 @@ public void NullTerminationCausesBCryptToTerminateStringInSomeFrameworksSetB(str Assert.False(BCryptExtendedV3.Verify(hmacKey,"", hash), "Null should be treated as part of password as per spec"); } +#if NETCOREAPP + +#else [Fact] public void LeadingByteDoesntTruncateHashSHA() { @@ -271,7 +274,7 @@ public void LeadingByteDoesntTruncateHashSHA() Assert.False(Convert.ToBase64String(hashA) == Convert.ToBase64String(hashB), "These shouldnt match as we hash the whole strings bytes, including the null byte"); } - +#endif private static bool ContainsNoNullBytes(byte[] bytes) { if (bytes == null) return false; diff --git a/tests/UnitTests/Base64Tests.cs b/tests/UnitTests/Base64Tests.cs index b320d68..993f654 100644 --- a/tests/UnitTests/Base64Tests.cs +++ b/tests/UnitTests/Base64Tests.cs @@ -25,6 +25,30 @@ namespace BCryptNet.UnitTests; public class Base64Tests { +#if NETCOREAPP + [Fact] + public void EncodeBase64_ValidInput_ReturnsCorrectBase64Encoding() + { + // Arrange + byte[] saltBytes = Encoding.UTF8.GetBytes("Hello, world!"); + + // Act + // Assert + string expectedResult = "QETqZE6qGFbtakviGO"; + Assert.Equal(expectedResult, new string(BCryptCore.EncodeBase64(saltBytes, saltBytes.Length))); + } + + [Fact] + public void EncodeBase64_InvalidLength_ThrowsArgumentException() + { + // Act and Assert + Assert.Throws(() => + { + byte[] saltBytes = Encoding.UTF8.GetBytes("Hello, world!"); + return new string(BCryptCore.EncodeBase64(saltBytes, -1)); + }); + } +#else [Fact] public void EncodeBase64_ValidInput_ReturnsCorrectBase64Encoding() { @@ -50,4 +74,5 @@ public void EncodeBase64_InvalidLength_ThrowsArgumentException() // Act and Assert Assert.Throws(() => BCryptCore.EncodeBase64(byteArray, invalidLength)); } +#endif } From d91243e95494818683ccdcd205d08649dc9d1d5f Mon Sep 17 00:00:00 2001 From: Chris McKee Date: Tue, 1 Apr 2025 17:08:02 +0100 Subject: [PATCH 03/27] feat: move the legacy pre-span code into its own file instead --- src/BCrypt.Net/BCryptBase.Core.cs | 420 -------------------------- src/BCrypt.Net/BCryptBase.Fwk.cs | 475 ++++++++++++++++++++++++++++++ src/BCrypt.Net/BCryptBase.cs | 469 +++++++++++++++-------------- 3 files changed, 730 insertions(+), 634 deletions(-) delete mode 100644 src/BCrypt.Net/BCryptBase.Core.cs create mode 100644 src/BCrypt.Net/BCryptBase.Fwk.cs diff --git a/src/BCrypt.Net/BCryptBase.Core.cs b/src/BCrypt.Net/BCryptBase.Core.cs deleted file mode 100644 index 9937efa..0000000 --- a/src/BCrypt.Net/BCryptBase.Core.cs +++ /dev/null @@ -1,420 +0,0 @@ -// /* -// The MIT License (MIT) -// Copyright (c) 2006 Damien Miller djm@mindrot.org (jBCrypt) -// Copyright (c) 2013 Ryan D. Emerle (.Net port) -// Copyright (c) 2016/2025 Chris McKee (.Net-core port / patches / new features) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files -// (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, -// merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished -// to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -// IN THE SOFTWARE. -// */ - -using System.Diagnostics; -using System.Globalization; -using System.Text; - -namespace BCryptNet; - -public partial class BCryptCore -{ -#if NETCOREAPP - /// - /// Create Password Hash Base - /// - /// - /// - /// - /// - /// - /// - /// - /// - internal static string CreatePasswordHash(ReadOnlySpan inputKey, ReadOnlySpan salt, HashType hashType = HashType.None, Func enhancedHashKeyGen = null) - { - Span outputBuffer = stackalloc char[60]; - CreatePasswordHash(inputKey, salt, outputBuffer, out var outputBufferWritten, hashType, enhancedHashKeyGen); - return new string(outputBuffer[..outputBufferWritten]); - } - - /// - /// Create Password Hash Base - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - internal static void CreatePasswordHash(ReadOnlySpan inputKey, ReadOnlySpan salt, Span outputBuffer, out int outputBufferWritten, HashType hashType = HashType.None, Func enhancedHashKeyGen = null) - { - if (salt.IsEmpty) - { - throw new ArgumentException("Invalid salt: salt cannot be empty", nameof(salt)); - } - - if (hashType == HashType.None && inputKey.Length > 72) - { - throw new ArgumentException("Invalid input key: input key cannot exceed 72 characters for bCrypt", nameof(inputKey)); - } - - if (enhancedHashKeyGen == null && hashType != HashType.None) - { - throw new ArgumentException("Invalid HashType, You can't have an enhanced hash without an implementation of the key generator.", nameof(hashType)); - } - - if(outputBuffer.Length != 60) - { - throw new ArgumentException("Output buffer must be 60 characters long", nameof(outputBuffer)); - } - - // Determine the starting offset and validate the salt - int startingOffset; - char bcryptMinorRevision = (char)0; - - if (salt[0] != '$' || salt[1] != '2') - { - throw new SaltParseException("Invalid salt version"); - } - - if (salt[2] == '$') - { - startingOffset = 3; - } - else - { - bcryptMinorRevision = salt[2]; - if (bcryptMinorRevision != 'a' && bcryptMinorRevision != 'b' && bcryptMinorRevision != 'x' && - bcryptMinorRevision != 'y' || salt[3] != '$') - { - throw new SaltParseException("Invalid salt revision"); - } - - startingOffset = 4; - } - - // Extract number of rounds - // Extract details from salt - if (!int.TryParse(salt.Slice(startingOffset, 2), NumberStyles.None, CultureInfo.InvariantCulture, out int workFactor)) - { - throw new SaltParseException("Missing salt rounds"); - } - - // Throw if log rounds are out of range on hash, deals with custom salts - if (workFactor < 1 || workFactor > 31) - { - throw new SaltParseException("Salt rounds out of range"); - } - - Span inputBytes; - switch (hashType) - { - case HashType.None: - bool appendNul = bcryptMinorRevision >= 'a'; - Span utf8Buffer = stackalloc byte[Encoding.UTF8.GetMaxByteCount(inputKey.Length + (appendNul ? 1 : 0))]; - int bytesWritten = Encoding.UTF8.GetBytes(inputKey, utf8Buffer); - if (appendNul) utf8Buffer[bytesWritten++] = 0; - inputBytes = utf8Buffer[..bytesWritten].ToArray(); - break; - default: - if (enhancedHashKeyGen == null) - { - throw new ArgumentException("Invalid HashType, You can't have an enhanced hash without an implementation of the key generator.", nameof(hashType)); - } - - inputBytes = enhancedHashKeyGen(new string(inputKey), hashType, bcryptMinorRevision); - break; - } - - if (!HashBytes(inputBytes, salt.Slice(startingOffset + 3, 22), bcryptMinorRevision, workFactor, outputBuffer, out int written)) - throw new BcryptAuthenticationException("Couldn't hash input"); - - outputBufferWritten = written; - } - - /// - /// - /// - /// - /// - /// - /// - /// - internal static bool HashBytes( - ReadOnlySpan inputBytes, - ReadOnlySpan extractedSalt, - char bcryptMinorRevision, - int workFactor, - Span destination, - out int charsWritten) - { - charsWritten = 0; - var bCrypt = new BCrypt(); - - Span saltBuffer = stackalloc byte[BCryptSaltLen]; - int written = DecodeBase64(extractedSalt, saltBuffer); - var saltBytes = saltBuffer[..written]; - - Span hashBuffer = stackalloc byte[BfCryptCiphertext.Length * 4]; - var hashBytes = bCrypt.CryptRaw(inputBytes, saltBytes, workFactor, hashBuffer); - - // Ensure the destination is large enough - // "$2x$10$" + base64(16 bytes) + base64(23 bytes) = 60 characters - if (destination.Length < 60) - return false; - - int pos = 0; - destination[pos++] = '$'; - destination[pos++] = '2'; - destination[pos++] = bcryptMinorRevision; - destination[pos++] = '$'; - - // Write work factor as 2-digit number - if (!workFactor.TryFormat(destination.Slice(pos), out int wfChars, "D2")) - return false; - pos += wfChars; - - destination[pos++] = '$'; - - // Write base64-encoded salt - if (!TryEncodeBase64(saltBytes, saltBytes.Length, destination.Slice(pos), out int saltChars)) - return false; - pos += saltChars; - - // Write base64-encoded hash - if (!TryEncodeBase64(hashBytes, (BfCryptCiphertextLength * 4) - 1, destination.Slice(pos), out int hashChars)) - return false; - pos += hashChars; - - charsWritten = pos; - return true; - } - - internal static string GenerateSalt(int workFactor = DefaultRounds, char bcryptMinorRevision = DefaultHashVersion) - { - if (workFactor < MinRounds || workFactor > MaxRounds) - { - throw new ArgumentOutOfRangeException(nameof(workFactor), workFactor, - $"The work factor must be between {MinRounds} and {MaxRounds} (inclusive)"); - } - - if (bcryptMinorRevision != 'a' && bcryptMinorRevision != 'b' && bcryptMinorRevision != 'x' && - bcryptMinorRevision != 'y') - { - throw new ArgumentException("BCrypt Revision should be a, b, x or y", nameof(bcryptMinorRevision)); - } - - byte[] saltBytes = new byte[BCryptSaltLen]; - - RngCsp.GetBytes(saltBytes); - - var result = new StringBuilder(29); - result.Append('$').Append('2').Append(bcryptMinorRevision).Append('$').Append(workFactor.ToString("D2", CultureInfo.InvariantCulture)).Append('$'); - - // Base65 encoded salt - result.Append(EncodeBase64(saltBytes, saltBytes.Length)); - - return result.ToString(); - } - - internal static bool TryEncodeBase64(ReadOnlySpan byteArray, int length, Span destination, out int charsWritten) - { - charsWritten = 0; - - if (length <= 0 || length > byteArray.Length) - return false; - - int encodedSize = (int)Math.Ceiling((length * 4D) / 3); - if (destination.Length < encodedSize) - return false; - - int pos = 0; - int off = 0; - while (off < length) - { - int c1 = byteArray[off++] & 0xff; - destination[pos++] = Base64Code[(c1 >> 2) & 0x3f]; - c1 = (c1 & 0x03) << 4; - - if (off >= length) - { - destination[pos++] = Base64Code[c1 & 0x3f]; - break; - } - - int c2 = byteArray[off++] & 0xff; - c1 |= (c2 >> 4) & 0x0f; - destination[pos++] = Base64Code[c1 & 0x3f]; - c1 = (c2 & 0x0f) << 2; - - if (off >= length) - { - destination[pos++] = Base64Code[c1 & 0x3f]; - break; - } - - c2 = byteArray[off++] & 0xff; - c1 |= (c2 >> 6) & 0x03; - destination[pos++] = Base64Code[c1 & 0x3f]; - destination[pos++] = Base64Code[c2 & 0x3f]; - } - - charsWritten = pos; - return true; - } - - internal static Span EncodeBase64(ReadOnlySpan byteArray, int length) - { - if (length <= 0 || length > byteArray.Length) - { - throw new ArgumentException("Invalid length", nameof(length)); - } - - int encodedSize = (int)Math.Ceiling((length * 4D) / 3); - char[] encoded = new char[encodedSize]; - - int pos = 0; - int off = 0; - while (off < length) - { - //Process first byte in group - int c1 = byteArray[off++] & 0xff; - encoded[pos++] = Base64Code[(c1 >> 2) & 0x3f]; - c1 = (c1 & 0x03) << 4; - if (off >= length) - { - encoded[pos++] = Base64Code[c1 & 0x3f]; - break; - } - - // second byte of group - int c2 = byteArray[off++] & 0xff; - c1 |= (c2 >> 4) & 0x0f; - encoded[pos++] = Base64Code[c1 & 0x3f]; - c1 = (c2 & 0x0f) << 2; - if (off >= length) - { - encoded[pos++] = Base64Code[c1 & 0x3f]; - break; - } - - // third byte of group - c2 = byteArray[off++] & 0xff; - c1 |= (c2 >> 6) & 0x03; - encoded[pos++] = Base64Code[c1 & 0x3f]; - encoded[pos++] = Base64Code[c2 & 0x3f]; - } - - return encoded; - } - - /// - /// Decode a string encoded using BCrypt's base64 scheme to a byte array. - /// Note that this is *not* compatible with the standard MIME-base64 encoding. - /// - /// Thrown when one or more arguments have unsupported or - /// illegal values. - /// The string to decode. - /// - /// The decoded byte array. - public static int DecodeBase64(ReadOnlySpan encodedSpan, Span destination) - { - int outputLength = 0; - int position = 0; - - while (position < encodedSpan.Length - 1 && outputLength < destination.Length) - { - int c1 = Char64(encodedSpan[position++]); - int c2 = Char64(encodedSpan[position++]); - if (c1 == -1 || c2 == -1) break; - - destination[outputLength] = (byte)((c1 << 2) | ((c2 & 0x30) >> 4)); - if (++outputLength >= destination.Length || position >= encodedSpan.Length) break; - - int c3 = Char64(encodedSpan[position++]); - if (c3 == -1) break; - - destination[outputLength] = (byte)(((c2 & 0x0F) << 4) | ((c3 & 0x3C) >> 2)); - if (++outputLength >= destination.Length || position >= encodedSpan.Length) break; - - int c4 = Char64(encodedSpan[position++]); - if (c4 == -1) break; - - destination[outputLength] = (byte)(((c3 & 0x03) << 6) | c4); - ++outputLength; - } - - return outputLength; - } - - internal ReadOnlySpan CryptRaw(ReadOnlySpan inputBytes, ReadOnlySpan saltBytes, int workFactor, Span destination) - { - int i; - int j; - - Span cdata = stackalloc uint[BfCryptCiphertext.Length]; - BfCryptCiphertext.CopyTo(cdata); - int clen = cdata.Length; - - if (workFactor < MinRounds || workFactor > MaxRounds) - { - throw new ArgumentException("Bad number of rounds", nameof(workFactor)); - } - - if (saltBytes.Length != BCryptSaltLen) - { - throw new ArgumentException("Bad salt Length", nameof(saltBytes)); - } - - uint rounds = 1u << workFactor; - - // We overflowed rounds at 31 - added safety check - if (rounds < 1) - { - throw new ArgumentException("Bad number of rounds", nameof(workFactor)); - } - - InitializeKey(); - EKSKey(saltBytes, inputBytes); - - for (i = 0; i != rounds; i++) - { - Key(inputBytes); - Key(saltBytes); - } - - for (i = 0; i < 64; i++) - { - for (j = 0; j < (clen >> 1); j++) - { - Encipher(cdata, j << 1); - } - } - - // Convert ciphertext to output byte-array - for (i = 0, j = 0; i < clen; i++) - { - // per-line extract first byte by shifting cdata word at index right 24 bits - // using >> op then isolate the least significant byte using mask 0xff - destination[j++] = (byte)((cdata[i] >> 24) & 0xff); - destination[j++] = (byte)((cdata[i] >> 16) & 0xff); - destination[j++] = (byte)((cdata[i] >> 8) & 0xff); - destination[j++] = (byte)(cdata[i] & 0xff); - } - - return destination; - } - -#endif -} diff --git a/src/BCrypt.Net/BCryptBase.Fwk.cs b/src/BCrypt.Net/BCryptBase.Fwk.cs new file mode 100644 index 0000000..8467072 --- /dev/null +++ b/src/BCrypt.Net/BCryptBase.Fwk.cs @@ -0,0 +1,475 @@ +// /* +// The MIT License (MIT) +// Copyright (c) 2006 Damien Miller djm@mindrot.org (jBCrypt) +// Copyright (c) 2013 Ryan D. Emerle (.Net port) +// Copyright (c) 2016/2025 Chris McKee (.Net-core port / patches / new features) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +// (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, +// merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished +// to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// */ + +using System.Diagnostics; +using System.Globalization; +using System.Text; + +namespace BCryptNet; + +/// +/// .Net Framework (pre-span) implementation +/// +public partial class BCryptCore +{ + #if !NETCOREAPP + /// + /// Create Password Hash Base + /// + /// + /// + /// + /// + /// + /// + /// + /// + internal static string CreatePasswordHash(string inputKey, string salt, HashType hashType = HashType.None, Func enhancedHashKeyGen = null) + { + if (string.IsNullOrEmpty(salt)) + { + throw new ArgumentException("Invalid salt: salt cannot be null or empty", nameof(salt)); + } + + if (hashType == HashType.None && inputKey.Length > 72) + { + throw new ArgumentException("Invalid input key: input key cannot exceed 72 characters for bCrypt", nameof(inputKey)); + } + + if (enhancedHashKeyGen == null && hashType != HashType.None) + { + throw new ArgumentException("Invalid HashType, You can't have an enhanced hash without an implementation of the key generator.", nameof(hashType)); + } + + // Determine the starting offset and validate the salt + int startingOffset; + char bcryptMinorRevision = (char)0; + if (salt[0] != '$' || salt[1] != '2') + { + throw new SaltParseException("Invalid salt version"); + } + + if (salt[2] == '$') + { + startingOffset = 3; + } + else + { + bcryptMinorRevision = salt[2]; + if (bcryptMinorRevision != 'a' && bcryptMinorRevision != 'b' && bcryptMinorRevision != 'x' && + bcryptMinorRevision != 'y' || salt[3] != '$') + { + throw new SaltParseException("Invalid salt revision"); + } + + startingOffset = 4; + } + + // Extract number of rounds + if (salt[startingOffset + 2] > '$') + { + throw new SaltParseException("Missing salt rounds"); + } + + // Extract details from salt + int workFactor = Convert.ToInt16(salt.Substring(startingOffset, 2), CultureInfo.InvariantCulture); + + // Throw if log rounds are out of range on hash, deals with custom salts + if (workFactor < 1 || workFactor > 31) + { + throw new SaltParseException("Salt rounds out of range"); + } + + byte[] inputBytes; + switch (hashType) + { + case HashType.None: + inputBytes = SafeUTF8.GetBytes(inputKey + (bcryptMinorRevision >= 'a' ? Nul : EmptyString)); + break; + default: + if (enhancedHashKeyGen == null) + { + throw new ArgumentException("Invalid HashType, You can't have an enhanced hash without an implementation of the key generator.", nameof(hashType)); + } + + inputBytes = enhancedHashKeyGen(inputKey, hashType, bcryptMinorRevision); + break; + } + + + return HashBytes(inputBytes, salt.Substring(startingOffset + 3, 22), bcryptMinorRevision, workFactor); + } + + internal static string HashBytes(byte[] inputBytes, string extractedSalt, char bcryptMinorRevision, int workFactor) + { + byte[] saltBytes = DecodeBase64(extractedSalt, BCryptSaltLen); + + BCrypt bCrypt = new BCrypt(); + + byte[] hashed = bCrypt.CryptRaw(inputBytes, saltBytes, workFactor); + + // Generate result string + var result = new StringBuilder(60); + result.Append('$').Append('2').Append(bcryptMinorRevision).Append('$').Append(workFactor.ToString("D2", CultureInfo.InvariantCulture)).Append('$'); + result.Append(EncodeBase64(saltBytes, saltBytes.Length)); + result.Append(EncodeBase64(hashed, (BfCryptCiphertextLength * 4) - 1)); + + return result.ToString(); + } + + /// + /// Generate a salt for use with the method. + /// + /// The log2 of the number of rounds of hashing to apply - the work + /// factor therefore increases as 2**workFactor. + /// + /// Work factor must be between 4 and 31 + /// A base64 encoded salt value. + /// BCrypt Revision should be a, b, x or y + internal static string GenerateSalt(int workFactor = DefaultRounds, char bcryptMinorRevision = DefaultHashVersion) + { + if (workFactor < MinRounds || workFactor > MaxRounds) + { + throw new ArgumentOutOfRangeException(nameof(workFactor), workFactor, + $"The work factor must be between {MinRounds} and {MaxRounds} (inclusive)"); + } + + if (bcryptMinorRevision != 'a' && bcryptMinorRevision != 'b' && bcryptMinorRevision != 'x' && + bcryptMinorRevision != 'y') + { + throw new ArgumentException("BCrypt Revision should be a, b, x or y", nameof(bcryptMinorRevision)); + } + + byte[] saltBytes = new byte[BCryptSaltLen]; + + RngCsp.GetBytes(saltBytes); + + var result = new StringBuilder(29); + result.Append('$').Append('2').Append(bcryptMinorRevision).Append('$').Append(workFactor.ToString("D2", CultureInfo.InvariantCulture)).Append('$'); + result.Append(EncodeBase64(saltBytes, saltBytes.Length)); + + return result.ToString(); + } + + /// + /// Encode a byte array using BCrypt's slightly-modified base64 encoding scheme. Note that this + /// is *not* compatible with the standard MIME-base64 encoding. + /// + /// Thrown when one or more arguments have unsupported or + /// illegal values. + /// The byte array to encode. + /// The number of bytes to encode. + /// Base64-encoded string. + internal static char[] EncodeBase64(byte[] byteArray, int length) + { + if (length <= 0 || length > byteArray.Length) + { + throw new ArgumentException("Invalid length", nameof(length)); + } + + int encodedSize = (int)Math.Ceiling((length * 4D) / 3); + char[] encoded = new char[encodedSize]; + + int pos = 0; + int off = 0; + while (off < length) + { + //Process first byte in group + int c1 = byteArray[off++] & 0xff; + encoded[pos++] = Base64Code[(c1 >> 2) & 0x3f]; + c1 = (c1 & 0x03) << 4; + if (off >= length) + { + encoded[pos++] = Base64Code[c1 & 0x3f]; + break; + } + + // second byte of group + int c2 = byteArray[off++] & 0xff; + c1 |= (c2 >> 4) & 0x0f; + encoded[pos++] = Base64Code[c1 & 0x3f]; + c1 = (c2 & 0x0f) << 2; + if (off >= length) + { + encoded[pos++] = Base64Code[c1 & 0x3f]; + break; + } + + // third byte of group + c2 = byteArray[off++] & 0xff; + c1 |= (c2 >> 6) & 0x03; + encoded[pos++] = Base64Code[c1 & 0x3f]; + encoded[pos++] = Base64Code[c2 & 0x3f]; + } + + return encoded; + } + + /// + /// Decode a string encoded using BCrypt's base64 scheme to a byte array. + /// Note that this is *not* compatible with the standard MIME-base64 encoding. + /// + /// Thrown when one or more arguments have unsupported or + /// illegal values. + /// The string to decode. + /// The maximum bytes to decode. + /// The decoded byte array. + internal static byte[] DecodeBase64(string encodedString, int maximumBytes) + { + int sourceLength = encodedString.Length; + int outputLength = 0; + + if (maximumBytes <= 0) + { + throw new ArgumentException("Invalid maximum bytes value", nameof(maximumBytes)); + } + + byte[] result = new byte[maximumBytes]; + + int position = 0; + while (position < sourceLength - 1 && outputLength < maximumBytes) + { + int c1 = Char64(encodedString[position++]); + int c2 = Char64(encodedString[position++]); + if (c1 == -1 || c2 == -1) + { + break; + } + + result[outputLength] = (byte)((c1 << 2) | ((c2 & 0x30) >> 4)); + if (++outputLength >= maximumBytes || position >= sourceLength) + { + break; + } + + int c3 = Char64(encodedString[position++]); + if (c3 == -1) + { + break; + } + + result[outputLength] = (byte)(((c2 & 0x0f) << 4) | ((c3 & 0x3c) >> 2)); + if (++outputLength >= maximumBytes || position >= sourceLength) + { + break; + } + + int c4 = Char64(encodedString[position++]); + result[outputLength] = (byte)(((c3 & 0x03) << 6) | c4); + + ++outputLength; + } + + return result; + } + + /// Blowfish encipher a single 64-bit block encoded as two 32-bit halves. + /// An array containing the two 32-bit half blocks. The plaintext to be encrypted + /// The position in the array of the blocks. + private void Encipher(uint[] blockArray, int offset) + { + uint block = blockArray[offset]; + uint r = blockArray[offset + 1]; + + block ^= _p[0]; + + unchecked + { + uint round; + for (round = 0; round <= BlowfishNumRounds - 2;) + { + // Feistel substitution on left word + uint n = _s[(block >> 24) & 0xff]; + n += _s[0x100 | ((block >> 16) & 0xff)]; + n ^= _s[0x200 | ((block >> 8) & 0xff)]; + n += _s[0x300 | (block & 0xff)]; + r ^= n ^ _p[++round]; + + // Feistel substitution on right word + n = _s[(r >> 24) & 0xff]; + n += _s[0x100 | ((r >> 16) & 0xff)]; + n ^= _s[0x200 | ((r >> 8) & 0xff)]; + n += _s[0x300 | (r & 0xff)]; + block ^= n ^ _p[++round]; + } + + blockArray[offset] = r ^ _p[BlowfishNumRounds + 1]; + blockArray[offset + 1] = block; + } + } + + /// Cyclically extract a word of key material. + /// The string to extract the data from. + /// [in, out] The current offset. + /// The next word of material from data. + private static uint StreamToWord(byte[] data, ref int offset) + { + int i; + uint word = 0; + + for (i = 0; i < 4; i++) + { + word = (word << 8) | (uint)(data[offset] & 0xff); + offset = (offset + 1) % data.Length; + } + + return word; + } + + /// Key the Blowfish cipher. + /// The key byte array. + private void Key(byte[] keyBytes) + { + int i; + int kOfP = 0; + + uint[] lr = { 0, 0 }; + int pLen = _p.Length, sLen = _s.Length; + + for (i = 0; i < pLen; i++) + { + _p[i] = _p[i] ^ StreamToWord(keyBytes, ref kOfP); + } + + for (i = 0; i < pLen; i += 2) + { + Encipher(lr, 0); + _p[i] = lr[0]; + _p[i + 1] = lr[1]; + } + + for (i = 0; i < sLen; i += 2) + { + Encipher(lr, 0); + _s[i] = lr[0]; + _s[i + 1] = lr[1]; + } + } + + /// + /// Perform the "enhanced key schedule" step described by Provos and Mazieres in + /// "A Future Adaptable Password Scheme" http://www.openbsd.org/papers/bcrypt-paper.ps. + /// + /// Salt byte array. + /// Input byte array. + // ReSharper disable once InconsistentNaming + private void EKSKey(byte[] saltBytes, byte[] inputBytes) + + { + int i; + int passwordOffset = 0; + int saltOffset = 0; + + uint[] lr = { 0, 0 }; + int pLen = _p.Length, sLen = _s.Length; + + for (i = 0; i < pLen; i++) + { + _p[i] = _p[i] ^ StreamToWord(inputBytes, ref passwordOffset); + } + + for (i = 0; i < pLen; i += 2) + { + lr[0] ^= StreamToWord(saltBytes, ref saltOffset); + lr[1] ^= StreamToWord(saltBytes, ref saltOffset); + Encipher(lr, 0); + _p[i] = lr[0]; + _p[i + 1] = lr[1]; + } + + for (i = 0; i < sLen; i += 2) + { + lr[0] ^= StreamToWord(saltBytes, ref saltOffset); + lr[1] ^= StreamToWord(saltBytes, ref saltOffset); + Encipher(lr, 0); + _s[i] = lr[0]; + _s[i + 1] = lr[1]; + } + } + + /// Perform the central hashing step in the BCrypt scheme. + /// Thrown when one or more arguments have unsupported or + /// illegal values. + /// The input byte array to hash. + /// The salt byte array to hash with. + /// The binary logarithm of the number of rounds of hashing to apply. + /// A byte array containing the hashed result. + internal byte[] CryptRaw(byte[] inputBytes, byte[] saltBytes, int workFactor) + + { + int i; + int j; + + uint[] cdata = new uint[BfCryptCiphertext.Length]; + Array.Copy(BfCryptCiphertext, cdata, BfCryptCiphertext.Length); + + int clen = cdata.Length; + + if (workFactor < MinRounds || workFactor > MaxRounds) + { + throw new ArgumentException("Bad number of rounds", nameof(workFactor)); + } + + if (saltBytes.Length != BCryptSaltLen) + { + throw new ArgumentException("Bad salt Length", nameof(saltBytes)); + } + + uint rounds = 1u << workFactor; + + // We overflowed rounds at 31 - added safety check + if (rounds < 1) + { + throw new ArgumentException("Bad number of rounds", nameof(workFactor)); + } + + InitializeKey(); + EKSKey(saltBytes, inputBytes); + + for (i = 0; i != rounds; i++) + { + Key(inputBytes); + Key(saltBytes); + } + + for (i = 0; i < 64; i++) + { + for (j = 0; j < (clen >> 1); j++) + { + Encipher(cdata, j << 1); + } + } + + // Convert ciphertext to output byte-array + byte[] ret = new byte[clen * 4]; + for (i = 0, j = 0; i < clen; i++) + { + // per-line extract first byte by shifting cdata word at index right 24 bits + // using >> op then isolate the least significant byte using mask 0xff + ret[j++] = (byte)((cdata[i] >> 24) & 0xff); + ret[j++] = (byte)((cdata[i] >> 16) & 0xff); + ret[j++] = (byte)((cdata[i] >> 8) & 0xff); + ret[j++] = (byte)(cdata[i] & 0xff); + } + + return ret; + } +#endif + +} diff --git a/src/BCrypt.Net/BCryptBase.cs b/src/BCrypt.Net/BCryptBase.cs index 116c5e3..4d61896 100644 --- a/src/BCrypt.Net/BCryptBase.cs +++ b/src/BCrypt.Net/BCryptBase.cs @@ -212,23 +212,88 @@ public partial class BCryptCore private uint[] _p; private uint[] _s; -#if !NETCOREAPP + // Compares two byte arrays for equality. The method is specifically written so that the loop is not optimised. + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + internal static bool SecureEquals(byte[] a, byte[] b) + { + if (a == null && b == null) + { + return true; + } + + if (a == null || b == null || a.Length != b.Length) + { + return false; + } + + int diff = 0; + for (var i = 0; i < a.Length; i++) + { + diff |= (a[i] ^ b[i]); + } + + return diff == 0; + } + + /// + /// Look up the 3 bits base64-encoded by the specified character, range-checking against + /// conversion table. + /// + /// The base64-encoded value. + /// The decoded value of x. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Char64(char character) + { + return character < 0 || character > Index64.Length ? -1 : Index64[character]; + } + + /// Initializes the Blowfish key schedule. + private void InitializeKey() + { + _p = new uint[POrig.Length]; + _s = new uint[SOrig.Length]; + Array.Copy(POrig, _p, POrig.Length); + Array.Copy(SOrig, _s, SOrig.Length); + } + +#if NETCOREAPP + /// + /// Create Password Hash Base + /// + /// + /// + /// + /// + /// + /// + /// + /// + internal static string CreatePasswordHash(ReadOnlySpan inputKey, ReadOnlySpan salt, HashType hashType = HashType.None, Func enhancedHashKeyGen = null) + { + Span outputBuffer = stackalloc char[60]; + CreatePasswordHash(inputKey, salt, outputBuffer, out var outputBufferWritten, hashType, enhancedHashKeyGen); + return new string(outputBuffer[..outputBufferWritten]); + } + /// /// Create Password Hash Base /// /// /// + /// + /// /// /// /// /// /// /// - internal static string CreatePasswordHash(string inputKey, string salt, HashType hashType = HashType.None, Func enhancedHashKeyGen = null) + internal static void CreatePasswordHash(ReadOnlySpan inputKey, ReadOnlySpan salt, Span outputBuffer, out int outputBufferWritten, HashType hashType = HashType.None, + Func enhancedHashKeyGen = null) { - if (string.IsNullOrEmpty(salt)) + if (salt.IsEmpty) { - throw new ArgumentException("Invalid salt: salt cannot be null or empty", nameof(salt)); + throw new ArgumentException("Invalid salt: salt cannot be empty", nameof(salt)); } if (hashType == HashType.None && inputKey.Length > 72) @@ -241,9 +306,15 @@ internal static string CreatePasswordHash(string inputKey, string salt, HashType throw new ArgumentException("Invalid HashType, You can't have an enhanced hash without an implementation of the key generator.", nameof(hashType)); } + if (outputBuffer.Length != 60) + { + throw new ArgumentException("Output buffer must be 60 characters long", nameof(outputBuffer)); + } + // Determine the starting offset and validate the salt int startingOffset; char bcryptMinorRevision = (char)0; + if (salt[0] != '$' || salt[1] != '2') { throw new SaltParseException("Invalid salt version"); @@ -266,71 +337,102 @@ internal static string CreatePasswordHash(string inputKey, string salt, HashType } // Extract number of rounds - if (salt[startingOffset + 2] > '$') + // Extract details from salt + if (!int.TryParse(salt.Slice(startingOffset, 2), NumberStyles.None, CultureInfo.InvariantCulture, out int workFactor)) { throw new SaltParseException("Missing salt rounds"); } - // Extract details from salt - int workFactor = Convert.ToInt16(salt.Substring(startingOffset, 2), CultureInfo.InvariantCulture); - // Throw if log rounds are out of range on hash, deals with custom salts if (workFactor < 1 || workFactor > 31) { throw new SaltParseException("Salt rounds out of range"); } - byte[] inputBytes; + Span inputBytes; switch (hashType) { case HashType.None: - inputBytes = SafeUTF8.GetBytes(inputKey + (bcryptMinorRevision >= 'a' ? Nul : EmptyString)); + bool appendNul = bcryptMinorRevision >= 'a'; + Span utf8Buffer = stackalloc byte[Encoding.UTF8.GetMaxByteCount(inputKey.Length + (appendNul ? 1 : 0))]; + int bytesWritten = Encoding.UTF8.GetBytes(inputKey, utf8Buffer); + if (appendNul) utf8Buffer[bytesWritten++] = 0; + inputBytes = utf8Buffer[..bytesWritten].ToArray(); break; default: if (enhancedHashKeyGen == null) { throw new ArgumentException("Invalid HashType, You can't have an enhanced hash without an implementation of the key generator.", nameof(hashType)); } - inputBytes = enhancedHashKeyGen(inputKey, hashType, bcryptMinorRevision); + + inputBytes = enhancedHashKeyGen(new string(inputKey), hashType, bcryptMinorRevision); break; } + if (!HashBytes(inputBytes, salt.Slice(startingOffset + 3, 22), bcryptMinorRevision, workFactor, outputBuffer, out int written)) + throw new BcryptAuthenticationException("Couldn't hash input"); - return HashBytes(inputBytes, salt.Substring(startingOffset + 3, 22), bcryptMinorRevision, workFactor); - + outputBufferWritten = written; } -#endif -#if !NETCOREAPP - internal static string HashBytes(byte[] inputBytes, string extractedSalt, char bcryptMinorRevision, int workFactor) + /// + /// + /// + /// + /// + /// + /// + /// + internal static bool HashBytes( + ReadOnlySpan inputBytes, + ReadOnlySpan extractedSalt, + char bcryptMinorRevision, + int workFactor, + Span destination, + out int charsWritten) { - byte[] saltBytes = DecodeBase64(extractedSalt, BCryptSaltLen); + charsWritten = 0; + var bCrypt = new BCrypt(); - BCrypt bCrypt = new BCrypt(); + Span saltBuffer = stackalloc byte[BCryptSaltLen]; + int written = DecodeBase64(extractedSalt, saltBuffer); + var saltBytes = saltBuffer[..written]; - byte[] hashed = bCrypt.CryptRaw(inputBytes, saltBytes, workFactor); + Span hashBuffer = stackalloc byte[BfCryptCiphertext.Length * 4]; + var hashBytes = bCrypt.CryptRaw(inputBytes, saltBytes, workFactor, hashBuffer); - // Generate result string - var result = new StringBuilder(60); - result.Append('$').Append('2').Append(bcryptMinorRevision).Append('$').Append(workFactor.ToString("D2", CultureInfo.InvariantCulture)).Append('$'); - result.Append(EncodeBase64(saltBytes, saltBytes.Length)); - result.Append(EncodeBase64(hashed, (BfCryptCiphertextLength * 4) - 1)); + // Ensure the destination is large enough + // "$2x$10$" + base64(16 bytes) + base64(23 bytes) = 60 characters + if (destination.Length < 60) + return false; - return result.ToString(); - } -#endif + int pos = 0; + destination[pos++] = '$'; + destination[pos++] = '2'; + destination[pos++] = bcryptMinorRevision; + destination[pos++] = '$'; + // Write work factor as 2-digit number + if (!workFactor.TryFormat(destination.Slice(pos), out int wfChars, "D2")) + return false; + pos += wfChars; + + destination[pos++] = '$'; + + // Write base64-encoded salt + if (!TryEncodeBase64(saltBytes, saltBytes.Length, destination.Slice(pos), out int saltChars)) + return false; + pos += saltChars; + + // Write base64-encoded hash + if (!TryEncodeBase64(hashBytes, (BfCryptCiphertextLength * 4) - 1, destination.Slice(pos), out int hashChars)) + return false; + pos += hashChars; + + charsWritten = pos; + return true; + } -#if !NETCOREAPP - /// - /// Generate a salt for use with the method. - /// - /// The log2 of the number of rounds of hashing to apply - the work - /// factor therefore increases as 2**workFactor. - /// - /// Work factor must be between 4 and 31 - /// A base64 encoded salt value. - /// BCrypt Revision should be a, b, x or y internal static string GenerateSalt(int workFactor = DefaultRounds, char bcryptMinorRevision = DefaultHashVersion) { if (workFactor < MinRounds || workFactor > MaxRounds) @@ -350,47 +452,61 @@ internal static string GenerateSalt(int workFactor = DefaultRounds, char bcryptM RngCsp.GetBytes(saltBytes); var result = new StringBuilder(29); - result.Append("$2").Append(bcryptMinorRevision).Append('$').Append(workFactor.ToString("D2")).Append('$'); + result.Append('$').Append('2').Append(bcryptMinorRevision).Append('$').Append(workFactor.ToString("D2", CultureInfo.InvariantCulture)).Append('$'); + + // Base65 encoded salt result.Append(EncodeBase64(saltBytes, saltBytes.Length)); return result.ToString(); } -#endif - // Compares two byte arrays for equality. The method is specifically written so that the loop is not optimised. - [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] - internal static bool SecureEquals(byte[] a, byte[] b) + internal static bool TryEncodeBase64(ReadOnlySpan byteArray, int length, Span destination, out int charsWritten) { - if (a == null && b == null) - { - return true; - } + charsWritten = 0; - if (a == null || b == null || a.Length != b.Length) - { + if (length <= 0 || length > byteArray.Length) return false; - } - int diff = 0; - for (var i = 0; i < a.Length; i++) + int encodedSize = (int)Math.Ceiling((length * 4D) / 3); + if (destination.Length < encodedSize) + return false; + + int pos = 0; + int off = 0; + while (off < length) { - diff |= (a[i] ^ b[i]); + int c1 = byteArray[off++] & 0xff; + destination[pos++] = Base64Code[(c1 >> 2) & 0x3f]; + c1 = (c1 & 0x03) << 4; + + if (off >= length) + { + destination[pos++] = Base64Code[c1 & 0x3f]; + break; + } + + int c2 = byteArray[off++] & 0xff; + c1 |= (c2 >> 4) & 0x0f; + destination[pos++] = Base64Code[c1 & 0x3f]; + c1 = (c2 & 0x0f) << 2; + + if (off >= length) + { + destination[pos++] = Base64Code[c1 & 0x3f]; + break; + } + + c2 = byteArray[off++] & 0xff; + c1 |= (c2 >> 6) & 0x03; + destination[pos++] = Base64Code[c1 & 0x3f]; + destination[pos++] = Base64Code[c2 & 0x3f]; } - return diff == 0; + charsWritten = pos; + return true; } - #if !NETCOREAPP - /// - /// Encode a byte array using BCrypt's slightly-modified base64 encoding scheme. Note that this - /// is *not* compatible with the standard MIME-base64 encoding. - /// - /// Thrown when one or more arguments have unsupported or - /// illegal values. - /// The byte array to encode. - /// The number of bytes to encode. - /// Base64-encoded string. - internal static char[] EncodeBase64(byte[] byteArray, int length) + internal static Span EncodeBase64(ReadOnlySpan byteArray, int length) { if (length <= 0 || length > byteArray.Length) { @@ -434,10 +550,7 @@ internal static char[] EncodeBase64(byte[] byteArray, int length) return encoded; } - #endif - -#if !NETCOREAPP /// /// Decode a string encoded using BCrypt's base64 scheme to a byte array. /// Note that this is *not* compatible with the standard MIME-base64 encoding. @@ -445,78 +558,100 @@ internal static char[] EncodeBase64(byte[] byteArray, int length) /// Thrown when one or more arguments have unsupported or /// illegal values. /// The string to decode. - /// The maximum bytes to decode. + /// /// The decoded byte array. - internal static byte[] DecodeBase64(string encodedString, int maximumBytes) + public static int DecodeBase64(ReadOnlySpan encodedSpan, Span destination) { - int sourceLength = encodedString.Length; int outputLength = 0; + int position = 0; + + while (position < encodedSpan.Length - 1 && outputLength < destination.Length) + { + int c1 = Char64(encodedSpan[position++]); + int c2 = Char64(encodedSpan[position++]); + if (c1 == -1 || c2 == -1) break; + + destination[outputLength] = (byte)((c1 << 2) | ((c2 & 0x30) >> 4)); + if (++outputLength >= destination.Length || position >= encodedSpan.Length) break; + + int c3 = Char64(encodedSpan[position++]); + if (c3 == -1) break; + + destination[outputLength] = (byte)(((c2 & 0x0F) << 4) | ((c3 & 0x3C) >> 2)); + if (++outputLength >= destination.Length || position >= encodedSpan.Length) break; - if (maximumBytes <= 0) + int c4 = Char64(encodedSpan[position++]); + if (c4 == -1) break; + + destination[outputLength] = (byte)(((c3 & 0x03) << 6) | c4); + ++outputLength; + } + + return outputLength; + } + + internal ReadOnlySpan CryptRaw(ReadOnlySpan inputBytes, ReadOnlySpan saltBytes, int workFactor, Span destination) + { + int i; + int j; + + Span cdata = stackalloc uint[BfCryptCiphertext.Length]; + BfCryptCiphertext.CopyTo(cdata); + int clen = cdata.Length; + + if (workFactor < MinRounds || workFactor > MaxRounds) { - throw new ArgumentException("Invalid maximum bytes value", nameof(maximumBytes)); + throw new ArgumentException("Bad number of rounds", nameof(workFactor)); } - byte[] result = new byte[maximumBytes]; + if (saltBytes.Length != BCryptSaltLen) + { + throw new ArgumentException("Bad salt Length", nameof(saltBytes)); + } - int position = 0; - while (position < sourceLength - 1 && outputLength < maximumBytes) + uint rounds = 1u << workFactor; + + // We overflowed rounds at 31 - added safety check + if (rounds < 1) { - int c1 = Char64(encodedString[position++]); - int c2 = Char64(encodedString[position++]); - if (c1 == -1 || c2 == -1) - { - break; - } + throw new ArgumentException("Bad number of rounds", nameof(workFactor)); + } - result[outputLength] = (byte)((c1 << 2) | ((c2 & 0x30) >> 4)); - if (++outputLength >= maximumBytes || position >= sourceLength) - { - break; - } + InitializeKey(); + EKSKey(saltBytes, inputBytes); - int c3 = Char64(encodedString[position++]); - if (c3 == -1) - { - break; - } + for (i = 0; i != rounds; i++) + { + Key(inputBytes); + Key(saltBytes); + } - result[outputLength] = (byte)(((c2 & 0x0f) << 4) | ((c3 & 0x3c) >> 2)); - if (++outputLength >= maximumBytes || position >= sourceLength) + for (i = 0; i < 64; i++) + { + for (j = 0; j < (clen >> 1); j++) { - break; + Encipher(cdata, j << 1); } - - int c4 = Char64(encodedString[position++]); - result[outputLength] = (byte)(((c3 & 0x03) << 6) | c4); - - ++outputLength; } - return result; - } -#endif + // Convert ciphertext to output byte-array + for (i = 0, j = 0; i < clen; i++) + { + // per-line extract first byte by shifting cdata word at index right 24 bits + // using >> op then isolate the least significant byte using mask 0xff + destination[j++] = (byte)((cdata[i] >> 24) & 0xff); + destination[j++] = (byte)((cdata[i] >> 16) & 0xff); + destination[j++] = (byte)((cdata[i] >> 8) & 0xff); + destination[j++] = (byte)(cdata[i] & 0xff); + } - /// - /// Look up the 3 bits base64-encoded by the specified character, range-checking against - /// conversion table. - /// - /// The base64-encoded value. - /// The decoded value of x. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int Char64(char character) - { - return character < 0 || character > Index64.Length ? -1 : Index64[character]; + return destination; } /// Blowfish encipher a single 64-bit block encoded as two 32-bit halves. /// An array containing the two 32-bit half blocks. The plaintext to be encrypted /// The position in the array of the blocks. -#if NETCOREAPP private void Encipher(Span blockArray, int offset) -#else - private void Encipher(uint[] blockArray, int offset) -#endif { uint block = blockArray[offset]; uint r = blockArray[offset + 1]; @@ -552,11 +687,7 @@ private void Encipher(uint[] blockArray, int offset) /// The string to extract the data from. /// [in, out] The current offset. /// The next word of material from data. -#if NETCOREAPP private static uint StreamToWord(ReadOnlySpan data, ref int offset) -#else - private static uint StreamToWord(byte[] data, ref int offset) -#endif { int i; uint word = 0; @@ -570,30 +701,14 @@ private static uint StreamToWord(byte[] data, ref int offset) return word; } - /// Initializes the Blowfish key schedule. - private void InitializeKey() - { - _p = new uint[POrig.Length]; - _s = new uint[SOrig.Length]; - Array.Copy(POrig, _p, POrig.Length); - Array.Copy(SOrig, _s, SOrig.Length); - } - /// Key the Blowfish cipher. /// The key byte array. -#if NETCOREAPP private void Key(ReadOnlySpan keyBytes) -#else - private void Key(byte[] keyBytes) -#endif { int i; int kOfP = 0; -#if NETCOREAPP Span lr = stackalloc uint[2] { 0, 0 }; -#else - uint[] lr = { 0, 0 }; -#endif + int pLen = _p.Length, sLen = _s.Length; for (i = 0; i < pLen; i++) @@ -623,20 +738,14 @@ private void Key(byte[] keyBytes) /// Salt byte array. /// Input byte array. // ReSharper disable once InconsistentNaming -#if NETCOREAPP private void EKSKey(ReadOnlySpan saltBytes, ReadOnlySpan inputBytes) -#else - private void EKSKey(byte[] saltBytes, byte[] inputBytes) -#endif { int i; int passwordOffset = 0; int saltOffset = 0; -#if NETCOREAPP + Span lr = stackalloc uint[2] { 0, 0 }; -#else - uint[] lr = { 0, 0 }; -#endif + int pLen = _p.Length, sLen = _s.Length; for (i = 0; i < pLen; i++) @@ -663,73 +772,5 @@ private void EKSKey(byte[] saltBytes, byte[] inputBytes) } } -#if !NETCOREAPP - /// Perform the central hashing step in the BCrypt scheme. - /// Thrown when one or more arguments have unsupported or - /// illegal values. - /// The input byte array to hash. - /// The salt byte array to hash with. - /// The binary logarithm of the number of rounds of hashing to apply. - /// A byte array containing the hashed result. - internal byte[] CryptRaw(byte[] inputBytes, byte[] saltBytes, int workFactor) - - { - int i; - int j; - - uint[] cdata = new uint[BfCryptCiphertext.Length]; - Array.Copy(BfCryptCiphertext, cdata, BfCryptCiphertext.Length); - - int clen = cdata.Length; - - if (workFactor < MinRounds || workFactor > MaxRounds) - { - throw new ArgumentException("Bad number of rounds", nameof(workFactor)); - } - - if (saltBytes.Length != BCryptSaltLen) - { - throw new ArgumentException("Bad salt Length", nameof(saltBytes)); - } - - uint rounds = 1u << workFactor; - - // We overflowed rounds at 31 - added safety check - if (rounds < 1) - { - throw new ArgumentException("Bad number of rounds", nameof(workFactor)); - } - - InitializeKey(); - EKSKey(saltBytes, inputBytes); - - for (i = 0; i != rounds; i++) - { - Key(inputBytes); - Key(saltBytes); - } - - for (i = 0; i < 64; i++) - { - for (j = 0; j < (clen >> 1); j++) - { - Encipher(cdata, j << 1); - } - } - - // Convert ciphertext to output byte-array - byte[] ret = new byte[clen * 4]; - for (i = 0, j = 0; i < clen; i++) - { - // per-line extract first byte by shifting cdata word at index right 24 bits - // using >> op then isolate the least significant byte using mask 0xff - ret[j++] = (byte)((cdata[i] >> 24) & 0xff); - ret[j++] = (byte)((cdata[i] >> 16) & 0xff); - ret[j++] = (byte)((cdata[i] >> 8) & 0xff); - ret[j++] = (byte)(cdata[i] & 0xff); - } - - return ret; - } - #endif +#endif } From c07bce0ab39bfaba4e07217c34f22cc7073a09a4 Mon Sep 17 00:00:00 2001 From: Chris McKee Date: Tue, 1 Apr 2025 18:33:55 +0100 Subject: [PATCH 04/27] fix: xmldoc 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 --- .gitignore | 2 + benchmark/4.0.0/version4.cs | 2 +- benchmark/4.0.3/BCrypt403.cs | 1230 +++++++++++++++++ benchmark/BCryptNet.BenchMarks.csproj | 3 +- benchmark/EncodeB64/Encoder.cs | 92 +- benchmark/Program.cs | 9 +- benchmark/TestB64Encoder.cs | 12 + benchmark/TestBcrypt_HashInterrogation.cs | 11 +- benchmark/TestBcrypt_Hashing.cs | 64 +- benchmark/TestBcrypt_Hashing_Enhanced.cs | 20 +- benchmark/TestVariantsOnStringBuilding.cs | 7 +- benchmark/readme.md | 4 +- .../BCryptPasswordHasher.cs | 3 +- .../StringUtils.cs | 8 +- src/BCrypt.Net/BCrypt.cs | 26 +- src/BCrypt.Net/BCryptBase.Fwk.cs | 1 - src/BCrypt.Net/BCryptBase.cs | 6 +- src/BCrypt.Net/BCryptExtendedV3.cs | 9 +- src/BCrypt.Net/HashParser.cs | 5 +- tests/UnitTests/BCrypt.Net.UnitTests.csproj | 2 +- tests/UnitTests/BCryptTests.cs | 3 +- tests/UnitTests/BCryptTestsExtendedv3.cs | 2 +- 22 files changed, 1445 insertions(+), 76 deletions(-) create mode 100644 benchmark/4.0.3/BCrypt403.cs diff --git a/.gitignore b/.gitignore index 94e51cc..69ac0f9 100644 --- a/.gitignore +++ b/.gitignore @@ -266,3 +266,5 @@ __pycache__/ /src/Benchmark/BenchmarkDotNet.Artifacts/results/* /docs/_site/ /assets/*.nupkg + +/benchmark/BenchmarkDotNet.Artifacts diff --git a/benchmark/4.0.0/version4.cs b/benchmark/4.0.0/version4.cs index 49ccc9d..d409842 100644 --- a/benchmark/4.0.0/version4.cs +++ b/benchmark/4.0.0/version4.cs @@ -6,7 +6,7 @@ // BASE namespace BCryptNet.BenchMarks._4._0._0 { - public class version4 + internal class BCryptV4 { /// BCrypt implementation. /// diff --git a/benchmark/4.0.3/BCrypt403.cs b/benchmark/4.0.3/BCrypt403.cs new file mode 100644 index 0000000..96b8a6b --- /dev/null +++ b/benchmark/4.0.3/BCrypt403.cs @@ -0,0 +1,1230 @@ +// /* +// The MIT License (MIT) +// Copyright (c) 2006 Damien Miller djm@mindrot.org (jBCrypt) +// Copyright (c) 2013 Ryan D. Emerle (.Net port) +// Copyright (c) 2016/2025 Chris McKee (.Net-core port / patches / new features) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +// (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, +// merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished +// to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// */ + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using System.Text; + +namespace BCryptNet.BenchMarks._4._0._3 +{ + internal class BCryptV403 + { + /// BCrypt implementation. + /// + /// + /// BCrypt implements OpenBSD-style Blowfish password hashing using the scheme described in + /// "A Future- + /// Adaptable Password Scheme" by Niels Provos and David Mazieres. + /// + /// + /// This password hashing system tries to thwart off-line password cracking using a + /// computationally-intensive hashing algorithm, based on Bruce Schneier's Blowfish cipher. + /// The work factor of the algorithm is parameterised, so it can be increased as computers + /// get faster. + /// + /// + /// To hash a password using the defaults, call the (which will generate a random salt and hash at default cost), like this: + /// + /// string pw_hash = BCrypt.HashPassword(plain_password); + /// + /// To hash a password using SHA384 pre-hashing for increased entropy call + /// (which will generate a random salt and hash at default cost), like this: + /// + /// string pw_hash = BCrypt.EnhancedHashPassword(plain_password); + /// + /// To check whether a plaintext password matches one that has been hashed previously, + /// use the method: + /// (To validate an enhanced hash you can pass true as the last parameter of Verify or use ) + /// + /// + /// if (BCrypt.Verify(candidate_password, stored_hash)) + /// Console.WriteLine("It matches"); + /// else + /// Console.WriteLine("It does not match"); + /// + /// + /// The method takes an optional parameter (workFactor) that + /// determines the computational complexity of the hashing: + /// + /// + /// string strong_salt = BCrypt.GenerateSalt(10); + /// string stronger_salt = BCrypt.GenerateSalt(12); + /// + /// + /// The amount of work increases exponentially (2^workFactor), so each increment is twice + /// as much work. The default workFactor is 10, and the valid range is 4 to 31. + /// + /// + internal sealed class BCrypt + { + // BCrypt parameters + /// + /// Default Work Factor + /// + private const int DefaultRounds = 11; + + private const int BCryptSaltLen = 128 / 8; // 128 bits + + private static readonly Encoding SafeUTF8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); + + private const HashType DefaultEnhancedHashType = HashType.SHA384; + + // Blowfish parameters + private const int BlowfishNumRounds = 16; + + /// + /// RandomNumberGenerator.Create calls RandomNumberGenerator.Create("System.Security.Cryptography.RandomNumberGenerator"), which will create an instance of RNGCryptoServiceProvider. + /// https://msdn.microsoft.com/en-us/library/42ks8fz1 + /// + private static readonly RandomNumberGenerator RngCsp = RandomNumberGenerator.Create(); // secure PRNG + + #region Initial contents of key schedule + + private static readonly uint[] POrig = + { + 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, 0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89, 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, 0x9216d5d9, 0x8979fb1b + }; + + private static readonly uint[] SOrig = + { + 0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, 0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99, 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e, 0x0d95748f, 0x728eb658, + 0x718bcd58, 0x82154aee, 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, 0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e, 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, + 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, 0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce, 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, 0xafd6ba33, 0x6c24cf5c, + 0x7a325381, 0x28958677, 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, 0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88, 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, + 0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, 0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0, 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, 0xa1f1651d, 0x39af0176, + 0x66ca593e, 0x82430e88, 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, 0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d, 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, + 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, 0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba, 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463, 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, 0x6dfc511f, 0x9b30952c, + 0xcc814544, 0xaf5ebd09, 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, 0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279, 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, + 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, 0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82, 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, 0x695b27b0, 0xbbca58c8, + 0xe1ffa35d, 0xb8f011a0, 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, 0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8, 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, + 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, 0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7, 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, 0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, 0x2f2f2218, 0xbe0e1777, + 0xea752dfe, 0x8b021fa1, 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, 0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477, 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, + 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, 0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af, 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, 0x83260376, 0x6295cfa9, + 0x11c81968, 0x4e734a41, 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915, 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, 0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915, 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, + 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a, 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266, 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, 0x193602a5, 0x75094c29, + 0xa0591340, 0xe4183a3e, 0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6, 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, 0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, 0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1, + 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, 0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff, 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, 0xd19113f9, 0x7ca92ff6, + 0x94324773, 0x22f54701, 0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7, 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, 0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf, + 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, 0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87, 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, 0xec7aec3a, 0xdb851dfa, + 0x63094366, 0xc464c3d2, 0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16, 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, 0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509, + 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, 0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f, 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a, 0xc6150eba, 0x94e2ea78, + 0xa5fc3c53, 0x1e0a2df4, 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960, 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, 0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802, + 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, 0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, 0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf, 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, 0xeecc86bc, 0x60622ca7, + 0x9cab5cab, 0xb2f3846e, 0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50, 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, 0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281, + 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, 0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128, 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, 0x5d4a14d9, 0xe864b7e3, + 0x42105d14, 0x203e13e0, 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0, 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250, 0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3, + 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, 0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061, 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, 0x7cde3759, 0xcbee7460, + 0x4085f2a7, 0xce77326e, 0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735, 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc, 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340, + 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7, 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068, 0xd4082471, 0x3320f46a, + 0x43b7d4b7, 0x500061af, 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, 0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45, 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, + 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb, 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, 0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42, 0x20fe9e35, 0xd9f385b9, + 0xee39d7ab, 0x3b124e8b, 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, 0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb, 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, + 0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33, 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c, 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, 0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc, 0x07f9c9ee, 0x41041f0f, + 0x404779a4, 0x5d886e17, 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, 0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b, 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, + 0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728, 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, 0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37, 0xa812dc60, 0xa1ebddf8, + 0x991be14c, 0xdb6e6b0d, 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b, 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, + 0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d, 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, 0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9, 0x44421659, 0x0a121386, + 0xd90cec6e, 0xd5abea2a, 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, 0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d, 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, + 0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61, 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, 0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2, 0x466e598e, 0x20b45770, + 0x8cd55591, 0xc902de4c, 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, 0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633, 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, + 0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52, 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, 0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62, 0x11e69ed7, 0x2338ea63, + 0x53c2dd94, 0xc2c21634, 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, 0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24, 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, + 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c, 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0, 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, 0x5cb0679e, 0x4fa33742, + 0xd3822740, 0x99bc9bbe, 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, 0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8, 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, + 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, 0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22, 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, 0x2826a2f9, 0xa73a3ae1, + 0x4ba99586, 0xef5562e9, 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, 0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51, 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, + 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, 0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b, 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28, 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, 0x15056dd4, 0x88f46dba, + 0x03a16125, 0x0564f0bd, 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, 0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb, 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, + 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, 0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32, 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, 0xb39a460a, 0x6445c0dd, + 0x586cdecf, 0x1c20c8ae, 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, 0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47, 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, + 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, 0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84, 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, 0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, 0x611560b1, 0xe7933fdc, + 0xbb3a792b, 0x344525bd, 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, 0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38, 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, + 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, 0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525, 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, 0xe0ec6e0e, 0x1698db3b, + 0x4c98a0be, 0x3278e964, 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e, 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, 0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d, 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, + 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, 0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02, 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, 0xe6c6c7bd, 0x327a140a, + 0x45e1d006, 0xc3f27b9a, 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, 0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0, 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, + 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, 0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9, 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6 + }; + + #endregion Initial contents of key schedule + + // BCrypt IV: "OrpheanBeholderScryDoubt" + private static readonly uint[] BfCryptCiphertext = { 0x4f727068, 0x65616e42, 0x65686f6c, 0x64657253, 0x63727944, 0x6f756274 }; + + // Table for Base64 encoding + private static readonly char[] Base64Code = + { + '.', '/', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', + 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' + }; + + // Table for Base64 decoding + private static readonly int[] Index64 = + { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 54, 55, 56, 57, 58, 59, 60, + 61, 62, 63, -1, -1, -1, -1, -1, -1, -1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, -1, -1, -1, -1, -1, -1, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, + 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, -1, -1, -1, -1, -1 + }; + + // Fixed configuration + private const string EmptyString = ""; + private const char DefaultHashVersion = 'a'; + private const string Nul = "\0"; + private const short MinRounds = 4; + private const short MaxRounds = 31; + + + // Expanded Blowfish key + private uint[] _p; + private uint[] _s; + + + /// + /// Validate existing hash and password, + /// + /// Current password / string + /// Current hash to validate password against + /// NEW password / string to be hashed + /// The log2 of the number of rounds of hashing to apply - the work + /// factor therefore increases as 2^workFactor. Default is 11 + /// By default this method will not accept a work factor lower + /// than the one set in the current hash and will set the new work-factor to match. + /// returned if the users hash and current pass doesn't validate + /// returned if the salt is invalid in any way + /// returned if the hash is invalid + /// returned if the user hash is null + /// New hash of new password + public static string ValidateAndReplacePassword(string currentKey, string currentHash, string newKey, int workFactor = DefaultRounds, bool forceWorkFactor = false) => + ValidateAndReplacePassword(currentKey, currentHash, false, HashType.None, newKey, false, HashType.None, workFactor, forceWorkFactor); + + + /// + /// Validate existing hash and password, + /// + /// Current password / string + /// Current hash to validate password against + /// Set to true,the string will undergo SHA384 hashing to make + /// use of available entropy prior to bcrypt hashing + /// HashType used (default SHA384) + /// + /// NEW password / string to be hashed + /// Set to true,the string will undergo SHA384 hashing to make + /// use of available entropy prior to bcrypt hashing + /// HashType to use (default SHA384) + /// The log2 of the number of rounds of hashing to apply - the work + /// factor therefore increases as 2^workFactor. Default is 11 + /// By default this method will not accept a work factor lower + /// than the one set in the current hash and will set the new work-factor to match. + /// returned if the users hash and current pass doesn't validate + /// returned if the salt is invalid in any way + /// returned if the hash is invalid + /// returned if the user hash is null + /// New hash of new password + public static string ValidateAndReplacePassword(string currentKey, string currentHash, bool currentKeyEnhancedEntropy, HashType oldHashType, + string newKey, bool newKeyEnhancedEntropy = false, HashType newHashType = DefaultEnhancedHashType, int workFactor = DefaultRounds, bool forceWorkFactor = false) + { + if (currentKey == null) + { + throw new ArgumentNullException(nameof(currentKey)); + } + + if (string.IsNullOrEmpty(currentHash)) + { + throw new ArgumentException("Invalid Hash", nameof(currentHash)); + } + + if (Verify(currentKey, currentHash, currentKeyEnhancedEntropy, oldHashType)) + { + // Determine the starting offset and validate the salt + int startingOffset; + + if (currentHash[0] != '$' || currentHash[1] != '2') + { + throw new SaltParseException("Invalid bcrypt version"); + } + else if (currentHash[2] == '$') + { + startingOffset = 3; + } + else + { + char minor = currentHash[2]; + if (minor != 'a' && minor != 'b' && minor != 'x' && minor != 'y' || currentHash[3] != '$') + { + throw new SaltParseException("Invalid bcrypt revision"); + } + + startingOffset = 4; + } + + // Extract number of rounds + if (currentHash[startingOffset + 2] > '$') + { + throw new SaltParseException("Missing work factor"); + } + + // Extract details from salt + int currentWorkFactor = Convert.ToInt16(currentHash.Substring(startingOffset, 2)); + + // Throw if log rounds are out of range on hash, deals with custom salts + if (workFactor < 1 || workFactor > 31) + { + throw new SaltParseException("Work factor out of range"); + } + + // Never downgrade workfactor (unless forced) + if (!forceWorkFactor && currentWorkFactor > workFactor) + { + workFactor = currentWorkFactor; + } + + return HashPassword(newKey, GenerateSalt(workFactor), newKeyEnhancedEntropy, newHashType); + } + + throw new BcryptAuthenticationException("Current credentials could not be authenticated"); + } + + /// + /// Hash a string using the OpenBSD BCrypt scheme and a salt generated by . + /// + /// Just an alias for HashPassword. + /// The string to hash. + /// The log2 of the number of rounds of hashing to apply - the work + /// factor therefore increases as 2^workFactor. Default is 11 + /// The hashed string. + /// Thrown when the salt could not be parsed. + [Obsolete("Replace with HashPassword, this method will be removed at a later date")] + public static string HashString(string inputKey, int workFactor = DefaultRounds) => HashPassword(inputKey, GenerateSalt(workFactor)); + + /// + /// Hash a password using the OpenBSD BCrypt scheme and a salt generated by . + /// + /// The password to hash. + /// The hashed password. + /// Thrown when the salt could not be parsed. + public static string HashPassword(string inputKey) => HashPassword(inputKey, GenerateSalt()); + + /// + /// Pre-hash a password with SHA384 then using the OpenBSD BCrypt scheme and a salt generated by . + /// + /// The password to hash. + /// The hashed password. + /// Thrown when the salt could not be parsed. + public static string EnhancedHashPassword(string inputKey) => HashPassword(inputKey, GenerateSalt(), true); + + /// + /// Pre-hash a password with SHA384 then using the OpenBSD BCrypt scheme and a salt generated by . + /// + /// The password to hash. + /// + /// The hashed password. + /// Thrown when the salt could not be parsed. + public static string EnhancedHashPassword(string inputKey, int workFactor) => HashPassword(inputKey, GenerateSalt(workFactor), true); + + /// + /// Pre-hash a password with SHA384 then using the OpenBSD BCrypt scheme and a salt generated by . + /// + /// The password to hash. + /// + /// Configurable hash type for enhanced entropy + /// The hashed password. + /// Thrown when the salt could not be parsed. + public static string EnhancedHashPassword(string inputKey, int workFactor, HashType hashType) => HashPassword(inputKey, GenerateSalt(workFactor), true, hashType); + + + /// + /// Pre-hash a password with SHA384 then using the OpenBSD BCrypt scheme and a salt generated by . + /// + /// The password to hash. + /// Defaults to 11 + /// Configurable hash type for enhanced entropy + /// The hashed password. + /// Thrown when the salt could not be parsed. + public static string EnhancedHashPassword(string inputKey, HashType hashType, int workFactor = DefaultRounds) => HashPassword(inputKey, GenerateSalt(workFactor), true, hashType); + + /// + /// Hash a password using the OpenBSD BCrypt scheme and a salt generated by using the given . + /// + /// The password to hash. + /// The log2 of the number of rounds of hashing to apply - the work + /// factor therefore increases as 2^workFactor. Default is 11 + /// Set to true,the string will undergo SHA384 hashing to make use of available entropy prior to bcrypt hashing + /// The hashed password. + /// Thrown when the salt could not be parsed. + public static string HashPassword(string inputKey, int workFactor, bool enhancedEntropy = false) => HashPassword(inputKey, GenerateSalt(workFactor), enhancedEntropy); + + /// Hash a password using the OpenBSD BCrypt scheme. + /// Thrown when one or more arguments have unsupported or illegal values. + /// The password or string to hash. + /// the salt to hash with (best generated using ). + /// The hashed password + /// Thrown when the could not be parsed. + public static string HashPassword(string inputKey, string salt) => HashPassword(inputKey, salt, false); + + /// Hash a password using the OpenBSD BCrypt scheme. + /// Thrown when one or more arguments have unsupported or illegal values. + /// The password or string to hash. + /// the salt to hash with (best generated using ). + /// Set to true,the string will undergo hashing (defaults to SHA384 then base64 encoding) to make use of available entropy prior to bcrypt hashing + /// Configurable hash type for enhanced entropy + /// The hashed password + /// Thrown when the is null. + /// Thrown when the could not be parsed. + public static string HashPassword(string inputKey, string salt, bool enhancedEntropy, HashType hashType = DefaultEnhancedHashType) + { + if (inputKey == null) + { + throw new ArgumentNullException(nameof(inputKey)); + } + + if (string.IsNullOrEmpty(salt)) + { + throw new ArgumentException("Invalid salt: salt cannot be null or empty", nameof(salt)); + } + + if (enhancedEntropy && hashType == HashType.None) + { + throw new ArgumentException("Invalid HashType, You can't have an enhanced hash with type none. HashType.None is used for internal clarity only.", nameof(hashType)); + } + + // Determine the starting offset and validate the salt + int startingOffset; + char bcryptMinorRevision = (char)0; + if (salt[0] != '$' || salt[1] != '2') + { + throw new SaltParseException("Invalid salt version"); + } + else if (salt[2] == '$') + { + startingOffset = 3; + } + else + { + bcryptMinorRevision = salt[2]; + if (bcryptMinorRevision != 'a' && bcryptMinorRevision != 'b' && bcryptMinorRevision != 'x' && bcryptMinorRevision != 'y' || salt[3] != '$') + { + throw new SaltParseException("Invalid salt revision"); + } + + startingOffset = 4; + } + + // Extract number of rounds + if (salt[startingOffset + 2] > '$') + { + throw new SaltParseException("Missing salt rounds"); + } + + // Extract details from salt + int workFactor = Convert.ToInt16(salt.Substring(startingOffset, 2)); + + // Throw if log rounds are out of range on hash, deals with custom salts + if (workFactor < 1 || workFactor > 31) + { + throw new SaltParseException("Salt rounds out of range"); + } + + string extractedSalt = salt.Substring(startingOffset + 3, 22); + + byte[] inputBytes; + + if (enhancedEntropy) + { + inputBytes = EnhancedHash(SafeUTF8.GetBytes(inputKey), bcryptMinorRevision, hashType); + } + else + { + inputBytes = SafeUTF8.GetBytes(inputKey + (bcryptMinorRevision >= 'a' ? Nul : EmptyString)); + } + + byte[] saltBytes = DecodeBase64(extractedSalt, BCryptSaltLen); + + BCrypt bCrypt = new BCrypt(); + + byte[] hashed = bCrypt.CryptRaw(inputBytes, saltBytes, workFactor); + + // Generate result string + var result = new StringBuilder(60); + result.Append("$2").Append(bcryptMinorRevision).Append('$').Append(workFactor.ToString("D2")).Append('$'); + result.Append(EncodeBase64(saltBytes, saltBytes.Length)); + result.Append(EncodeBase64(hashed, (BfCryptCiphertext.Length * 4) - 1)); + + return result.ToString(); + } + + /// + /// Hashes key, base64 encodes before returning byte array + /// + /// + /// + /// + /// + private static byte[] EnhancedHash(byte[] inputBytes, char bcryptMinorRevision, HashType hashType) + { + switch (hashType) + { + case HashType.SHA256: + using (var sha = SHA256.Create()) + inputBytes = SafeUTF8.GetBytes(Convert.ToBase64String(sha.ComputeHash(inputBytes)) + (bcryptMinorRevision >= 'a' ? Nul : EmptyString)); + break; + case HashType.SHA384: + using (var sha = SHA384.Create()) + inputBytes = SafeUTF8.GetBytes(Convert.ToBase64String(sha.ComputeHash(inputBytes)) + (bcryptMinorRevision >= 'a' ? Nul : EmptyString)); + break; + case HashType.SHA512: + using (var sha = SHA512.Create()) + inputBytes = SafeUTF8.GetBytes(Convert.ToBase64String(sha.ComputeHash(inputBytes)) + (bcryptMinorRevision >= 'a' ? Nul : EmptyString)); + break; + default: + throw new ArgumentOutOfRangeException(nameof(hashType), hashType, null); + } + + return inputBytes; + } + + + /// + /// Generate a salt for use with the method. + /// + /// The log2 of the number of rounds of hashing to apply - the work + /// factor therefore increases as 2**workFactor. + /// + /// Work factor must be between 4 and 31 + /// A base64 encoded salt value. + /// BCrypt Revision should be a, b, x or y + public static string GenerateSalt(int workFactor, char bcryptMinorRevision = DefaultHashVersion) + { + if (workFactor < MinRounds || workFactor > MaxRounds) + { + throw new ArgumentOutOfRangeException(nameof(workFactor), workFactor, $"The work factor must be between {MinRounds} and {MaxRounds} (inclusive)"); + } + + if (bcryptMinorRevision != 'a' && bcryptMinorRevision != 'b' && bcryptMinorRevision != 'x' && bcryptMinorRevision != 'y') + { + throw new ArgumentException("BCrypt Revision should be a, b, x or y", nameof(bcryptMinorRevision)); + } + + byte[] saltBytes = new byte[BCryptSaltLen]; + + RngCsp.GetBytes(saltBytes); + + var result = new StringBuilder(29); + result.Append("$2").Append(bcryptMinorRevision).Append('$').Append(workFactor.ToString("D2")).Append('$'); + result.Append(EncodeBase64(saltBytes, saltBytes.Length)); + + return result.ToString(); + } + + + /// + /// Based on password_needs_rehash in PHP this method will return true + /// if the work factor (logrounds) set on the hash is lower than the new minimum workload passed in + /// + /// full bcrypt hash + /// target workload + /// true if new work factor is higher than the one in the hash + /// throws if the current hash workload (logrounds) can not be parsed + /// + public static bool PasswordNeedsRehash(string hash, int newMinimumWorkLoad) + { + int currentWorkLoad = HashParser.GetWorkFactor(hash); + + return currentWorkLoad < newMinimumWorkLoad; + } + + /// + /// Takes a valid hash and outputs its component parts + /// + /// + /// + public static HashInformation InterrogateHash(string hash) + { + try + { + return HashParser.GetHashInformation(hash); + } + catch (Exception ex) + { + throw new HashInformationException("Error handling string interrogation", ex); + } + } + + + /// + /// Generate a salt for use with the method + /// selecting a reasonable default for the number of hashing rounds to apply. + /// + /// A base64 encoded salt value. + public static string GenerateSalt() + { + return GenerateSalt(DefaultRounds); + } + + /// + /// Verifies that the hash of the given matches the provided + /// ; the string will undergo SHA384 hashing to maintain the enhanced entropy work done during hashing + /// + /// The text to verify. + /// The previously-hashed password. + /// HashType used (default SHA384) + /// true if the passwords match, false otherwise. + public static bool EnhancedVerify(string text, string hash, HashType hashType = DefaultEnhancedHashType) => Verify(text, hash, true, hashType); + + /// + /// Verifies that the hash of the given matches the provided + /// + /// + /// The text to verify. + /// The previously-hashed password. + /// Set to true,the string will undergo SHA384 hashing to make use of available entropy prior to bcrypt hashing + /// HashType used (default SHA384) + /// true if the passwords match, false otherwise. + /// Thrown when one or more arguments have unsupported or illegal values. + /// Thrown when the salt could not be parsed. + public static bool Verify(string text, string hash, bool enhancedEntropy = false, HashType hashType = DefaultEnhancedHashType) + { + return SecureEquals(SafeUTF8.GetBytes(hash), SafeUTF8.GetBytes(HashPassword(text, hash, enhancedEntropy, hashType))); + } + + // Compares two byte arrays for equality. The method is specifically written so that the loop is not optimised. + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + private static bool SecureEquals(byte[] a, byte[] b) + { + if (a == null && b == null) + { + return true; + } + + if (a == null || b == null || a.Length != b.Length) + { + return false; + } + + int diff = 0; + for (var i = 0; i < a.Length; i++) + { + diff |= (a[i] ^ b[i]); + } + + return diff == 0; + } + + + /// + /// Encode a byte array using BCrypt's slightly-modified base64 encoding scheme. Note that this + /// is *not* compatible with the standard MIME-base64 encoding. + /// + /// Thrown when one or more arguments have unsupported or + /// illegal values. + /// The byte array to encode. + /// The number of bytes to encode. + /// Base64-encoded string. + internal static char[] EncodeBase64(byte[] byteArray, int length) + { + if (length <= 0 || length > byteArray.Length) + { + throw new ArgumentException("Invalid length", nameof(length)); + } + + int encodedSize = (int)Math.Ceiling((length * 4D) / 3); + char[] encoded = new char[encodedSize]; + + int pos = 0; + int off = 0; + while (off < length) + { + int c1 = byteArray[off++] & 0xff; + encoded[pos++] = Base64Code[(c1 >> 2) & 0x3f]; + c1 = (c1 & 0x03) << 4; + if (off >= length) + { + encoded[pos++] = Base64Code[c1 & 0x3f]; + break; + } + + int c2 = byteArray[off++] & 0xff; + c1 |= (c2 >> 4) & 0x0f; + encoded[pos++] = Base64Code[c1 & 0x3f]; + c1 = (c2 & 0x0f) << 2; + if (off >= length) + { + encoded[pos++] = Base64Code[c1 & 0x3f]; + break; + } + + c2 = byteArray[off++] & 0xff; + c1 |= (c2 >> 6) & 0x03; + encoded[pos++] = Base64Code[c1 & 0x3f]; + encoded[pos++] = Base64Code[c2 & 0x3f]; + } + + return encoded; + } + + /// + /// Decode a string encoded using BCrypt's base64 scheme to a byte array. + /// Note that this is *not* compatible with the standard MIME-base64 encoding. + /// + /// Thrown when one or more arguments have unsupported or + /// illegal values. + /// The string to decode. + /// The maximum bytes to decode. + /// The decoded byte array. + internal static byte[] DecodeBase64(string encodedString, int maximumBytes) + { + int sourceLength = encodedString.Length; + int outputLength = 0; + + if (maximumBytes <= 0) + { + throw new ArgumentException("Invalid maximum bytes value", nameof(maximumBytes)); + } + + byte[] result = new byte[maximumBytes]; + + int position = 0; + while (position < sourceLength - 1 && outputLength < maximumBytes) + { + int c1 = Char64(encodedString[position++]); + int c2 = Char64(encodedString[position++]); + if (c1 == -1 || c2 == -1) + { + break; + } + + result[outputLength] = (byte)((c1 << 2) | ((c2 & 0x30) >> 4)); + if (++outputLength >= maximumBytes || position >= sourceLength) + { + break; + } + + int c3 = Char64(encodedString[position++]); + if (c3 == -1) + { + break; + } + + result[outputLength] = (byte)(((c2 & 0x0f) << 4) | ((c3 & 0x3c) >> 2)); + if (++outputLength >= maximumBytes || position >= sourceLength) + { + break; + } + + int c4 = Char64(encodedString[position++]); + result[outputLength] = (byte)(((c3 & 0x03) << 6) | c4); + + ++outputLength; + } + + return result; + } + + /// + /// Look up the 3 bits base64-encoded by the specified character, range-checking against + /// conversion table. + /// + /// The base64-encoded value. + /// The decoded value of x. + private static int Char64(char character) + { + return character < 0 || character > Index64.Length ? -1 : Index64[character]; + } + + /// Blowfish encipher a single 64-bit block encoded as two 32-bit halves. + /// An array containing the two 32-bit half blocks. + /// The position in the array of the blocks. +#if NETCOREAPP + private void Encipher(Span blockArray, int offset) +#else + private void Encipher(uint[] blockArray, int offset) +#endif + { + uint block = blockArray[offset]; + uint r = blockArray[offset + 1]; + + block ^= _p[0]; + + unchecked + { + uint round; + for (round = 0; round <= BlowfishNumRounds - 2;) + { + // Feistel substitution on left word + uint n = _s[(block >> 24) & 0xff]; + n += _s[0x100 | ((block >> 16) & 0xff)]; + n ^= _s[0x200 | ((block >> 8) & 0xff)]; + n += _s[0x300 | (block & 0xff)]; + r ^= n ^ _p[++round]; + + // Feistel substitution on right word + n = _s[(r >> 24) & 0xff]; + n += _s[0x100 | ((r >> 16) & 0xff)]; + n ^= _s[0x200 | ((r >> 8) & 0xff)]; + n += _s[0x300 | (r & 0xff)]; + block ^= n ^ _p[++round]; + } + + blockArray[offset] = r ^ _p[BlowfishNumRounds + 1]; + blockArray[offset + 1] = block; + } + } + + /// Cyclically extract a word of key material. + /// The string to extract the data from. + /// [in,out] The current offset. + /// The next word of material from data. +#if NETCOREAPP + private static uint StreamToWord(ReadOnlySpan data, ref int offset) +#else + private static uint StreamToWord(byte[] data, ref int offset) +#endif + { + int i; + uint word = 0; + + for (i = 0; i < 4; i++) + { + word = (word << 8) | (uint)(data[offset] & 0xff); + offset = (offset + 1) % data.Length; + } + + return word; + } + + /// Initializes the Blowfish key schedule. + private void InitializeKey() + { + _p = new uint[POrig.Length]; + _s = new uint[SOrig.Length]; + Array.Copy(POrig, _p, POrig.Length); + Array.Copy(SOrig, _s, SOrig.Length); + } + + /// Key the Blowfish cipher. + /// The key byte array. +#if NETCOREAPP + private void Key(ReadOnlySpan keyBytes) +#else + private void Key(byte[] keyBytes) +#endif + { + int i; + int koffp = 0; +#if NETCOREAPP + Span lr = stackalloc uint[2] { 0, 0 }; +#else + uint[] lr = { 0, 0 }; +#endif + int plen = _p.Length, slen = _s.Length; + + for (i = 0; i < plen; i++) + { + _p[i] = _p[i] ^ StreamToWord(keyBytes, ref koffp); + } + + for (i = 0; i < plen; i += 2) + { + Encipher(lr, 0); + _p[i] = lr[0]; + _p[i + 1] = lr[1]; + } + + for (i = 0; i < slen; i += 2) + { + Encipher(lr, 0); + _s[i] = lr[0]; + _s[i + 1] = lr[1]; + } + } + + /// + /// Perform the "enhanced key schedule" step described by Provos and Mazieres in + /// "A Future Adaptable Password Scheme" http://www.openbsd.org/papers/bcrypt-paper.ps. + /// + /// Salt byte array. + /// Input byte array. + // ReSharper disable once InconsistentNaming +#if NETCOREAPP + private void EKSKey(ReadOnlySpan saltBytes, ReadOnlySpan inputBytes) +#else + private void EKSKey(byte[] saltBytes, byte[] inputBytes) +#endif + { + int i; + int passwordOffset = 0; + int saltOffset = 0; +#if NETCOREAPP + Span lr = stackalloc uint[2] { 0, 0 }; +#else + uint[] lr = { 0, 0 }; +#endif + int plen = _p.Length, slen = _s.Length; + + for (i = 0; i < plen; i++) + { + _p[i] = _p[i] ^ StreamToWord(inputBytes, ref passwordOffset); + } + + for (i = 0; i < plen; i += 2) + { + lr[0] ^= StreamToWord(saltBytes, ref saltOffset); + lr[1] ^= StreamToWord(saltBytes, ref saltOffset); + Encipher(lr, 0); + _p[i] = lr[0]; + _p[i + 1] = lr[1]; + } + + for (i = 0; i < slen; i += 2) + { + lr[0] ^= StreamToWord(saltBytes, ref saltOffset); + lr[1] ^= StreamToWord(saltBytes, ref saltOffset); + Encipher(lr, 0); + _s[i] = lr[0]; + _s[i + 1] = lr[1]; + } + } + + /// Perform the central hashing step in the BCrypt scheme. + /// Thrown when one or more arguments have unsupported or + /// illegal values. + /// The input byte array to hash. + /// The salt byte array to hash with. + /// The binary logarithm of the number of rounds of hashing to apply. + /// A byte array containing the hashed result. +#if NETCOREAPP + internal byte[] CryptRaw(ReadOnlySpan inputBytes, ReadOnlySpan saltBytes, int workFactor) +#else + internal byte[] CryptRaw(byte[] inputBytes, byte[] saltBytes, int workFactor) +#endif + { + int i; + int j; + +#if NETCOREAPP + Span cdata = stackalloc uint[BfCryptCiphertext.Length]; + BfCryptCiphertext.CopyTo(cdata); +#else + uint[] cdata = new uint[BfCryptCiphertext.Length]; + Array.Copy(BfCryptCiphertext, cdata, BfCryptCiphertext.Length); +#endif + + int clen = cdata.Length; + + if (workFactor < MinRounds || workFactor > MaxRounds) + { + throw new ArgumentException("Bad number of rounds", nameof(workFactor)); + } + + if (saltBytes.Length != BCryptSaltLen) + { + throw new ArgumentException("Bad salt Length", nameof(saltBytes)); + } + + uint rounds = 1u << workFactor; + + // We overflowed rounds at 31 - added safety check + if (rounds < 1) + { + throw new ArgumentException("Bad number of rounds", nameof(workFactor)); + } + + InitializeKey(); + EKSKey(saltBytes, inputBytes); + + for (i = 0; i != rounds; i++) + { + Key(inputBytes); + Key(saltBytes); + } + + for (i = 0; i < 64; i++) + { + for (j = 0; j < (clen >> 1); j++) + { + Encipher(cdata, j << 1); + } + } + + byte[] ret = new byte[clen * 4]; + for (i = 0, j = 0; i < clen; i++) + { + ret[j++] = (byte)((cdata[i] >> 24) & 0xff); + ret[j++] = (byte)((cdata[i] >> 16) & 0xff); + ret[j++] = (byte)((cdata[i] >> 8) & 0xff); + ret[j++] = (byte)(cdata[i] & 0xff); + } + + return ret; + } + } + + /// + /// HashInformation : A value object that contains the results of interrogating a hash + /// Namely its settings (2a$10 for example); version (2a); workfactor (log rounds), and the raw hash returned + /// + [SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Local")] + internal sealed class HashInformation + { + /// Constructor. + /// The message. + /// The message. + /// The message. + /// The message. + internal HashInformation(string settings, string version, string workFactor, string rawHash) + { + Settings = settings; + Version = version; + WorkFactor = workFactor; + RawHash = rawHash; + } + + /// + /// Settings string + /// + public string Settings { get; private set; } + + /// + /// Hash Version + /// + public string Version { get; private set; } + + /// + /// log rounds used / workfactor + /// + public string WorkFactor { get; private set; } + + /// + /// Raw Hash + /// + public string RawHash { get; private set; } + } + + /// + /// Exception for signalling hash validation errors. + [Serializable] + internal class BcryptAuthenticationException : Exception + { + /// + /// Default constructor. + public BcryptAuthenticationException() + { + } + + /// + /// Initializes a new instance of . + /// The message. + public BcryptAuthenticationException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of . + /// The message. + /// The inner exception. + public BcryptAuthenticationException(string message, Exception innerException) + : base(message, innerException) + { + } + } + + [Serializable] + internal sealed class HashInformationException : Exception + { + /// + /// Default Constructor + /// + public HashInformationException() + { + } + + /// + /// Initializes a new instance of . + /// + /// + public HashInformationException(string message) : base(message) + { + } + + /// + /// Initializes a new instance of . + /// + /// + /// + public HashInformationException(string message, Exception innerException) : base(message, innerException) + { + } + } + + internal static class HashParser + { + private static readonly HashFormatDescriptor OldFormatDescriptor = new HashFormatDescriptor(versionLength: 1); + private static readonly HashFormatDescriptor NewFormatDescriptor = new HashFormatDescriptor(versionLength: 2); + + public static HashInformation GetHashInformation(string hash) + { + if (!IsValidHash(hash, out var format)) + { + ThrowInvalidHashFormat(); + } + + return new HashInformation( + hash.Substring(0, format.SettingLength), + hash.Substring(1, format.VersionLength), + hash.Substring(format.WorkfactorOffset, 2), + hash.Substring(format.HashOffset)); + } + + public static int GetWorkFactor(string hash) + { + if (!IsValidHash(hash, out var format)) + { + ThrowInvalidHashFormat(); + } + + int offset = format.WorkfactorOffset; + + return 10 * (hash[offset] - '0') + (hash[offset + 1] - '0'); + } + + internal static bool IsValidHash(string hash, out HashFormatDescriptor format) + { + if (hash is null) + { + throw new ArgumentNullException(nameof(hash)); + } + + if (hash.Length != 59 && hash.Length != 60) + { + // Incorrect full hash length + format = null; + return false; + } + + if (!hash.StartsWith("$2")) + { + // Not a bcrypt hash + format = null; + return false; + } + + // Validate version + int offset = 2; + if (IsValidBCryptVersionChar(hash[offset])) + { + offset++; + format = NewFormatDescriptor; + } + else + { + format = OldFormatDescriptor; + } + + if (hash[offset++] != '$') + { + format = null; + return false; + } + + // Validate workfactor + if (!IsAsciiNumeric(hash[offset++]) + || !IsAsciiNumeric(hash[offset++])) + { + format = null; + return false; + } + + if (hash[offset++] != '$') + { + format = null; + return false; + } + + // Validate hash + for (int i = offset; i < hash.Length; ++i) + { + if (!IsValidBCryptBase64Char(hash[i])) + { + format = null; + return false; + } + } + + return true; + } + + private static bool IsValidBCryptVersionChar(char value) + { + return value == 'a' + || value == 'b' + || value == 'x' + || value == 'y'; + } + + private static bool IsValidBCryptBase64Char(char value) + { + // Ordered by ascending ASCII value + return value == '.' + || value == '/' + || (value >= '0' && value <= '9') + || (value >= 'A' && value <= 'Z') + || (value >= 'a' && value <= 'z'); + } + + private static bool IsAsciiNumeric(char value) + { + return value >= '0' && value <= '9'; + } + + private static void ThrowInvalidHashFormat() + { + throw new SaltParseException("Invalid Hash Format"); + } + + internal class HashFormatDescriptor + { + public HashFormatDescriptor(int versionLength) + { + VersionLength = versionLength; + WorkfactorOffset = 1 + VersionLength + 1; + SettingLength = WorkfactorOffset + 2; + HashOffset = SettingLength + 1; + } + + public int VersionLength { get; } + + public int WorkfactorOffset { get; } + + public int SettingLength { get; } + + public int HashOffset { get; } + } + + internal enum HashType + { + None = -1, + SHA256 = 0, + SHA384 = 1, + SHA512 = 2 + } + } + } +} diff --git a/benchmark/BCryptNet.BenchMarks.csproj b/benchmark/BCryptNet.BenchMarks.csproj index 6a2425d..d119224 100644 --- a/benchmark/BCryptNet.BenchMarks.csproj +++ b/benchmark/BCryptNet.BenchMarks.csproj @@ -3,7 +3,7 @@ BCryptNet.BenchMarks BCryptNet.BenchMarks Exe - net48;net9.0 + net9.0 true Release;Debug AnyCPU @@ -12,6 +12,7 @@ portable false false + latest diff --git a/benchmark/EncodeB64/Encoder.cs b/benchmark/EncodeB64/Encoder.cs index dd44b05..c35b044 100644 --- a/benchmark/EncodeB64/Encoder.cs +++ b/benchmark/EncodeB64/Encoder.cs @@ -129,5 +129,95 @@ public static char[] EncodeBase64AsBytes(byte[] byteArray, int length) return encoded; } + + internal static ReadOnlySpan EncodeBase64StackAlloc(ReadOnlySpan byteArray, int length) + { + if (length <= 0 || length > byteArray.Length) + { + throw new ArgumentException("Invalid length", nameof(length)); + } + + int encodedSize = (int)Math.Ceiling((length * 4D) / 3); + Span encoded = stackalloc char[encodedSize]; + + int pos = 0; + int off = 0; + while (off < length) + { + //Process first byte in group + int c1 = byteArray[off++] & 0xff; + encoded[pos++] = Base64Code[(c1 >> 2) & 0x3f]; + c1 = (c1 & 0x03) << 4; + if (off >= length) + { + encoded[pos++] = Base64Code[c1 & 0x3f]; + break; + } + + // second byte of group + int c2 = byteArray[off++] & 0xff; + c1 |= (c2 >> 4) & 0x0f; + encoded[pos++] = Base64Code[c1 & 0x3f]; + c1 = (c2 & 0x0f) << 2; + if (off >= length) + { + encoded[pos++] = Base64Code[c1 & 0x3f]; + break; + } + + // third byte of group + c2 = byteArray[off++] & 0xff; + c1 |= (c2 >> 6) & 0x03; + encoded[pos++] = Base64Code[c1 & 0x3f]; + encoded[pos++] = Base64Code[c2 & 0x3f]; + } + + return encoded.ToArray(); + } + + internal static ReadOnlySpan EncodeBase64HeapAlloc(ReadOnlySpan byteArray, int length) + { + if (length <= 0 || length > byteArray.Length) + { + throw new ArgumentException("Invalid length", nameof(length)); + } + + int encodedSize = (int)Math.Ceiling((length * 4D) / 3); + Span encoded = new char[encodedSize]; + + int pos = 0; + int off = 0; + while (off < length) + { + //Process first byte in group + int c1 = byteArray[off++] & 0xff; + encoded[pos++] = Base64Code[(c1 >> 2) & 0x3f]; + c1 = (c1 & 0x03) << 4; + if (off >= length) + { + encoded[pos++] = Base64Code[c1 & 0x3f]; + break; + } + + // second byte of group + int c2 = byteArray[off++] & 0xff; + c1 |= (c2 >> 4) & 0x0f; + encoded[pos++] = Base64Code[c1 & 0x3f]; + c1 = (c2 & 0x0f) << 2; + if (off >= length) + { + encoded[pos++] = Base64Code[c1 & 0x3f]; + break; + } + + // third byte of group + c2 = byteArray[off++] & 0xff; + c1 |= (c2 >> 6) & 0x03; + encoded[pos++] = Base64Code[c1 & 0x3f]; + encoded[pos++] = Base64Code[c2 & 0x3f]; + } + + return encoded; + } } -} \ No newline at end of file +} diff --git a/benchmark/Program.cs b/benchmark/Program.cs index 8d44a48..e5eea6d 100644 --- a/benchmark/Program.cs +++ b/benchmark/Program.cs @@ -15,15 +15,10 @@ static void Main(string[] args) { #if DEBUG BenchmarkRunner.Run(new DebugInProcessConfig().AddValidator(ExecutionValidator.FailOnError)); - // BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly) - // .Run(args, new DebugInProcessConfig() - // // .With(Job.Default.With(CoreRuntime.Latest)) - // // .With(Job.Default.With(ClrRuntime.Net48)) - // .AddValidator(ExecutionValidator.FailOnError)); #else var config = DefaultConfig.Instance - .With(Job.Default.With(CoreRuntime.Core60)) - .With(Job.Default.With(ClrRuntime.Net48)); + .With(Job.Default.With(CoreRuntime.Core90)) + ; // BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config); BenchmarkRunner.Run(config); diff --git a/benchmark/TestB64Encoder.cs b/benchmark/TestB64Encoder.cs index 5a2f3a7..93d2f81 100644 --- a/benchmark/TestB64Encoder.cs +++ b/benchmark/TestB64Encoder.cs @@ -30,5 +30,17 @@ public void EncodeBase64AsBytes() { var decoded = EncodeB64Methods.EncodeBase64AsBytes(SaltBytes, 16); } + + [Benchmark] + public void EncodeBase64StackAlloc() + { + var decoded = EncodeB64Methods.EncodeBase64StackAlloc(SaltBytes, 16); + } + + [Benchmark] + public void EncodeBase64HeapSpanAlloc() + { + var decoded = EncodeB64Methods.EncodeBase64HeapAlloc(SaltBytes, 16); + } } } diff --git a/benchmark/TestBcrypt_HashInterrogation.cs b/benchmark/TestBcrypt_HashInterrogation.cs index f4c4ea2..49ab54b 100644 --- a/benchmark/TestBcrypt_HashInterrogation.cs +++ b/benchmark/TestBcrypt_HashInterrogation.cs @@ -1,5 +1,6 @@ using BCryptNet.BenchMarks._3._2._1; using BCryptNet.BenchMarks._4._0._0; +using BCryptNet.BenchMarks._4._0._3; using BenchmarkDotNet.Attributes; #pragma warning disable 1591 @@ -24,7 +25,15 @@ public void InterrogateHashUsingRegex(string hash) [Arguments( "$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq")] public void InterrogateHashUsingParserV4(string hash) { - version4.BCrypt.InterrogateHash(hash); + BCryptV4.BCrypt.InterrogateHash(hash); + } + + [Benchmark()] + [Arguments("$2a$12$k42ZFHFWqBp3vWli.nIn8uYyIkbvYRvodzbfbK18SSsY.CsIQPlxO")] + [Arguments( "$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq")] + public void InterrogateHashUsingParserV403(string hash) + { + BCryptV403.BCrypt.InterrogateHash(hash); } [Benchmark()] diff --git a/benchmark/TestBcrypt_Hashing.cs b/benchmark/TestBcrypt_Hashing.cs index 3483dc5..714d9aa 100644 --- a/benchmark/TestBcrypt_Hashing.cs +++ b/benchmark/TestBcrypt_Hashing.cs @@ -3,6 +3,7 @@ using BCryptNet.BenchMarks._3._2._1; using BCryptNet.BenchMarks._3._5.perfmerge_1; using BCryptNet.BenchMarks._4._0._0; +using BCryptNet.BenchMarks._4._0._3; using BenchmarkDotNet.Attributes; #pragma warning disable 1591 @@ -16,26 +17,26 @@ public class TestBcrypt_Hashing { public IEnumerable Data() { - yield return new object[] { "", "$2a$06$DCq7YPn5Rq63x1Lad4cll.", "$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s." }; - yield return new object[] { "", "$2a$08$HqWuK6/Ng6sg9gQzbLrgb.", "$2a$08$HqWuK6/Ng6sg9gQzbLrgb.Tl.ZHfXLhvt/SgVyWhQqgqcZ7ZuUtye" }; - yield return new object[] { "", "$2a$10$k1wbIrmNyFAPwPVPSVa/ze", "$2a$10$k1wbIrmNyFAPwPVPSVa/zecw2BCEnBwVS2GbrmgzxFUOqW9dk4TCW" }; - yield return new object[] { "", "$2a$12$k42ZFHFWqBp3vWli.nIn8u", "$2a$12$k42ZFHFWqBp3vWli.nIn8uYyIkbvYRvodzbfbK18SSsY.CsIQPlxO" }; - yield return new object[] { "a", "$2a$06$m0CrhHm10qJ3lXRY.5zDGO", "$2a$06$m0CrhHm10qJ3lXRY.5zDGO3rS2KdeeWLuGmsfGlMfOxih58VYVfxe" }; - yield return new object[] { "a", "$2a$08$cfcvVd2aQ8CMvoMpP2EBfe", "$2a$08$cfcvVd2aQ8CMvoMpP2EBfeodLEkkFJ9umNEfPD18.hUF62qqlC/V." }; - yield return new object[] { "a", "$2a$10$k87L/MF28Q673VKh8/cPi.", "$2a$10$k87L/MF28Q673VKh8/cPi.SUl7MU/rWuSiIDDFayrKk/1tBsSQu4u" }; - yield return new object[] { "a", "$2a$12$8NJH3LsPrANStV6XtBakCe", "$2a$12$8NJH3LsPrANStV6XtBakCez0cKHXVxmvxIlcz785vxAIZrihHZpeS" }; - yield return new object[] { "abc", "$2a$06$If6bvum7DFjUnE9p2uDeDu", "$2a$06$If6bvum7DFjUnE9p2uDeDu0YHzrHM6tf.iqN8.yx.jNN1ILEf7h0i" }; - yield return new object[] { "abc", "$2a$08$Ro0CUfOqk6cXEKf3dyaM7O", "$2a$08$Ro0CUfOqk6cXEKf3dyaM7OhSCvnwM9s4wIX9JeLapehKK5YdLxKcm" }; - yield return new object[] { "abc", "$2a$10$WvvTPHKwdBJ3uk0Z37EMR.", "$2a$10$WvvTPHKwdBJ3uk0Z37EMR.hLA2W6N9AEBhEgrAOljy2Ae5MtaSIUi" }; - yield return new object[] { "abc", "$2a$12$EXRkfkdmXn2gzds2SSitu.", "$2a$12$EXRkfkdmXn2gzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q" }; - yield return new object[] { "abcdefghijklmnopqrstuvwxyz", "$2a$06$.rCVZVOThsIa97pEDOxvGu", "$2a$06$.rCVZVOThsIa97pEDOxvGuRRgzG64bvtJ0938xuqzv18d3ZpQhstC" }; - yield return new object[] { "abcdefghijklmnopqrstuvwxyz", "$2a$08$aTsUwsyowQuzRrDqFflhge", "$2a$08$aTsUwsyowQuzRrDqFflhgekJ8d9/7Z3GV3UcgvzQW3J5zMyrTvlz." }; - yield return new object[] { "abcdefghijklmnopqrstuvwxyz", "$2a$10$fVH8e28OQRj9tqiDXs1e1u", "$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq" }; - yield return new object[] { "abcdefghijklmnopqrstuvwxyz", "$2a$12$D4G5f18o7aMMfwasBL7Gpu", "$2a$12$D4G5f18o7aMMfwasBL7GpuQWuP3pkrZrOAnqP.bmezbMng.QwJ/pG" }; - yield return new object[] { "~!@#$%^&*() ~!@#$%^&*()PNBFRD", "$2a$06$fPIsBO8qRqkjj273rfaOI.", "$2a$06$fPIsBO8qRqkjj273rfaOI.HtSV9jLDpTbZn782DC6/t7qT67P6FfO" }; - yield return new object[] { "~!@#$%^&*() ~!@#$%^&*()PNBFRD", "$2a$08$Eq2r4G/76Wv39MzSX262hu", "$2a$08$Eq2r4G/76Wv39MzSX262huzPz612MZiYHVUJe/OcOql2jo4.9UxTW" }; - yield return new object[] { "~!@#$%^&*() ~!@#$%^&*()PNBFRD", "$2a$10$LgfYWkbzEvQ4JakH7rOvHe", "$2a$10$LgfYWkbzEvQ4JakH7rOvHe0y8pHKF9OaFgwUZ2q7W2FFZmZzJYlfS" }; - yield return new object[] { "~!@#$%^&*() ~!@#$%^&*()PNBFRD", "$2a$12$WApznUOJfkEGSmYRfnkrPO", "$2a$12$WApznUOJfkEGSmYRfnkrPOr466oFDCaj4b6HY3EXGvfxm43seyhgC" }; + yield return ["", "$2a$06$DCq7YPn5Rq63x1Lad4cll.", "$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s."]; + yield return ["", "$2a$08$HqWuK6/Ng6sg9gQzbLrgb.", "$2a$08$HqWuK6/Ng6sg9gQzbLrgb.Tl.ZHfXLhvt/SgVyWhQqgqcZ7ZuUtye"]; + yield return ["", "$2a$10$k1wbIrmNyFAPwPVPSVa/ze", "$2a$10$k1wbIrmNyFAPwPVPSVa/zecw2BCEnBwVS2GbrmgzxFUOqW9dk4TCW"]; + yield return ["", "$2a$12$k42ZFHFWqBp3vWli.nIn8u", "$2a$12$k42ZFHFWqBp3vWli.nIn8uYyIkbvYRvodzbfbK18SSsY.CsIQPlxO"]; + yield return ["a", "$2a$06$m0CrhHm10qJ3lXRY.5zDGO", "$2a$06$m0CrhHm10qJ3lXRY.5zDGO3rS2KdeeWLuGmsfGlMfOxih58VYVfxe"]; + yield return ["a", "$2a$08$cfcvVd2aQ8CMvoMpP2EBfe", "$2a$08$cfcvVd2aQ8CMvoMpP2EBfeodLEkkFJ9umNEfPD18.hUF62qqlC/V."]; + yield return ["a", "$2a$10$k87L/MF28Q673VKh8/cPi.", "$2a$10$k87L/MF28Q673VKh8/cPi.SUl7MU/rWuSiIDDFayrKk/1tBsSQu4u"]; + yield return ["a", "$2a$12$8NJH3LsPrANStV6XtBakCe", "$2a$12$8NJH3LsPrANStV6XtBakCez0cKHXVxmvxIlcz785vxAIZrihHZpeS"]; + yield return ["abc", "$2a$06$If6bvum7DFjUnE9p2uDeDu", "$2a$06$If6bvum7DFjUnE9p2uDeDu0YHzrHM6tf.iqN8.yx.jNN1ILEf7h0i"]; + yield return ["abc", "$2a$08$Ro0CUfOqk6cXEKf3dyaM7O", "$2a$08$Ro0CUfOqk6cXEKf3dyaM7OhSCvnwM9s4wIX9JeLapehKK5YdLxKcm"]; + yield return ["abc", "$2a$10$WvvTPHKwdBJ3uk0Z37EMR.", "$2a$10$WvvTPHKwdBJ3uk0Z37EMR.hLA2W6N9AEBhEgrAOljy2Ae5MtaSIUi"]; + yield return ["abc", "$2a$12$EXRkfkdmXn2gzds2SSitu.", "$2a$12$EXRkfkdmXn2gzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q"]; + yield return ["abcdefghijklmnopqrstuvwxyz", "$2a$06$.rCVZVOThsIa97pEDOxvGu", "$2a$06$.rCVZVOThsIa97pEDOxvGuRRgzG64bvtJ0938xuqzv18d3ZpQhstC"]; + yield return ["abcdefghijklmnopqrstuvwxyz", "$2a$08$aTsUwsyowQuzRrDqFflhge", "$2a$08$aTsUwsyowQuzRrDqFflhgekJ8d9/7Z3GV3UcgvzQW3J5zMyrTvlz."]; + yield return ["abcdefghijklmnopqrstuvwxyz", "$2a$10$fVH8e28OQRj9tqiDXs1e1u", "$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq"]; + yield return ["abcdefghijklmnopqrstuvwxyz", "$2a$12$D4G5f18o7aMMfwasBL7Gpu", "$2a$12$D4G5f18o7aMMfwasBL7GpuQWuP3pkrZrOAnqP.bmezbMng.QwJ/pG"]; + yield return ["~!@#$%^&*() ~!@#$%^&*()PNBFRD", "$2a$06$fPIsBO8qRqkjj273rfaOI.", "$2a$06$fPIsBO8qRqkjj273rfaOI.HtSV9jLDpTbZn782DC6/t7qT67P6FfO"]; + yield return ["~!@#$%^&*() ~!@#$%^&*()PNBFRD", "$2a$08$Eq2r4G/76Wv39MzSX262hu", "$2a$08$Eq2r4G/76Wv39MzSX262huzPz612MZiYHVUJe/OcOql2jo4.9UxTW"]; + yield return ["~!@#$%^&*() ~!@#$%^&*()PNBFRD", "$2a$10$LgfYWkbzEvQ4JakH7rOvHe", "$2a$10$LgfYWkbzEvQ4JakH7rOvHe0y8pHKF9OaFgwUZ2q7W2FFZmZzJYlfS"]; + yield return ["~!@#$%^&*() ~!@#$%^&*()PNBFRD", "$2a$12$WApznUOJfkEGSmYRfnkrPO", "$2a$12$WApznUOJfkEGSmYRfnkrPOr466oFDCaj4b6HY3EXGvfxm43seyhgC"]; } [Benchmark(Baseline = true)] @@ -60,30 +61,27 @@ public string TestHashValidatePerf1(string key, string salt, string hash) [ArgumentsSource(nameof(Data))] public string TestHashValidateV4(string key, string salt, string hash) { - string hashed = version4.BCrypt.HashPassword(key, salt); - var validateHashCheck = BCrypt.Verify(key, hashed); + string hashed = BCryptV4.BCrypt.HashPassword(key, salt); + var validateHashCheck = BCryptV4.BCrypt.Verify(key, hashed); return hashed + validateHashCheck.ToString(); - } - + } + [Benchmark] [ArgumentsSource(nameof(Data))] - public string TestHashValidateCurrent(string key, string salt, string hash) + public string TestHashValidateV403(string key, string salt, string hash) { - string hashed = BCrypt.HashPassword(key, salt); - var validateHashCheck = BCrypt.Verify(key, hashed); + string hashed = BCryptV403.BCrypt.HashPassword(key, salt); + var validateHashCheck = BCryptV403.BCrypt.Verify(key, hashed); return hashed + validateHashCheck.ToString(); } - -#if NETCOREAPP + [Benchmark] [ArgumentsSource(nameof(Data))] - public string TestHashValidateCurrentSpan(string key, string salt, string hash) + public string TestHashValidateCurrent(string key, string salt, string hash) { - string hashed = BCrypt.HashPassword(key.AsSpan(), salt.AsSpan()); + string hashed = BCrypt.HashPassword(key, salt); var validateHashCheck = BCrypt.Verify(key, hashed); return hashed + validateHashCheck.ToString(); } -#endif - } } diff --git a/benchmark/TestBcrypt_Hashing_Enhanced.cs b/benchmark/TestBcrypt_Hashing_Enhanced.cs index 489fae3..5ab565b 100644 --- a/benchmark/TestBcrypt_Hashing_Enhanced.cs +++ b/benchmark/TestBcrypt_Hashing_Enhanced.cs @@ -69,8 +69,8 @@ public string TestHashValidateEnhancedv3perf(string key, string salt, string has [ArgumentsSource(nameof(Data))] public string TestHashValidateEnhancedv4perf(string key, string salt, string hash) { - string hashed = version4.BCrypt.HashPassword(key, salt, enhancedEntropy: true); - var validateHashCheck = version4.BCrypt.EnhancedVerify(key, hashed); + string hashed = BCryptV4.BCrypt.HashPassword(key, salt, enhancedEntropy: true); + var validateHashCheck = BCryptV4.BCrypt.EnhancedVerify(key, hashed); return hashed + validateHashCheck.ToString(); } @@ -85,13 +85,13 @@ public string TestHashValidateEnhancedCurrent(string key, string salt, string ha private static readonly string Hmackey = Guid.NewGuid().ToString(); - // [Benchmark] - // [ArgumentsSource(nameof(Data))] - // public string TestHashValidateEnhancedNet8Plus(string key, string salt, string hash) - // { - // string hashed = BCryptExtendedV3.HashPassword(Hmackey, key, salt); - // var validateHashCheck = BCryptExtendedV3.Verify(Hmackey, key, hashed); - // return hashed + validateHashCheck.ToString(); - // } + [Benchmark] + [ArgumentsSource(nameof(Data))] + public string TestHashValidateEnhancedNet8Plus(string key, string salt, string hash) + { + string hashed = BCryptExtendedV3.HashPassword(Hmackey, key, salt); + var validateHashCheck = BCryptExtendedV3.Verify(Hmackey, key, hashed); + return hashed + validateHashCheck.ToString(); + } } } diff --git a/benchmark/TestVariantsOnStringBuilding.cs b/benchmark/TestVariantsOnStringBuilding.cs index f4026de..665012d 100644 --- a/benchmark/TestVariantsOnStringBuilding.cs +++ b/benchmark/TestVariantsOnStringBuilding.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Globalization; +using System.Text; using BCryptNet.BenchMarks._3._2._1; using BCryptNet.BenchMarks.EncodeB64; using BenchmarkDotNet.Attributes; @@ -82,7 +83,7 @@ public string Original_StrBuilder_SinEncoding_AppendChar_Sized_PRFmt() public string Original_StrBuilder_SinEncoding_AppendChar_Sized_PRFmt_MoreChar() { var result = new StringBuilder(60); - result.Append('$').Append('2').Append(bcryptMinorRevision).Append('$').Append(workFactor.ToString("D2")).Append('$') + result.Append('$').Append('2').Append(bcryptMinorRevision).Append('$').Append(workFactor.ToString("D2", CultureInfo.InvariantCulture)).Append('$') .Append(EncodedSaltAsChars) .Append(EncodedHashAsChars); @@ -121,7 +122,7 @@ public string Original_StrBuilder_SinEncoding_AppendChar_Sized_FROMSTRING_PRFmt_ result.Append("$2") .Append(bcryptMinorRevision) .Append("$") - .AppendFormat("{0:00}", workFactor) + .Append($"{workFactor:00}") .Append("$") .Append(salt) .Append(hash); diff --git a/benchmark/readme.md b/benchmark/readme.md index dee07e6..006278c 100644 --- a/benchmark/readme.md +++ b/benchmark/readme.md @@ -2,11 +2,11 @@ Running these -`dotnet run -c Release -f net48 -- --runtimes net48 net9.0 --platform x64` +`dotnet run -c Release -f net9.0 -- --runtimes net9.0 --platform x64` or -`dotnet run -c Release -f net48 -- --runtimes net48 net9.0 --filter * --stopOnFirstError --platform x64` +`dotnet run -c Release -f net9.0 -- --runtimes net9.0 --filter * --stopOnFirstError --platform x64` _Change the framework and remove the netfwk runtimes if running on linux._ diff --git a/src/BCrypt.Net.IdentityExtensions/BCryptPasswordHasher.cs b/src/BCrypt.Net.IdentityExtensions/BCryptPasswordHasher.cs index 3d25c0b..d21b65e 100644 --- a/src/BCrypt.Net.IdentityExtensions/BCryptPasswordHasher.cs +++ b/src/BCrypt.Net.IdentityExtensions/BCryptPasswordHasher.cs @@ -17,7 +17,6 @@ // IN THE SOFTWARE. // */ -using System; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; @@ -101,6 +100,6 @@ public PasswordVerificationResult VerifyHashedPassword(TUser user, string hashed private static bool CheckForAspIdentityHash(string hash) { if (hash[0] == '$') return false; - return hash.FromBase64().ToHex().StartsWith("00") || hash.FromBase64().ToHex().StartsWith("01"); + return hash.FromBase64().ToHex().StartsWith("00", StringComparison.InvariantCulture) || hash.FromBase64().ToHex().StartsWith("01", StringComparison.InvariantCulture); } } diff --git a/src/BCrypt.Net.IdentityExtensions/StringUtils.cs b/src/BCrypt.Net.IdentityExtensions/StringUtils.cs index 7f2dcf4..4cb897b 100644 --- a/src/BCrypt.Net.IdentityExtensions/StringUtils.cs +++ b/src/BCrypt.Net.IdentityExtensions/StringUtils.cs @@ -17,7 +17,7 @@ // IN THE SOFTWARE. // */ -using System; +using System.Globalization; namespace BCryptNet.IdentityExtensions; @@ -39,7 +39,11 @@ internal static string ToHex(this byte[] data) { try { - return BitConverter.ToString(data).Replace("-", "").ToLower(); +#if NET9_0_OR_GREATER + return Convert.ToHexStringLower(data); +#else + return BitConverter.ToString(data).Replace("-", "", StringComparison.InvariantCulture).ToLower(CultureInfo.InvariantCulture); +#endif } catch { diff --git a/src/BCrypt.Net/BCrypt.cs b/src/BCrypt.Net/BCrypt.cs index f212c9c..7f7ce63 100644 --- a/src/BCrypt.Net/BCrypt.cs +++ b/src/BCrypt.Net/BCrypt.cs @@ -179,13 +179,37 @@ public static string HashPassword(string inputKey, string salt) } #if NETCOREAPP + /// + /// Hash a password using the OpenBSD BCrypt scheme with a manually supplied salt/>. + /// + /// + /// You should generally leave generating salts to the library. + /// + /// The password to hash. + /// The log2 of the number of rounds of hashing to apply - the work + /// factor therefore increases as 2^workFactor. Default is 11 + /// The hashed password. + /// Thrown when the salt could not be parsed. public static string HashPassword(ReadOnlySpan inputKey, ReadOnlySpan salt) { Span outputBuffer = stackalloc char[60]; - BCrypt.HashPassword(inputKey, salt, outputBuffer, out var outputBufferWritten); + HashPassword(inputKey, salt, outputBuffer, out var outputBufferWritten); return new string(outputBuffer[..outputBufferWritten]); } + /// + /// Hash a password using the OpenBSD BCrypt scheme with a manually supplied salt/>. + /// + /// + /// You should generally leave generating salts to the library. + /// + /// The password to hash. + /// The log2 of the number of rounds of hashing to apply - the work + /// factor therefore increases as 2^workFactor. Default is 11 + /// + /// + /// The hashed password. + /// Thrown when the salt could not be parsed. public static void HashPassword(ReadOnlySpan inputKey, ReadOnlySpan salt, Span outputBuffer, out int outputBufferWritten) => CreatePasswordHash(inputKey, salt, outputBuffer, out outputBufferWritten); #endif diff --git a/src/BCrypt.Net/BCryptBase.Fwk.cs b/src/BCrypt.Net/BCryptBase.Fwk.cs index 8467072..56f5d43 100644 --- a/src/BCrypt.Net/BCryptBase.Fwk.cs +++ b/src/BCrypt.Net/BCryptBase.Fwk.cs @@ -411,7 +411,6 @@ private void EKSKey(byte[] saltBytes, byte[] inputBytes) /// The binary logarithm of the number of rounds of hashing to apply. /// A byte array containing the hashed result. internal byte[] CryptRaw(byte[] inputBytes, byte[] saltBytes, int workFactor) - { int i; int j; diff --git a/src/BCrypt.Net/BCryptBase.cs b/src/BCrypt.Net/BCryptBase.cs index 4d61896..ef222ed 100644 --- a/src/BCrypt.Net/BCryptBase.cs +++ b/src/BCrypt.Net/BCryptBase.cs @@ -506,7 +506,7 @@ internal static bool TryEncodeBase64(ReadOnlySpan byteArray, int length, S return true; } - internal static Span EncodeBase64(ReadOnlySpan byteArray, int length) + internal static ReadOnlySpan EncodeBase64(ReadOnlySpan byteArray, int length) { if (length <= 0 || length > byteArray.Length) { @@ -514,7 +514,7 @@ internal static Span EncodeBase64(ReadOnlySpan byteArray, int length } int encodedSize = (int)Math.Ceiling((length * 4D) / 3); - char[] encoded = new char[encodedSize]; + Span encoded = stackalloc char[encodedSize]; int pos = 0; int off = 0; @@ -548,7 +548,7 @@ internal static Span EncodeBase64(ReadOnlySpan byteArray, int length encoded[pos++] = Base64Code[c2 & 0x3f]; } - return encoded; + return encoded.ToArray(); } /// diff --git a/src/BCrypt.Net/BCryptExtendedV3.cs b/src/BCrypt.Net/BCryptExtendedV3.cs index 641e7da..15eb97d 100644 --- a/src/BCrypt.Net/BCryptExtendedV3.cs +++ b/src/BCrypt.Net/BCryptExtendedV3.cs @@ -1,7 +1,7 @@ -using System.Security.Cryptography; +#if NETCOREAPP +using System.Security.Cryptography; namespace BCryptNet; -#if NET8_0_OR_GREATER /// /// BCrypt Enhanced /// Created to be compatible with other programming-language implementations of pre-hashed keys @@ -33,7 +33,10 @@ public static string HashPassword(string hmacKey, string inputKey, int workFacto return CreatePasswordHash(inputKey, GenerateSalt(workFactor), hashType, (key, type, version) => EnhancedHash(hmacKey, key, type, version)); } - /// + /// + /// Pre-hash a password with HMAC-SHA3-384 then using the OpenBSD BCrypt scheme and a salt generated by . + /// + /// /// /// You should generally leave generating salts to the library. /// diff --git a/src/BCrypt.Net/HashParser.cs b/src/BCrypt.Net/HashParser.cs index 4f60db7..bc56ffa 100644 --- a/src/BCrypt.Net/HashParser.cs +++ b/src/BCrypt.Net/HashParser.cs @@ -105,8 +105,9 @@ internal static bool IsValidHash(string hash, out HashFormatDescriptor format) } // Validate workfactor - if (!IsAsciiNumeric(hash[offset++]) - || !IsAsciiNumeric(hash[offset++])) +#pragma warning disable S1764 + if (!IsAsciiNumeric(hash[offset++]) || !IsAsciiNumeric(hash[offset++])) +#pragma warning restore S1764 { format = null; return false; diff --git a/tests/UnitTests/BCrypt.Net.UnitTests.csproj b/tests/UnitTests/BCrypt.Net.UnitTests.csproj index f80933c..44b7522 100644 --- a/tests/UnitTests/BCrypt.Net.UnitTests.csproj +++ b/tests/UnitTests/BCrypt.Net.UnitTests.csproj @@ -2,7 +2,7 @@ BCrypt.Net.UnitTests BCryptNet.UnitTests - net9.0 + net9.0;net48 Debug default diff --git a/tests/UnitTests/BCryptTests.cs b/tests/UnitTests/BCryptTests.cs index 03a7582..645c25d 100644 --- a/tests/UnitTests/BCryptTests.cs +++ b/tests/UnitTests/BCryptTests.cs @@ -230,6 +230,7 @@ public void TestHashPassword() Trace.WriteLine(""); } +#if NETCOREAPP /** * Test method for 'BCrypt.HashPassword(string, string)' */ @@ -312,7 +313,7 @@ public void TestHashPasswordSpanBuffer() Trace.WriteLine(sw.ElapsedMilliseconds); Trace.WriteLine(""); } - +#endif /** * Test method for 'BCrypt.HashPassword(string, string)' diff --git a/tests/UnitTests/BCryptTestsExtendedv3.cs b/tests/UnitTests/BCryptTestsExtendedv3.cs index 493faad..c9651ba 100644 --- a/tests/UnitTests/BCryptTestsExtendedv3.cs +++ b/tests/UnitTests/BCryptTestsExtendedv3.cs @@ -26,7 +26,7 @@ IN THE SOFTWARE. namespace BCryptNet.UnitTests { -#if NET8_0_OR_GREATER +#if NETCOREAPP /// /// BCrypt tests From 671dfe3f784c1ede61849cada32062defee9d1ec Mon Sep 17 00:00:00 2001 From: Chris McKee Date: Tue, 1 Apr 2025 19:02:37 +0100 Subject: [PATCH 05/27] chore: cleanup benchmarks --- benchmark/3.2.1/BaseLine.cs | 2 +- benchmark/3.5.perfmerge_1/PerfMerge1.cs | 2 +- benchmark/DecodeB64/Decoder.cs | 37 +++++++-- benchmark/HashParser/HashParser.cs | 6 +- benchmark/InterrogateHashBenchmarks.cs | 32 -------- benchmark/Program.cs | 13 +-- benchmark/TestB64Decoder.cs | 22 ++--- benchmark/TestB64Encoder.cs | 2 +- .../TestBcryptHashingEnhancedValidation.cs | 81 +++++++++++++++++++ ...on.cs => TestBcrypt_Hash_Interrogation.cs} | 13 ++- benchmark/TestBcrypt_Hashing.cs | 40 ++------- benchmark/TestBcrypt_Hashing_Enhanced.cs | 62 +++----------- benchmark/TestBcrypt_Hashing_Validation.cs | 75 +++++++++++++++++ benchmark/TestVariantsOnStringBuilding.cs | 4 +- 14 files changed, 243 insertions(+), 148 deletions(-) delete mode 100644 benchmark/InterrogateHashBenchmarks.cs create mode 100644 benchmark/TestBcryptHashingEnhancedValidation.cs rename benchmark/{TestBcrypt_HashInterrogation.cs => TestBcrypt_Hash_Interrogation.cs} (77%) create mode 100644 benchmark/TestBcrypt_Hashing_Validation.cs diff --git a/benchmark/3.2.1/BaseLine.cs b/benchmark/3.2.1/BaseLine.cs index 86fef14..68cf9c0 100644 --- a/benchmark/3.2.1/BaseLine.cs +++ b/benchmark/3.2.1/BaseLine.cs @@ -7,7 +7,7 @@ // BASE namespace BCryptNet.BenchMarks._3._2._1 { - public class BaseLine + public class BCryptBaseLine { [Serializable] diff --git a/benchmark/3.5.perfmerge_1/PerfMerge1.cs b/benchmark/3.5.perfmerge_1/PerfMerge1.cs index 11b5825..555f5c7 100644 --- a/benchmark/3.5.perfmerge_1/PerfMerge1.cs +++ b/benchmark/3.5.perfmerge_1/PerfMerge1.cs @@ -6,7 +6,7 @@ // BASE namespace BCryptNet.BenchMarks._3._5.perfmerge_1 { - public class PerfMerge1 + public class BCrypt305PerfMerge1 { /// BCrypt implementation. /// diff --git a/benchmark/DecodeB64/Decoder.cs b/benchmark/DecodeB64/Decoder.cs index 00793b9..07bd388 100644 --- a/benchmark/DecodeB64/Decoder.cs +++ b/benchmark/DecodeB64/Decoder.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.CompilerServices; using System.Text; namespace BCryptNet.BenchMarks.DecodeB64 @@ -21,6 +22,7 @@ internal static class DecodeB64Methods 51, 52, 53, -1, -1, -1, -1, -1 }; + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int Char64(char character) { return character < 0 || character > Index64.Length ? -1 : Index64[character]; @@ -77,8 +79,6 @@ public static byte[] DecodeBase64ToBytes(string encodedString, int maximumBytes) return result; } -#if !NETFRAMEWORK || NETCOREAPP - internal static byte[] DecodeBase64StringCreateSpan(string encodedString, int maximumBytes) { @@ -142,8 +142,6 @@ internal static byte[] DecodeBase64StringCreateSpan(string encodedString, int ma return ret.ToArray(); } -#endif - internal static byte[] DecodeBase64StandardSized(string encodedString, int maximumBytes) { int sourceLength = encodedString.Length; @@ -197,7 +195,6 @@ internal static byte[] DecodeBase64StandardSized(string encodedString, int maxim } - internal static byte[] DecodeBase64StandardUnSized(string encodedString, int maximumBytes) { @@ -251,5 +248,35 @@ internal static byte[] DecodeBase64StandardUnSized(string encodedString, int max return bval; } + + public static int DecodeBase64(ReadOnlySpan encodedSpan, Span destination) + { + int outputLength = 0; + int position = 0; + + while (position < encodedSpan.Length - 1 && outputLength < destination.Length) + { + int c1 = Char64(encodedSpan[position++]); + int c2 = Char64(encodedSpan[position++]); + if (c1 == -1 || c2 == -1) break; + + destination[outputLength] = (byte)((c1 << 2) | ((c2 & 0x30) >> 4)); + if (++outputLength >= destination.Length || position >= encodedSpan.Length) break; + + int c3 = Char64(encodedSpan[position++]); + if (c3 == -1) break; + + destination[outputLength] = (byte)(((c2 & 0x0F) << 4) | ((c3 & 0x3C) >> 2)); + if (++outputLength >= destination.Length || position >= encodedSpan.Length) break; + + int c4 = Char64(encodedSpan[position++]); + if (c4 == -1) break; + + destination[outputLength] = (byte)(((c3 & 0x03) << 6) | c4); + ++outputLength; + } + + return outputLength; + } } } diff --git a/benchmark/HashParser/HashParser.cs b/benchmark/HashParser/HashParser.cs index 35f5f07..df7453c 100644 --- a/benchmark/HashParser/HashParser.cs +++ b/benchmark/HashParser/HashParser.cs @@ -8,14 +8,14 @@ internal static class Decoder private static readonly HashFormatDescriptor OldFormatDescriptor = new HashFormatDescriptor(versionLength: 1); private static readonly HashFormatDescriptor NewFormatDescriptor = new HashFormatDescriptor(versionLength: 2); - public static BaseLine.HashInformation GetHashInformation(string hash) + public static BCryptBaseLine.HashInformation GetHashInformation(string hash) { if (!IsValidHash(hash, out var format)) { ThrowInvalidHashFormat(); } - return new BaseLine.HashInformation( + return new BCryptBaseLine.HashInformation( hash.Substring(0, format.SettingLength), hash.Substring(1, format.VersionLength), hash.Substring(format.WorkfactorOffset, 2), @@ -125,7 +125,7 @@ private static bool IsAsciiNumeric(char value) private static void ThrowInvalidHashFormat() { - throw new BaseLine.SaltParseException("Invalid Hash Format"); + throw new BCryptBaseLine.SaltParseException("Invalid Hash Format"); } private class HashFormatDescriptor diff --git a/benchmark/InterrogateHashBenchmarks.cs b/benchmark/InterrogateHashBenchmarks.cs deleted file mode 100644 index 5660483..0000000 --- a/benchmark/InterrogateHashBenchmarks.cs +++ /dev/null @@ -1,32 +0,0 @@ -using BCryptNet.BenchMarks._3._2._1; -using BCryptNet.BenchMarks.HashParser; -using BenchmarkDotNet.Attributes; - -#pragma warning disable 1591 - -namespace BCryptNet.BenchMarks -{ - [MemoryDiagnoser] - [RPlotExporter, RankColumn] - [KeepBenchmarkFiles] - public class InterrogateHashBenchmarks - { - - [Benchmark(Baseline = true)] - [Arguments("$2a$12$k42ZFHFWqBp3vWli.nIn8uYyIkbvYRvodzbfbK18SSsY.CsIQPlxO")] - [Arguments( "$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq")] - public void InterrogateHashUsingRegex(string hash) - { - BaseLine.BCrypt.InterrogateHash(hash); - } - - [Benchmark()] - [Arguments("$2a$12$k42ZFHFWqBp3vWli.nIn8uYyIkbvYRvodzbfbK18SSsY.CsIQPlxO")] - [Arguments( "$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq")] - public void InterrogateHashUsingParser(string hash) - { - Decoder.GetHashInformation(hash); - } - - } -} diff --git a/benchmark/Program.cs b/benchmark/Program.cs index e5eea6d..ee80bb0 100644 --- a/benchmark/Program.cs +++ b/benchmark/Program.cs @@ -14,18 +14,19 @@ class Program static void Main(string[] args) { #if DEBUG - BenchmarkRunner.Run(new DebugInProcessConfig().AddValidator(ExecutionValidator.FailOnError)); + BenchmarkRunner.Run(new DebugInProcessConfig().AddValidator(ExecutionValidator.FailOnError)); #else var config = DefaultConfig.Instance .With(Job.Default.With(CoreRuntime.Core90)) ; - // BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config); - + BenchmarkRunner.Run(config); - BenchmarkRunner.Run(config); - + BenchmarkRunner.Run(config); + BenchmarkRunner.Run(config); + BenchmarkRunner.Run(config); + // Tests for testing in isolation - BenchmarkRunner.Run(config); + BenchmarkRunner.Run(config); BenchmarkRunner.Run(config); BenchmarkRunner.Run(config); BenchmarkRunner.Run(config); diff --git a/benchmark/TestB64Decoder.cs b/benchmark/TestB64Decoder.cs index 6c3ac58..fbb01cf 100644 --- a/benchmark/TestB64Decoder.cs +++ b/benchmark/TestB64Decoder.cs @@ -1,4 +1,5 @@ -using BCryptNet.BenchMarks.DecodeB64; +using System; +using BCryptNet.BenchMarks.DecodeB64; using BenchmarkDotNet.Attributes; #pragma warning disable 1591 @@ -27,7 +28,6 @@ public byte[] DecodeBase64StandardSized(string salt) return DecodeB64Methods.DecodeBase64StandardSized(salt, 16); } -#if !NETFRAMEWORK [Benchmark] [Arguments("DCq7YPn5Rq63x1Lad4cll.")] [Arguments("HqWuK6/Ng6sg9gQzbLrgb.")] @@ -35,23 +35,23 @@ public byte[] DecodeBase64StringCreateSpan(string salt) { return DecodeB64Methods.DecodeBase64StringCreateSpan(salt, 16); } -#else - [Benchmark(Description = "Deliberately Ignore")] + + [Benchmark] [Arguments("DCq7YPn5Rq63x1Lad4cll.")] [Arguments("HqWuK6/Ng6sg9gQzbLrgb.")] - public byte[] DecodeBase64StringCreateSpan(string salt) + public byte[] DecodeBase64ToBytes(string salt) { - // Deliberately empty https://github.com/dotnet/BenchmarkDotNet/issues/1863#issuecomment-988288587 - return null; + return DecodeB64Methods.DecodeBase64ToBytes(salt, 16); } -#endif - + [Benchmark] [Arguments("DCq7YPn5Rq63x1Lad4cll.")] [Arguments("HqWuK6/Ng6sg9gQzbLrgb.")] - public byte[] DecodeBase64ToBytes(string salt) + public byte[] DecodeBase64ToSpan(string salt) { - return DecodeB64Methods.DecodeBase64ToBytes(salt, 16); + Span saltBuffer = stackalloc byte[128 / 8]; + int written = DecodeB64Methods.DecodeBase64(salt, saltBuffer); + return saltBuffer[..written].ToArray(); } } diff --git a/benchmark/TestB64Encoder.cs b/benchmark/TestB64Encoder.cs index 93d2f81..6c14414 100644 --- a/benchmark/TestB64Encoder.cs +++ b/benchmark/TestB64Encoder.cs @@ -11,7 +11,7 @@ namespace BCryptNet.BenchMarks [KeepBenchmarkFiles] public class TestB64Encoder { - private static readonly byte[] SaltBytes = BaseLine.BCrypt.DecodeBase64("sGBxdT2q8Qd84NyZEkwTY.", 16); + private static readonly byte[] SaltBytes = BCryptBaseLine.BCrypt.DecodeBase64("sGBxdT2q8Qd84NyZEkwTY.", 16); [Benchmark(Baseline = true)] public void EncodeBase64Unsized() diff --git a/benchmark/TestBcryptHashingEnhancedValidation.cs b/benchmark/TestBcryptHashingEnhancedValidation.cs new file mode 100644 index 0000000..c33ced4 --- /dev/null +++ b/benchmark/TestBcryptHashingEnhancedValidation.cs @@ -0,0 +1,81 @@ +// /* +// The MIT License (MIT) +// Copyright (c) 2006 Damien Miller djm@mindrot.org (jBCrypt) +// Copyright (c) 2013 Ryan D. Emerle (.Net port) +// Copyright (c) 2016/2025 Chris McKee (.Net-core port / patches / new features) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +// (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, +// merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished +// to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// */ + +using System; +using System.Collections.Generic; +using BCryptNet.BenchMarks._3._2._1; +using BCryptNet.BenchMarks._3._5.perfmerge_1; +using BCryptNet.BenchMarks._4._0._0; +using BenchmarkDotNet.Attributes; + +namespace BCryptNet.BenchMarks; + +#pragma warning disable 1591 +[MemoryDiagnoser] +[RPlotExporter, RankColumn] +[KeepBenchmarkFiles] +public class TestBcryptHashingEnhancedValidation +{ + public IEnumerable Data() + { + yield return ["~!@#$%^&*() ~!@#$%^&*()PNBFRD"]; + } + + private readonly string _baselineEnhancedHash = BCryptBaseLine.BCrypt.EnhancedHashPassword("~!@#$%^&*() ~!@#$%^&*()PNBFRD", BCryptBaseLine.HashType.SHA384, 12); + private readonly string _bCrypt305PerfMerge1EnhancedHash = BCrypt305PerfMerge1.BCrypt.EnhancedHashPassword("~!@#$%^&*() ~!@#$%^&*()PNBFRD", BCrypt305PerfMerge1.HashType.SHA384, 12); + private readonly string _bCryptV4PerfMerge1EnhancedHash = BCryptV4.BCrypt.EnhancedHashPassword("~!@#$%^&*() ~!@#$%^&*()PNBFRD", BCryptV4.HashType.SHA384, 12); + private readonly string _bCryptExtendedV2EnhancedHash = BCryptExtendedV2.HashPassword("~!@#$%^&*() ~!@#$%^&*()PNBFRD", 12, HashType.SHA384); + private static readonly string Hmackey = Guid.NewGuid().ToString(); + private readonly string _bCryptExtendedV3EnhancedHash = BCryptExtendedV3.HashPassword(Hmackey, "~!@#$%^&*() ~!@#$%^&*()PNBFRD", 12, HashType.SHA384); + + [Benchmark(Baseline = true)] + [ArgumentsSource(nameof(Data))] + public bool TestHashValidateEnhanced(string key) + { + return BCryptBaseLine.BCrypt.EnhancedVerify(key, _baselineEnhancedHash); + } + + [Benchmark] + [ArgumentsSource(nameof(Data))] + public bool TestHashValidateEnhancedv305Perf1(string key) + { + return BCrypt305PerfMerge1.BCrypt.EnhancedVerify(key, _bCrypt305PerfMerge1EnhancedHash); + } + + [Benchmark] + [ArgumentsSource(nameof(Data))] + public bool TestHashValidateEnhancedv4Perf(string key) + { + return BCryptV4.BCrypt.EnhancedVerify(key, _bCryptV4PerfMerge1EnhancedHash); + } + + [Benchmark] + [ArgumentsSource(nameof(Data))] + public bool TestHashValidateEnhancedCurrent(string key) + { + return BCryptExtendedV2.Verify(key, _bCryptExtendedV2EnhancedHash); + } + + [Benchmark] + [ArgumentsSource(nameof(Data))] + public bool TestHashValidateEnhancedNet8Plus(string key) + { + return BCryptExtendedV3.Verify(Hmackey, key, _bCryptExtendedV3EnhancedHash); + } +} diff --git a/benchmark/TestBcrypt_HashInterrogation.cs b/benchmark/TestBcrypt_Hash_Interrogation.cs similarity index 77% rename from benchmark/TestBcrypt_HashInterrogation.cs rename to benchmark/TestBcrypt_Hash_Interrogation.cs index 49ab54b..344f61a 100644 --- a/benchmark/TestBcrypt_HashInterrogation.cs +++ b/benchmark/TestBcrypt_Hash_Interrogation.cs @@ -1,6 +1,7 @@ using BCryptNet.BenchMarks._3._2._1; using BCryptNet.BenchMarks._4._0._0; using BCryptNet.BenchMarks._4._0._3; +using BCryptNet.BenchMarks.HashParser; using BenchmarkDotNet.Attributes; #pragma warning disable 1591 @@ -10,14 +11,14 @@ namespace BCryptNet.BenchMarks [MemoryDiagnoser] [RPlotExporter, RankColumn] [KeepBenchmarkFiles] - public class TestBcrypt_HashInterrogation + public class TestBcrypt_Hash_Interrogation { [Benchmark(Baseline = true)] [Arguments("$2a$12$k42ZFHFWqBp3vWli.nIn8uYyIkbvYRvodzbfbK18SSsY.CsIQPlxO")] [Arguments( "$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq")] public void InterrogateHashUsingRegex(string hash) { - BaseLine.BCrypt.InterrogateHash(hash); + BCryptBaseLine.BCrypt.InterrogateHash(hash); } [Benchmark()] @@ -43,5 +44,13 @@ public void InterrogateHashUsingParserCurrent(string hash) { BCrypt.InterrogateHash(hash); } + + [Benchmark()] + [Arguments("$2a$12$k42ZFHFWqBp3vWli.nIn8uYyIkbvYRvodzbfbK18SSsY.CsIQPlxO")] + [Arguments( "$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq")] + public void InterrogateHashUsingParser(string hash) + { + Decoder.GetHashInformation(hash); + } } } diff --git a/benchmark/TestBcrypt_Hashing.cs b/benchmark/TestBcrypt_Hashing.cs index 714d9aa..24f4821 100644 --- a/benchmark/TestBcrypt_Hashing.cs +++ b/benchmark/TestBcrypt_Hashing.cs @@ -17,24 +17,6 @@ public class TestBcrypt_Hashing { public IEnumerable Data() { - yield return ["", "$2a$06$DCq7YPn5Rq63x1Lad4cll.", "$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s."]; - yield return ["", "$2a$08$HqWuK6/Ng6sg9gQzbLrgb.", "$2a$08$HqWuK6/Ng6sg9gQzbLrgb.Tl.ZHfXLhvt/SgVyWhQqgqcZ7ZuUtye"]; - yield return ["", "$2a$10$k1wbIrmNyFAPwPVPSVa/ze", "$2a$10$k1wbIrmNyFAPwPVPSVa/zecw2BCEnBwVS2GbrmgzxFUOqW9dk4TCW"]; - yield return ["", "$2a$12$k42ZFHFWqBp3vWli.nIn8u", "$2a$12$k42ZFHFWqBp3vWli.nIn8uYyIkbvYRvodzbfbK18SSsY.CsIQPlxO"]; - yield return ["a", "$2a$06$m0CrhHm10qJ3lXRY.5zDGO", "$2a$06$m0CrhHm10qJ3lXRY.5zDGO3rS2KdeeWLuGmsfGlMfOxih58VYVfxe"]; - yield return ["a", "$2a$08$cfcvVd2aQ8CMvoMpP2EBfe", "$2a$08$cfcvVd2aQ8CMvoMpP2EBfeodLEkkFJ9umNEfPD18.hUF62qqlC/V."]; - yield return ["a", "$2a$10$k87L/MF28Q673VKh8/cPi.", "$2a$10$k87L/MF28Q673VKh8/cPi.SUl7MU/rWuSiIDDFayrKk/1tBsSQu4u"]; - yield return ["a", "$2a$12$8NJH3LsPrANStV6XtBakCe", "$2a$12$8NJH3LsPrANStV6XtBakCez0cKHXVxmvxIlcz785vxAIZrihHZpeS"]; - yield return ["abc", "$2a$06$If6bvum7DFjUnE9p2uDeDu", "$2a$06$If6bvum7DFjUnE9p2uDeDu0YHzrHM6tf.iqN8.yx.jNN1ILEf7h0i"]; - yield return ["abc", "$2a$08$Ro0CUfOqk6cXEKf3dyaM7O", "$2a$08$Ro0CUfOqk6cXEKf3dyaM7OhSCvnwM9s4wIX9JeLapehKK5YdLxKcm"]; - yield return ["abc", "$2a$10$WvvTPHKwdBJ3uk0Z37EMR.", "$2a$10$WvvTPHKwdBJ3uk0Z37EMR.hLA2W6N9AEBhEgrAOljy2Ae5MtaSIUi"]; - yield return ["abc", "$2a$12$EXRkfkdmXn2gzds2SSitu.", "$2a$12$EXRkfkdmXn2gzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q"]; - yield return ["abcdefghijklmnopqrstuvwxyz", "$2a$06$.rCVZVOThsIa97pEDOxvGu", "$2a$06$.rCVZVOThsIa97pEDOxvGuRRgzG64bvtJ0938xuqzv18d3ZpQhstC"]; - yield return ["abcdefghijklmnopqrstuvwxyz", "$2a$08$aTsUwsyowQuzRrDqFflhge", "$2a$08$aTsUwsyowQuzRrDqFflhgekJ8d9/7Z3GV3UcgvzQW3J5zMyrTvlz."]; - yield return ["abcdefghijklmnopqrstuvwxyz", "$2a$10$fVH8e28OQRj9tqiDXs1e1u", "$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq"]; - yield return ["abcdefghijklmnopqrstuvwxyz", "$2a$12$D4G5f18o7aMMfwasBL7Gpu", "$2a$12$D4G5f18o7aMMfwasBL7GpuQWuP3pkrZrOAnqP.bmezbMng.QwJ/pG"]; - yield return ["~!@#$%^&*() ~!@#$%^&*()PNBFRD", "$2a$06$fPIsBO8qRqkjj273rfaOI.", "$2a$06$fPIsBO8qRqkjj273rfaOI.HtSV9jLDpTbZn782DC6/t7qT67P6FfO"]; - yield return ["~!@#$%^&*() ~!@#$%^&*()PNBFRD", "$2a$08$Eq2r4G/76Wv39MzSX262hu", "$2a$08$Eq2r4G/76Wv39MzSX262huzPz612MZiYHVUJe/OcOql2jo4.9UxTW"]; yield return ["~!@#$%^&*() ~!@#$%^&*()PNBFRD", "$2a$10$LgfYWkbzEvQ4JakH7rOvHe", "$2a$10$LgfYWkbzEvQ4JakH7rOvHe0y8pHKF9OaFgwUZ2q7W2FFZmZzJYlfS"]; yield return ["~!@#$%^&*() ~!@#$%^&*()PNBFRD", "$2a$12$WApznUOJfkEGSmYRfnkrPO", "$2a$12$WApznUOJfkEGSmYRfnkrPOr466oFDCaj4b6HY3EXGvfxm43seyhgC"]; } @@ -43,45 +25,35 @@ public IEnumerable Data() [ArgumentsSource(nameof(Data))] public string TestHashValidate(string key, string salt, string hash) { - string hashed = BaseLine.BCrypt.HashPassword(key, salt, enhancedEntropy: false); - var validateHashCheck = BaseLine.BCrypt.Verify(key, hashed); - return hashed + validateHashCheck.ToString(); + return BCryptBaseLine.BCrypt.HashPassword(key, salt, enhancedEntropy: false); } [Benchmark] [ArgumentsSource(nameof(Data))] public string TestHashValidatePerf1(string key, string salt, string hash) { - string hashed = PerfMerge1.BCrypt.HashPassword(key, salt, enhancedEntropy: false); - var validateHashCheck = PerfMerge1.BCrypt.Verify(key, hashed); - return hashed + validateHashCheck.ToString(); + return BCrypt305PerfMerge1.BCrypt.HashPassword(key, salt, enhancedEntropy: false); } [Benchmark] [ArgumentsSource(nameof(Data))] public string TestHashValidateV4(string key, string salt, string hash) { - string hashed = BCryptV4.BCrypt.HashPassword(key, salt); - var validateHashCheck = BCryptV4.BCrypt.Verify(key, hashed); - return hashed + validateHashCheck.ToString(); + return BCryptV4.BCrypt.HashPassword(key, salt); } [Benchmark] [ArgumentsSource(nameof(Data))] public string TestHashValidateV403(string key, string salt, string hash) { - string hashed = BCryptV403.BCrypt.HashPassword(key, salt); - var validateHashCheck = BCryptV403.BCrypt.Verify(key, hashed); - return hashed + validateHashCheck.ToString(); + return BCryptV403.BCrypt.HashPassword(key, salt); } - + [Benchmark] [ArgumentsSource(nameof(Data))] public string TestHashValidateCurrent(string key, string salt, string hash) { - string hashed = BCrypt.HashPassword(key, salt); - var validateHashCheck = BCrypt.Verify(key, hashed); - return hashed + validateHashCheck.ToString(); + return BCrypt.HashPassword(key, salt); } } } diff --git a/benchmark/TestBcrypt_Hashing_Enhanced.cs b/benchmark/TestBcrypt_Hashing_Enhanced.cs index 5ab565b..b5d4679 100644 --- a/benchmark/TestBcrypt_Hashing_Enhanced.cs +++ b/benchmark/TestBcrypt_Hashing_Enhanced.cs @@ -12,86 +12,48 @@ namespace BCryptNet.BenchMarks [MemoryDiagnoser] [RPlotExporter, RankColumn] [KeepBenchmarkFiles] - public class TestBcrypt_Hashing_Enhanced + public class TestBcryptHashingEnhanced { public IEnumerable Data() { - yield return new object[] { "", "$2a$06$DCq7YPn5Rq63x1Lad4cll.", "$2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s." }; - yield return new object[] { "", "$2a$08$HqWuK6/Ng6sg9gQzbLrgb.", "$2a$08$HqWuK6/Ng6sg9gQzbLrgb.Tl.ZHfXLhvt/SgVyWhQqgqcZ7ZuUtye" }; - yield return new object[] { "", "$2a$10$k1wbIrmNyFAPwPVPSVa/ze", "$2a$10$k1wbIrmNyFAPwPVPSVa/zecw2BCEnBwVS2GbrmgzxFUOqW9dk4TCW" }; - yield return new object[] { "", "$2a$12$k42ZFHFWqBp3vWli.nIn8u", "$2a$12$k42ZFHFWqBp3vWli.nIn8uYyIkbvYRvodzbfbK18SSsY.CsIQPlxO" }; - yield return new object[] { "a", "$2a$06$m0CrhHm10qJ3lXRY.5zDGO", "$2a$06$m0CrhHm10qJ3lXRY.5zDGO3rS2KdeeWLuGmsfGlMfOxih58VYVfxe" }; - yield return new object[] { "a", "$2a$08$cfcvVd2aQ8CMvoMpP2EBfe", "$2a$08$cfcvVd2aQ8CMvoMpP2EBfeodLEkkFJ9umNEfPD18.hUF62qqlC/V." }; - yield return new object[] { "a", "$2a$10$k87L/MF28Q673VKh8/cPi.", "$2a$10$k87L/MF28Q673VKh8/cPi.SUl7MU/rWuSiIDDFayrKk/1tBsSQu4u" }; - yield return new object[] { "a", "$2a$12$8NJH3LsPrANStV6XtBakCe", "$2a$12$8NJH3LsPrANStV6XtBakCez0cKHXVxmvxIlcz785vxAIZrihHZpeS" }; - yield return new object[] { "abc", "$2a$06$If6bvum7DFjUnE9p2uDeDu", "$2a$06$If6bvum7DFjUnE9p2uDeDu0YHzrHM6tf.iqN8.yx.jNN1ILEf7h0i" }; - yield return new object[] { "abc", "$2a$08$Ro0CUfOqk6cXEKf3dyaM7O", "$2a$08$Ro0CUfOqk6cXEKf3dyaM7OhSCvnwM9s4wIX9JeLapehKK5YdLxKcm" }; - yield return new object[] { "abc", "$2a$10$WvvTPHKwdBJ3uk0Z37EMR.", "$2a$10$WvvTPHKwdBJ3uk0Z37EMR.hLA2W6N9AEBhEgrAOljy2Ae5MtaSIUi" }; - yield return new object[] { "abc", "$2a$12$EXRkfkdmXn2gzds2SSitu.", "$2a$12$EXRkfkdmXn2gzds2SSitu.MW9.gAVqa9eLS1//RYtYCmB1eLHg.9q" }; - yield return new object[] { "abcdefghijklmnopqrstuvwxyz", "$2a$06$.rCVZVOThsIa97pEDOxvGu", "$2a$06$.rCVZVOThsIa97pEDOxvGuRRgzG64bvtJ0938xuqzv18d3ZpQhstC" }; - yield return new object[] { "abcdefghijklmnopqrstuvwxyz", "$2a$08$aTsUwsyowQuzRrDqFflhge", "$2a$08$aTsUwsyowQuzRrDqFflhgekJ8d9/7Z3GV3UcgvzQW3J5zMyrTvlz." }; - yield return new object[] { "abcdefghijklmnopqrstuvwxyz", "$2a$10$fVH8e28OQRj9tqiDXs1e1u", "$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq" }; - yield return new object[] { "abcdefghijklmnopqrstuvwxyz", "$2a$12$D4G5f18o7aMMfwasBL7Gpu", "$2a$12$D4G5f18o7aMMfwasBL7GpuQWuP3pkrZrOAnqP.bmezbMng.QwJ/pG" }; - yield return new object[] { "~!@#$%^&*() ~!@#$%^&*()PNBFRD", "$2a$06$fPIsBO8qRqkjj273rfaOI.", "$2a$06$fPIsBO8qRqkjj273rfaOI.HtSV9jLDpTbZn782DC6/t7qT67P6FfO" }; - yield return new object[] { "~!@#$%^&*() ~!@#$%^&*()PNBFRD", "$2a$08$Eq2r4G/76Wv39MzSX262hu", "$2a$08$Eq2r4G/76Wv39MzSX262huzPz612MZiYHVUJe/OcOql2jo4.9UxTW" }; - yield return new object[] { "~!@#$%^&*() ~!@#$%^&*()PNBFRD", "$2a$10$LgfYWkbzEvQ4JakH7rOvHe", "$2a$10$LgfYWkbzEvQ4JakH7rOvHe0y8pHKF9OaFgwUZ2q7W2FFZmZzJYlfS" }; - yield return new object[] { "~!@#$%^&*() ~!@#$%^&*()PNBFRD", "$2a$12$WApznUOJfkEGSmYRfnkrPO", "$2a$12$WApznUOJfkEGSmYRfnkrPOr466oFDCaj4b6HY3EXGvfxm43seyhgC" }; + yield return ["~!@#$%^&*() ~!@#$%^&*()PNBFRD", "$2a$10$LgfYWkbzEvQ4JakH7rOvHe"]; } [Benchmark(Baseline = true)] [ArgumentsSource(nameof(Data))] - public string TestHashValidateEnhanced(string key, string salt, string hash) + public string TestHashValidateEnhanced(string key, string salt) { - string hashed = BaseLine.BCrypt.HashPassword(key, salt, enhancedEntropy: true); - var validateHashCheck = BaseLine.BCrypt.EnhancedVerify(key, hashed); - return hashed + validateHashCheck.ToString(); + return BCryptBaseLine.BCrypt.HashPassword(key, salt, enhancedEntropy: true); } [Benchmark] [ArgumentsSource(nameof(Data))] - public string TestHashValidateEnhancedPerf1(string key, string salt, string hash) + public string TestHashValidateEnhancedv3Perf(string key, string salt) { - string hashed = PerfMerge1.BCrypt.HashPassword(key, salt, enhancedEntropy: true); - var validateHashCheck = PerfMerge1.BCrypt.EnhancedVerify(key, hashed); - return hashed + validateHashCheck.ToString(); + return BCrypt305PerfMerge1.BCrypt.HashPassword(key, salt, enhancedEntropy: true); } [Benchmark] [ArgumentsSource(nameof(Data))] - public string TestHashValidateEnhancedv3perf(string key, string salt, string hash) + public string TestHashValidateEnhancedv4Perf(string key, string salt) { - string hashed = PerfMerge1.BCrypt.HashPassword(key, salt, enhancedEntropy: true); - var validateHashCheck = PerfMerge1.BCrypt.EnhancedVerify(key, hashed); - return hashed + validateHashCheck.ToString(); + return BCryptV4.BCrypt.HashPassword(key, salt, enhancedEntropy: true); } [Benchmark] [ArgumentsSource(nameof(Data))] - public string TestHashValidateEnhancedv4perf(string key, string salt, string hash) + public string TestHashValidateEnhancedCurrent(string key, string salt) { - string hashed = BCryptV4.BCrypt.HashPassword(key, salt, enhancedEntropy: true); - var validateHashCheck = BCryptV4.BCrypt.EnhancedVerify(key, hashed); - return hashed + validateHashCheck.ToString(); - } - - [Benchmark] - [ArgumentsSource(nameof(Data))] - public string TestHashValidateEnhancedCurrent(string key, string salt, string hash) - { - string hashed = BCryptExtendedV2.HashPassword(key, salt); - var validateHashCheck = BCryptExtendedV2.Verify(key, hashed); - return hashed + validateHashCheck.ToString(); + return BCryptExtendedV2.HashPassword(key, salt); } private static readonly string Hmackey = Guid.NewGuid().ToString(); [Benchmark] [ArgumentsSource(nameof(Data))] - public string TestHashValidateEnhancedNet8Plus(string key, string salt, string hash) + public string TestHashValidateEnhancedNet8Plus(string key, string salt) { - string hashed = BCryptExtendedV3.HashPassword(Hmackey, key, salt); - var validateHashCheck = BCryptExtendedV3.Verify(Hmackey, key, hashed); - return hashed + validateHashCheck.ToString(); + return BCryptExtendedV3.HashPassword(Hmackey, key, salt); } } } diff --git a/benchmark/TestBcrypt_Hashing_Validation.cs b/benchmark/TestBcrypt_Hashing_Validation.cs new file mode 100644 index 0000000..5c81ae6 --- /dev/null +++ b/benchmark/TestBcrypt_Hashing_Validation.cs @@ -0,0 +1,75 @@ +// /* +// The MIT License (MIT) +// Copyright (c) 2006 Damien Miller djm@mindrot.org (jBCrypt) +// Copyright (c) 2013 Ryan D. Emerle (.Net port) +// Copyright (c) 2016/2025 Chris McKee (.Net-core port / patches / new features) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +// (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, +// merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished +// to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// */ + +using System.Collections.Generic; +using BCryptNet.BenchMarks._3._2._1; +using BCryptNet.BenchMarks._3._5.perfmerge_1; +using BCryptNet.BenchMarks._4._0._0; +using BCryptNet.BenchMarks._4._0._3; +using BenchmarkDotNet.Attributes; + +namespace BCryptNet.BenchMarks; + +#pragma warning disable 1591 +[MemoryDiagnoser] +[RPlotExporter, RankColumn] +[KeepBenchmarkFiles] +public class TestBcrypt_Hashing_Validation +{ + public IEnumerable Data() + { + yield return ["~!@#$%^&*() ~!@#$%^&*()PNBFRD", "$2a$10$LgfYWkbzEvQ4JakH7rOvHe", "$2a$10$LgfYWkbzEvQ4JakH7rOvHe0y8pHKF9OaFgwUZ2q7W2FFZmZzJYlfS"]; + yield return ["~!@#$%^&*() ~!@#$%^&*()PNBFRD", "$2a$12$WApznUOJfkEGSmYRfnkrPO", "$2a$12$WApznUOJfkEGSmYRfnkrPOr466oFDCaj4b6HY3EXGvfxm43seyhgC"]; + } + + [Benchmark(Baseline = true)] + [ArgumentsSource(nameof(Data))] + public bool TestHashValidate(string key, string salt, string hash) + { + return BCryptBaseLine.BCrypt.Verify(key, hash); + } + + [Benchmark] + [ArgumentsSource(nameof(Data))] + public bool TestHashValidatePerf1(string key, string salt, string hash) + { + return BCrypt305PerfMerge1.BCrypt.Verify(key, hash); + } + + [Benchmark] + [ArgumentsSource(nameof(Data))] + public bool TestHashValidateV4(string key, string salt, string hash) + { + return BCryptV4.BCrypt.Verify(key, hash); + } + + [Benchmark] + [ArgumentsSource(nameof(Data))] + public bool TestHashValidateV403(string key, string salt, string hash) + { + return BCryptV403.BCrypt.Verify(key, hash); + } + + [Benchmark] + [ArgumentsSource(nameof(Data))] + public bool TestHashValidateCurrent(string key, string salt, string hash) + { + return BCrypt.Verify(key, hash); + } +} diff --git a/benchmark/TestVariantsOnStringBuilding.cs b/benchmark/TestVariantsOnStringBuilding.cs index 665012d..65310bd 100644 --- a/benchmark/TestVariantsOnStringBuilding.cs +++ b/benchmark/TestVariantsOnStringBuilding.cs @@ -19,8 +19,8 @@ public class TestVariantsOnStringBuilding private static readonly string hash = "TV4S6ytwfsfvkgY8jIucDrjc8deX1s."; private static readonly string salt = "DCq7YPn5Rq63x1Lad4cll."; - private static readonly byte[] SaltBytes = BaseLine.BCrypt.DecodeBase64(salt, 16); - private static readonly byte[] HashBytes = BaseLine.BCrypt.DecodeBase64(hash, 23); + private static readonly byte[] SaltBytes = BCryptBaseLine.BCrypt.DecodeBase64(salt, 16); + private static readonly byte[] HashBytes = BCryptBaseLine.BCrypt.DecodeBase64(hash, 23); private static readonly char[] EncodedSaltAsChars = EncodeB64Methods.EncodeBase64AsBytes(SaltBytes, 16); private static readonly char[] EncodedHashAsChars = EncodeB64Methods.EncodeBase64AsBytes(HashBytes, 23); From 6d352ed798adad34c7823f885b9b9673ce69023e Mon Sep 17 00:00:00 2001 From: Chris McKee Date: Wed, 2 Apr 2025 01:08:21 +0100 Subject: [PATCH 06/27] rejig --- src/BCrypt.Net/BCryptBase.cs | 101 +++++++++-------------------- src/BCrypt.Net/BCryptExtendedV3.cs | 2 + tests/UnitTests/BCryptTests.cs | 6 +- 3 files changed, 37 insertions(+), 72 deletions(-) diff --git a/src/BCrypt.Net/BCryptBase.cs b/src/BCrypt.Net/BCryptBase.cs index ef222ed..c78d9d5 100644 --- a/src/BCrypt.Net/BCryptBase.cs +++ b/src/BCrypt.Net/BCryptBase.cs @@ -355,10 +355,15 @@ internal static void CreatePasswordHash(ReadOnlySpan inputKey, ReadOnlySpa case HashType.None: bool appendNul = bcryptMinorRevision >= 'a'; Span utf8Buffer = stackalloc byte[Encoding.UTF8.GetMaxByteCount(inputKey.Length + (appendNul ? 1 : 0))]; - int bytesWritten = Encoding.UTF8.GetBytes(inputKey, utf8Buffer); + int bytesWritten = SafeUTF8.GetBytes(inputKey, utf8Buffer); if (appendNul) utf8Buffer[bytesWritten++] = 0; - inputBytes = utf8Buffer[..bytesWritten].ToArray(); - break; + + if (!HashBytes(utf8Buffer[..bytesWritten], salt.Slice(startingOffset + 3, 22), bcryptMinorRevision, workFactor, outputBuffer, out int hashBytesWriten)) + throw new BcryptAuthenticationException("Couldn't hash input"); + + outputBufferWritten = hashBytesWriten; + return; + default: if (enhancedHashKeyGen == null) { @@ -366,13 +371,14 @@ internal static void CreatePasswordHash(ReadOnlySpan inputKey, ReadOnlySpa } inputBytes = enhancedHashKeyGen(new string(inputKey), hashType, bcryptMinorRevision); - break; - } - if (!HashBytes(inputBytes, salt.Slice(startingOffset + 3, 22), bcryptMinorRevision, workFactor, outputBuffer, out int written)) - throw new BcryptAuthenticationException("Couldn't hash input"); + if (!HashBytes(inputBytes, salt.Slice(startingOffset + 3, 22), bcryptMinorRevision, workFactor, outputBuffer, out int written)) + throw new BcryptAuthenticationException("Couldn't hash input"); + + outputBufferWritten = written; - outputBufferWritten = written; + return; + } } /// @@ -413,27 +419,27 @@ internal static bool HashBytes( destination[pos++] = '$'; // Write work factor as 2-digit number - if (!workFactor.TryFormat(destination.Slice(pos), out int wfChars, "D2")) + if (!workFactor.TryFormat(destination.Slice(pos), out int wfChars, "D2", CultureInfo.InvariantCulture)) return false; pos += wfChars; destination[pos++] = '$'; // Write base64-encoded salt - if (!TryEncodeBase64(saltBytes, saltBytes.Length, destination.Slice(pos), out int saltChars)) - return false; - pos += saltChars; + var s = EncodeBase64(saltBytes, saltBytes.Length); + s.TryCopyTo(destination.Slice(pos)); + pos += s.Length; // Write base64-encoded hash - if (!TryEncodeBase64(hashBytes, (BfCryptCiphertextLength * 4) - 1, destination.Slice(pos), out int hashChars)) - return false; - pos += hashChars; + var hashEncoded = EncodeBase64(hashBytes, (BfCryptCiphertextLength * 4) - 1); + hashEncoded.TryCopyTo(destination.Slice(pos)); + pos += hashEncoded.Length; charsWritten = pos; return true; } - internal static string GenerateSalt(int workFactor = DefaultRounds, char bcryptMinorRevision = DefaultHashVersion) + internal static ReadOnlySpan GenerateSalt(int workFactor = DefaultRounds, char bcryptMinorRevision = DefaultHashVersion) { if (workFactor < MinRounds || workFactor > MaxRounds) { @@ -447,63 +453,20 @@ internal static string GenerateSalt(int workFactor = DefaultRounds, char bcryptM throw new ArgumentException("BCrypt Revision should be a, b, x or y", nameof(bcryptMinorRevision)); } - byte[] saltBytes = new byte[BCryptSaltLen]; + Span saltBytes = stackalloc byte[BCryptSaltLen]; RngCsp.GetBytes(saltBytes); - var result = new StringBuilder(29); - result.Append('$').Append('2').Append(bcryptMinorRevision).Append('$').Append(workFactor.ToString("D2", CultureInfo.InvariantCulture)).Append('$'); - - // Base65 encoded salt - result.Append(EncodeBase64(saltBytes, saltBytes.Length)); - - return result.ToString(); - } + Span result = stackalloc char[29]; // Adjust the length as needed + result[0] = '$'; + result[1] = '2'; + result[2] = bcryptMinorRevision; + result[3] = '$'; + workFactor.TryFormat(result.Slice(4, 2), out int _, "D2", CultureInfo.InvariantCulture); + result[6] = '$'; + EncodeBase64(saltBytes, saltBytes.Length).CopyTo(result.Slice(7)); - internal static bool TryEncodeBase64(ReadOnlySpan byteArray, int length, Span destination, out int charsWritten) - { - charsWritten = 0; - - if (length <= 0 || length > byteArray.Length) - return false; - - int encodedSize = (int)Math.Ceiling((length * 4D) / 3); - if (destination.Length < encodedSize) - return false; - - int pos = 0; - int off = 0; - while (off < length) - { - int c1 = byteArray[off++] & 0xff; - destination[pos++] = Base64Code[(c1 >> 2) & 0x3f]; - c1 = (c1 & 0x03) << 4; - - if (off >= length) - { - destination[pos++] = Base64Code[c1 & 0x3f]; - break; - } - - int c2 = byteArray[off++] & 0xff; - c1 |= (c2 >> 4) & 0x0f; - destination[pos++] = Base64Code[c1 & 0x3f]; - c1 = (c2 & 0x0f) << 2; - - if (off >= length) - { - destination[pos++] = Base64Code[c1 & 0x3f]; - break; - } - - c2 = byteArray[off++] & 0xff; - c1 |= (c2 >> 6) & 0x03; - destination[pos++] = Base64Code[c1 & 0x3f]; - destination[pos++] = Base64Code[c2 & 0x3f]; - } - - charsWritten = pos; - return true; + return result.ToArray(); } internal static ReadOnlySpan EncodeBase64(ReadOnlySpan byteArray, int length) diff --git a/src/BCrypt.Net/BCryptExtendedV3.cs b/src/BCrypt.Net/BCryptExtendedV3.cs index 15eb97d..79e3658 100644 --- a/src/BCrypt.Net/BCryptExtendedV3.cs +++ b/src/BCrypt.Net/BCryptExtendedV3.cs @@ -47,6 +47,8 @@ public static string HashPassword(string hmacKey, string inputKey, int workFacto /// The hashed password. /// Thrown when the salt could not be parsed. public static string HashPassword(string hmacKey, string inputKey, string salt, HashType hashType = DefaultEnhancedHashType) => + CreatePasswordHash(inputKey, salt.AsSpan(), hashType, (key, type, version) => EnhancedHash(hmacKey,key, type, version)); + public static string HashPassword(string hmacKey, string inputKey, ReadOnlySpan salt, HashType hashType = DefaultEnhancedHashType) => CreatePasswordHash(inputKey, salt, hashType, (key, type, version) => EnhancedHash(hmacKey,key, type, version)); /// diff --git a/tests/UnitTests/BCryptTests.cs b/tests/UnitTests/BCryptTests.cs index 645c25d..a58356c 100644 --- a/tests/UnitTests/BCryptTests.cs +++ b/tests/UnitTests/BCryptTests.cs @@ -527,7 +527,7 @@ public void TestGenerateSaltWithWorkFactor() for (int j = 0; j < _testVectors.Length / 3; j++) { string plain = _testVectors[j, 0]; - string salt = BCrypt.GenerateSalt(i); + var salt = BCrypt.GenerateSalt(i); string hashed1 = BCrypt.HashPassword(plain, salt); string hashed2 = BCrypt.HashPassword(plain, hashed1); Assert.Equal(hashed1, hashed2); @@ -544,7 +544,7 @@ public void TestGenerateSaltWithMaxWorkFactor() for (int j = 0; j < _testVectors.Length / 3; j++) { string plain = _testVectors[j, 0]; - string salt = BCrypt.GenerateSalt(31); + var salt = BCrypt.GenerateSalt(31); string hashed1 = BCrypt.HashPassword(plain, salt); string hashed2 = BCrypt.HashPassword(plain, hashed1); Assert.Equal(hashed1, hashed2); @@ -563,7 +563,7 @@ public void TestGenerateSalt() for (int i = 0; i < _testVectors.Length / 3; i++) { string plain = _testVectors[i, 0]; - string salt = BCrypt.GenerateSalt(); + var salt = BCrypt.GenerateSalt(); string hashed1 = BCrypt.HashPassword(plain, salt); string hashed2 = BCrypt.HashPassword(plain, hashed1); Assert.Equal(hashed1, hashed2); From a464094742ee91f61449cdd84c0204d24e28f9ca Mon Sep 17 00:00:00 2001 From: Chris McKee Date: Wed, 2 Apr 2025 12:19:11 +0100 Subject: [PATCH 07/27] Benchmarks --- benchmark/Program.cs | 6 ++-- benchmark/TestB64Decoder.cs | 14 ++++++---- benchmark/TestB64Encoder.cs | 16 +++++++---- .../TestBcryptHashingEnhancedValidation.cs | 7 ++++- benchmark/TestBcrypt_Hash_Interrogation.cs | 7 ++++- benchmark/TestBcrypt_Hashing.cs | 7 ++++- benchmark/TestBcrypt_Hashing_Enhanced.cs | 7 ++++- benchmark/TestBcrypt_Hashing_Validation.cs | 7 ++++- benchmark/TestVariantsOnStringBuilding.cs | 9 ++++-- src/BCrypt.Net/BCrypt.cs | 4 ++- src/BCrypt.Net/BCryptBase.cs | 27 +++++++++--------- src/BCrypt.Net/BCryptExtendedV2.cs | 5 ++-- src/BCrypt.Net/BCryptExtendedV3.cs | 28 +++++++++++++++---- tests/UnitTests/BCrypt.Net.UnitTests.csproj | 5 +--- 14 files changed, 100 insertions(+), 49 deletions(-) diff --git a/benchmark/Program.cs b/benchmark/Program.cs index ee80bb0..d52f48e 100644 --- a/benchmark/Program.cs +++ b/benchmark/Program.cs @@ -14,7 +14,7 @@ class Program static void Main(string[] args) { #if DEBUG - BenchmarkRunner.Run(new DebugInProcessConfig().AddValidator(ExecutionValidator.FailOnError)); + BenchmarkRunner.Run(new DebugInProcessConfig().AddValidator(ExecutionValidator.FailOnError)); #else var config = DefaultConfig.Instance .With(Job.Default.With(CoreRuntime.Core90)) @@ -24,8 +24,8 @@ static void Main(string[] args) BenchmarkRunner.Run(config); BenchmarkRunner.Run(config); BenchmarkRunner.Run(config); - - // Tests for testing in isolation + // + // // Tests for testing in isolation BenchmarkRunner.Run(config); BenchmarkRunner.Run(config); BenchmarkRunner.Run(config); diff --git a/benchmark/TestB64Decoder.cs b/benchmark/TestB64Decoder.cs index fbb01cf..a00d21c 100644 --- a/benchmark/TestB64Decoder.cs +++ b/benchmark/TestB64Decoder.cs @@ -1,17 +1,22 @@ using System; using BCryptNet.BenchMarks.DecodeB64; using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Order; #pragma warning disable 1591 namespace BCryptNet.BenchMarks { [MemoryDiagnoser] - [RPlotExporter, RankColumn] + /*[RPlotExporter]*/ + [RankColumn] + //[GcServer(true)] + [Orderer(SummaryOrderPolicy.Declared)] [KeepBenchmarkFiles] + [MarkdownExporterAttribute.GitHub] + // [ReturnValueValidator(failOnError: true)] public class TestB64Decoder { - [Benchmark(Baseline = true)] [Arguments("DCq7YPn5Rq63x1Lad4cll.")] [Arguments("HqWuK6/Ng6sg9gQzbLrgb.")] @@ -35,13 +40,13 @@ public byte[] DecodeBase64StringCreateSpan(string salt) { return DecodeB64Methods.DecodeBase64StringCreateSpan(salt, 16); } - + [Benchmark] [Arguments("DCq7YPn5Rq63x1Lad4cll.")] [Arguments("HqWuK6/Ng6sg9gQzbLrgb.")] public byte[] DecodeBase64ToBytes(string salt) { - return DecodeB64Methods.DecodeBase64ToBytes(salt, 16); + return DecodeB64Methods.DecodeBase64ToBytes(salt, 16); } [Benchmark] @@ -53,6 +58,5 @@ public byte[] DecodeBase64ToSpan(string salt) int written = DecodeB64Methods.DecodeBase64(salt, saltBuffer); return saltBuffer[..written].ToArray(); } - } } diff --git a/benchmark/TestB64Encoder.cs b/benchmark/TestB64Encoder.cs index 6c14414..789cacc 100644 --- a/benchmark/TestB64Encoder.cs +++ b/benchmark/TestB64Encoder.cs @@ -1,14 +1,18 @@ using BCryptNet.BenchMarks._3._2._1; using BCryptNet.BenchMarks.EncodeB64; using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Order; #pragma warning disable 1591 namespace BCryptNet.BenchMarks { [MemoryDiagnoser] - [RPlotExporter, RankColumn] + /*[RPlotExporter]*/[RankColumn] + //[GcServer(true)] + [Orderer(SummaryOrderPolicy.Declared)] [KeepBenchmarkFiles] + [MarkdownExporterAttribute.GitHub] public class TestB64Encoder { private static readonly byte[] SaltBytes = BCryptBaseLine.BCrypt.DecodeBase64("sGBxdT2q8Qd84NyZEkwTY.", 16); @@ -16,31 +20,31 @@ public class TestB64Encoder [Benchmark(Baseline = true)] public void EncodeBase64Unsized() { - var decoded = EncodeB64Methods.EncodeBase64Unsized(SaltBytes, 16); + EncodeB64Methods.EncodeBase64Unsized(SaltBytes, 16); } [Benchmark] public void EncodeBase64Sized() { - var decoded = EncodeB64Methods.EncodeBase64Sized(SaltBytes, 16); + EncodeB64Methods.EncodeBase64Sized(SaltBytes, 16); } [Benchmark] public void EncodeBase64AsBytes() { - var decoded = EncodeB64Methods.EncodeBase64AsBytes(SaltBytes, 16); + EncodeB64Methods.EncodeBase64AsBytes(SaltBytes, 16); } [Benchmark] public void EncodeBase64StackAlloc() { - var decoded = EncodeB64Methods.EncodeBase64StackAlloc(SaltBytes, 16); + EncodeB64Methods.EncodeBase64StackAlloc(SaltBytes, 16); } [Benchmark] public void EncodeBase64HeapSpanAlloc() { - var decoded = EncodeB64Methods.EncodeBase64HeapAlloc(SaltBytes, 16); + EncodeB64Methods.EncodeBase64HeapAlloc(SaltBytes, 16); } } } diff --git a/benchmark/TestBcryptHashingEnhancedValidation.cs b/benchmark/TestBcryptHashingEnhancedValidation.cs index c33ced4..4e4cd6d 100644 --- a/benchmark/TestBcryptHashingEnhancedValidation.cs +++ b/benchmark/TestBcryptHashingEnhancedValidation.cs @@ -23,13 +23,18 @@ using BCryptNet.BenchMarks._3._5.perfmerge_1; using BCryptNet.BenchMarks._4._0._0; using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Order; namespace BCryptNet.BenchMarks; #pragma warning disable 1591 [MemoryDiagnoser] -[RPlotExporter, RankColumn] +/*[RPlotExporter]*/[RankColumn] +//[GcServer(true)] +[Orderer(SummaryOrderPolicy.Declared)] [KeepBenchmarkFiles] +[MarkdownExporterAttribute.GitHub] +// [ReturnValueValidator(failOnError: true)] public class TestBcryptHashingEnhancedValidation { public IEnumerable Data() diff --git a/benchmark/TestBcrypt_Hash_Interrogation.cs b/benchmark/TestBcrypt_Hash_Interrogation.cs index 344f61a..88365c1 100644 --- a/benchmark/TestBcrypt_Hash_Interrogation.cs +++ b/benchmark/TestBcrypt_Hash_Interrogation.cs @@ -3,14 +3,19 @@ using BCryptNet.BenchMarks._4._0._3; using BCryptNet.BenchMarks.HashParser; using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Order; #pragma warning disable 1591 namespace BCryptNet.BenchMarks { [MemoryDiagnoser] - [RPlotExporter, RankColumn] + /*[RPlotExporter]*/[RankColumn] + //[GcServer(true)] + [Orderer(SummaryOrderPolicy.Declared)] [KeepBenchmarkFiles] + [MarkdownExporterAttribute.GitHub] + // [ReturnValueValidator(failOnError: true)] public class TestBcrypt_Hash_Interrogation { [Benchmark(Baseline = true)] diff --git a/benchmark/TestBcrypt_Hashing.cs b/benchmark/TestBcrypt_Hashing.cs index 24f4821..bb9f63d 100644 --- a/benchmark/TestBcrypt_Hashing.cs +++ b/benchmark/TestBcrypt_Hashing.cs @@ -5,14 +5,19 @@ using BCryptNet.BenchMarks._4._0._0; using BCryptNet.BenchMarks._4._0._3; using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Order; #pragma warning disable 1591 namespace BCryptNet.BenchMarks { [MemoryDiagnoser] - [RPlotExporter, RankColumn] + /*[RPlotExporter]*/[RankColumn] + //[GcServer(true)] + [Orderer(SummaryOrderPolicy.Declared)] [KeepBenchmarkFiles] + [MarkdownExporterAttribute.GitHub] + // [ReturnValueValidator(failOnError: true)] public class TestBcrypt_Hashing { public IEnumerable Data() diff --git a/benchmark/TestBcrypt_Hashing_Enhanced.cs b/benchmark/TestBcrypt_Hashing_Enhanced.cs index b5d4679..dad7b34 100644 --- a/benchmark/TestBcrypt_Hashing_Enhanced.cs +++ b/benchmark/TestBcrypt_Hashing_Enhanced.cs @@ -4,14 +4,19 @@ using BCryptNet.BenchMarks._3._5.perfmerge_1; using BCryptNet.BenchMarks._4._0._0; using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Order; #pragma warning disable 1591 namespace BCryptNet.BenchMarks { [MemoryDiagnoser] - [RPlotExporter, RankColumn] + /*[RPlotExporter]*/[RankColumn] + //[GcServer(true)] + [Orderer(SummaryOrderPolicy.Declared)] [KeepBenchmarkFiles] + [MarkdownExporterAttribute.GitHub] + // [ReturnValueValidator(failOnError: true)] public class TestBcryptHashingEnhanced { public IEnumerable Data() diff --git a/benchmark/TestBcrypt_Hashing_Validation.cs b/benchmark/TestBcrypt_Hashing_Validation.cs index 5c81ae6..66f12d8 100644 --- a/benchmark/TestBcrypt_Hashing_Validation.cs +++ b/benchmark/TestBcrypt_Hashing_Validation.cs @@ -23,13 +23,18 @@ using BCryptNet.BenchMarks._4._0._0; using BCryptNet.BenchMarks._4._0._3; using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Order; namespace BCryptNet.BenchMarks; #pragma warning disable 1591 [MemoryDiagnoser] -[RPlotExporter, RankColumn] +/*[RPlotExporter]*/[RankColumn] +//[GcServer(true)] +[Orderer(SummaryOrderPolicy.Declared)] [KeepBenchmarkFiles] +[MarkdownExporterAttribute.GitHub] +// [ReturnValueValidator(failOnError: true)] public class TestBcrypt_Hashing_Validation { public IEnumerable Data() diff --git a/benchmark/TestVariantsOnStringBuilding.cs b/benchmark/TestVariantsOnStringBuilding.cs index 65310bd..1eb998c 100644 --- a/benchmark/TestVariantsOnStringBuilding.cs +++ b/benchmark/TestVariantsOnStringBuilding.cs @@ -3,16 +3,19 @@ using BCryptNet.BenchMarks._3._2._1; using BCryptNet.BenchMarks.EncodeB64; using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Order; #pragma warning disable 1591 namespace BCryptNet.BenchMarks { [MemoryDiagnoser] - [CategoriesColumn] - [RPlotExporter, RankColumn] - [ReturnValueValidator(failOnError: true)] + /*[RPlotExporter]*/[RankColumn] + //[GcServer(true)] + [Orderer(SummaryOrderPolicy.Declared)] [KeepBenchmarkFiles] + [MarkdownExporterAttribute.GitHub] + [ReturnValueValidator(failOnError: true)] public class TestVariantsOnStringBuilding { private readonly string bcryptMinorRevision = "a"; diff --git a/src/BCrypt.Net/BCrypt.cs b/src/BCrypt.Net/BCrypt.cs index 7f7ce63..5609075 100644 --- a/src/BCrypt.Net/BCrypt.cs +++ b/src/BCrypt.Net/BCrypt.cs @@ -17,6 +17,8 @@ The above copyright notice and this permission notice shall be included in all c IN THE SOFTWARE. */ +using System.Globalization; + namespace BCryptNet { /// BCrypt implementation. @@ -122,7 +124,7 @@ public static string ValidateAndUpgradeHash(string currentKey, string currentHas } // Extract details from salt - int currentWorkFactor = Convert.ToInt16(currentHash.Substring(startingOffset, 2)); + int currentWorkFactor = Convert.ToInt16(currentHash.Substring(startingOffset, 2), CultureInfo.InvariantCulture); // Never downgrade work-factor (unless forced) if (!forceWorkFactor && currentWorkFactor > workFactor) diff --git a/src/BCrypt.Net/BCryptBase.cs b/src/BCrypt.Net/BCryptBase.cs index c78d9d5..08aaa55 100644 --- a/src/BCrypt.Net/BCryptBase.cs +++ b/src/BCrypt.Net/BCryptBase.cs @@ -349,7 +349,6 @@ internal static void CreatePasswordHash(ReadOnlySpan inputKey, ReadOnlySpa throw new SaltParseException("Salt rounds out of range"); } - Span inputBytes; switch (hashType) { case HashType.None: @@ -357,8 +356,8 @@ internal static void CreatePasswordHash(ReadOnlySpan inputKey, ReadOnlySpa Span utf8Buffer = stackalloc byte[Encoding.UTF8.GetMaxByteCount(inputKey.Length + (appendNul ? 1 : 0))]; int bytesWritten = SafeUTF8.GetBytes(inputKey, utf8Buffer); if (appendNul) utf8Buffer[bytesWritten++] = 0; - - if (!HashBytes(utf8Buffer[..bytesWritten], salt.Slice(startingOffset + 3, 22), bcryptMinorRevision, workFactor, outputBuffer, out int hashBytesWriten)) + Span inputBytes = utf8Buffer[..bytesWritten]; + if (!HashBytes(inputBytes, salt.Slice(startingOffset + 3, 22), bcryptMinorRevision, workFactor, outputBuffer, out int hashBytesWriten)) throw new BcryptAuthenticationException("Couldn't hash input"); outputBufferWritten = hashBytesWriten; @@ -370,9 +369,8 @@ internal static void CreatePasswordHash(ReadOnlySpan inputKey, ReadOnlySpa throw new ArgumentException("Invalid HashType, You can't have an enhanced hash without an implementation of the key generator.", nameof(hashType)); } - inputBytes = enhancedHashKeyGen(new string(inputKey), hashType, bcryptMinorRevision); - - if (!HashBytes(inputBytes, salt.Slice(startingOffset + 3, 22), bcryptMinorRevision, workFactor, outputBuffer, out int written)) + Span eInputBytes = enhancedHashKeyGen(new string(inputKey), hashType, bcryptMinorRevision); + if (!HashBytes(eInputBytes, salt.Slice(startingOffset + 3, 22), bcryptMinorRevision, workFactor, outputBuffer, out int written)) throw new BcryptAuthenticationException("Couldn't hash input"); outputBufferWritten = written; @@ -388,6 +386,8 @@ internal static void CreatePasswordHash(ReadOnlySpan inputKey, ReadOnlySpa /// /// /// + /// + /// /// internal static bool HashBytes( ReadOnlySpan inputBytes, @@ -419,7 +419,7 @@ internal static bool HashBytes( destination[pos++] = '$'; // Write work factor as 2-digit number - if (!workFactor.TryFormat(destination.Slice(pos), out int wfChars, "D2", CultureInfo.InvariantCulture)) + if (!workFactor.TryFormat(destination[pos..], out int wfChars, "D2", CultureInfo.InvariantCulture)) return false; pos += wfChars; @@ -427,12 +427,12 @@ internal static bool HashBytes( // Write base64-encoded salt var s = EncodeBase64(saltBytes, saltBytes.Length); - s.TryCopyTo(destination.Slice(pos)); + s.TryCopyTo(destination[pos..]); pos += s.Length; // Write base64-encoded hash var hashEncoded = EncodeBase64(hashBytes, (BfCryptCiphertextLength * 4) - 1); - hashEncoded.TryCopyTo(destination.Slice(pos)); + hashEncoded.TryCopyTo(destination[pos..]); pos += hashEncoded.Length; charsWritten = pos; @@ -462,9 +462,9 @@ internal static ReadOnlySpan GenerateSalt(int workFactor = DefaultRounds, result[1] = '2'; result[2] = bcryptMinorRevision; result[3] = '$'; - workFactor.TryFormat(result.Slice(4, 2), out int _, "D2", CultureInfo.InvariantCulture); + workFactor.TryFormat(result.Slice(4, 2), out _, "D2", CultureInfo.InvariantCulture); result[6] = '$'; - EncodeBase64(saltBytes, saltBytes.Length).CopyTo(result.Slice(7)); + EncodeBase64(saltBytes, saltBytes.Length).CopyTo(result[7..]); return result.ToArray(); } @@ -555,9 +555,6 @@ public static int DecodeBase64(ReadOnlySpan encodedSpan, Span destin internal ReadOnlySpan CryptRaw(ReadOnlySpan inputBytes, ReadOnlySpan saltBytes, int workFactor, Span destination) { - int i; - int j; - Span cdata = stackalloc uint[BfCryptCiphertext.Length]; BfCryptCiphertext.CopyTo(cdata); int clen = cdata.Length; @@ -583,6 +580,8 @@ internal ReadOnlySpan CryptRaw(ReadOnlySpan inputBytes, ReadOnlySpan InitializeKey(); EKSKey(saltBytes, inputBytes); + int i,j; + for (i = 0; i != rounds; i++) { Key(inputBytes); diff --git a/src/BCrypt.Net/BCryptExtendedV2.cs b/src/BCrypt.Net/BCryptExtendedV2.cs index f4e4f44..3936c17 100644 --- a/src/BCrypt.Net/BCryptExtendedV2.cs +++ b/src/BCrypt.Net/BCryptExtendedV2.cs @@ -1,4 +1,5 @@ -using System.Security.Cryptography; +using System.Globalization; +using System.Security.Cryptography; namespace BCryptNet; @@ -171,7 +172,7 @@ public static string ValidateAndUpgradeHash(string currentKey, } // Extract details from salt - int currentWorkFactor = Convert.ToInt16(currentHash.Substring(startingOffset, 2)); + int currentWorkFactor = Convert.ToInt16(currentHash.Substring(startingOffset, 2), CultureInfo.InvariantCulture); // Never downgrade work-factor (unless forced) if (!forceWorkFactor && currentWorkFactor > workFactor) diff --git a/src/BCrypt.Net/BCryptExtendedV3.cs b/src/BCrypt.Net/BCryptExtendedV3.cs index 79e3658..1cfb7e9 100644 --- a/src/BCrypt.Net/BCryptExtendedV3.cs +++ b/src/BCrypt.Net/BCryptExtendedV3.cs @@ -1,4 +1,5 @@ #if NETCOREAPP +using System.Globalization; using System.Security.Cryptography; namespace BCryptNet; @@ -10,9 +11,10 @@ namespace BCryptNet; /// /// To hash a password using SHA384 pre-hashing for increased entropy see /// -/// string pw_hash = BCryptExtendedV3.HashPassword(hmac_key, plain_password); -/// (To validate an enhanced hash you can pass true as the last parameter of Verify or use ) -/// +/// +/// string pw_hash = BCryptExtendedV3.HashPassword(hmac_key, plain_password); +/// // (To validate an enhanced hash you can pass true as the last parameter of Verify or use ) +/// [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Overloads are for different purposes")] public sealed class BCryptExtendedV3 : BCryptCore { @@ -47,8 +49,22 @@ public static string HashPassword(string hmacKey, string inputKey, int workFacto /// The hashed password. /// Thrown when the salt could not be parsed. public static string HashPassword(string hmacKey, string inputKey, string salt, HashType hashType = DefaultEnhancedHashType) => - CreatePasswordHash(inputKey, salt.AsSpan(), hashType, (key, type, version) => EnhancedHash(hmacKey,key, type, version)); - public static string HashPassword(string hmacKey, string inputKey, ReadOnlySpan salt, HashType hashType = DefaultEnhancedHashType) => + CreatePasswordHash(inputKey, salt, hashType, (key, type, version) => EnhancedHash(hmacKey,key, type, version)); + + /// + /// Pre-hash a password with HMAC-SHA3-384 then using the OpenBSD BCrypt scheme and a salt generated by . + /// + /// + /// + /// You should generally leave generating salts to the library. + /// + /// Key used in HMAC hashing + /// The password to hash. + /// + /// HashType used (default SHA384) + /// The hashed password. + /// Thrown when the salt could not be parsed. + public static string HashPassword(string hmacKey, ReadOnlySpan inputKey, ReadOnlySpan salt, HashType hashType = DefaultEnhancedHashType) => CreatePasswordHash(inputKey, salt, hashType, (key, type, version) => EnhancedHash(hmacKey,key, type, version)); /// @@ -186,7 +202,7 @@ public static string ValidateAndUpgradeHash( } // Extract details from salt - int currentWorkFactor = Convert.ToInt16(currentHash.Substring(startingOffset, 2)); + int currentWorkFactor = Convert.ToInt16(currentHash.Substring(startingOffset, 2), CultureInfo.InvariantCulture); // Never downgrade work-factor (unless forced) if (!forceWorkFactor && currentWorkFactor > workFactor) diff --git a/tests/UnitTests/BCrypt.Net.UnitTests.csproj b/tests/UnitTests/BCrypt.Net.UnitTests.csproj index 44b7522..efa64c4 100644 --- a/tests/UnitTests/BCrypt.Net.UnitTests.csproj +++ b/tests/UnitTests/BCrypt.Net.UnitTests.csproj @@ -12,10 +12,7 @@ Full $(DefineConstants);WINDOWS - - - - 1701;1702;CS1591 + AnyCPU From b1059a235fa0d43b3a267704c9bbdcabecf96ef8 Mon Sep 17 00:00:00 2001 From: Chris McKee Date: Wed, 2 Apr 2025 12:48:13 +0100 Subject: [PATCH 08/27] Switch func to a bog standard delegate (looks neater in compiler statements) Clone BCryptExtendedV2 to BCryptExtendedV2.Std as we can't use the newer span wiring --- src/BCrypt.Net/BCryptBase.cs | 12 +- src/BCrypt.Net/BCryptExtendedV2.Std.cs | 190 +++++++++++++++++++++++++ src/BCrypt.Net/BCryptExtendedV2.cs | 14 +- src/BCrypt.Net/BCryptExtendedV3.cs | 23 ++- 4 files changed, 219 insertions(+), 20 deletions(-) create mode 100644 src/BCrypt.Net/BCryptExtendedV2.Std.cs diff --git a/src/BCrypt.Net/BCryptBase.cs b/src/BCrypt.Net/BCryptBase.cs index 08aaa55..a59b050 100644 --- a/src/BCrypt.Net/BCryptBase.cs +++ b/src/BCrypt.Net/BCryptBase.cs @@ -268,13 +268,19 @@ private void InitializeKey() /// /// /// - internal static string CreatePasswordHash(ReadOnlySpan inputKey, ReadOnlySpan salt, HashType hashType = HashType.None, Func enhancedHashKeyGen = null) + internal static string CreatePasswordHash(ReadOnlySpan inputKey, ReadOnlySpan salt, HashType hashType = HashType.None, EnhancedHashDelegate enhancedHashKeyGen = null) { Span outputBuffer = stackalloc char[60]; CreatePasswordHash(inputKey, salt, outputBuffer, out var outputBufferWritten, hashType, enhancedHashKeyGen); return new string(outputBuffer[..outputBufferWritten]); } + #if NETCOREAPP + internal delegate Span EnhancedHashDelegate(ReadOnlySpan inputKey, HashType hashType, char bcryptMinorRevision); + #else + internal delegate byte[] EnhancedHashDelegate(string inputKey, HashType hashType, char bcryptMinorRevision); + #endif + /// /// Create Password Hash Base /// @@ -289,7 +295,7 @@ internal static string CreatePasswordHash(ReadOnlySpan inputKey, ReadOnlyS /// /// internal static void CreatePasswordHash(ReadOnlySpan inputKey, ReadOnlySpan salt, Span outputBuffer, out int outputBufferWritten, HashType hashType = HashType.None, - Func enhancedHashKeyGen = null) + EnhancedHashDelegate enhancedHashKeyGen = null) { if (salt.IsEmpty) { @@ -369,7 +375,7 @@ internal static void CreatePasswordHash(ReadOnlySpan inputKey, ReadOnlySpa throw new ArgumentException("Invalid HashType, You can't have an enhanced hash without an implementation of the key generator.", nameof(hashType)); } - Span eInputBytes = enhancedHashKeyGen(new string(inputKey), hashType, bcryptMinorRevision); + Span eInputBytes = enhancedHashKeyGen(inputKey, hashType, bcryptMinorRevision); if (!HashBytes(eInputBytes, salt.Slice(startingOffset + 3, 22), bcryptMinorRevision, workFactor, outputBuffer, out int written)) throw new BcryptAuthenticationException("Couldn't hash input"); diff --git a/src/BCrypt.Net/BCryptExtendedV2.Std.cs b/src/BCrypt.Net/BCryptExtendedV2.Std.cs new file mode 100644 index 0000000..be504b7 --- /dev/null +++ b/src/BCrypt.Net/BCryptExtendedV2.Std.cs @@ -0,0 +1,190 @@ +#if !NETCOREAPP +using System.Globalization; +using System.Security.Cryptography; + +namespace BCryptNet; + +/// +/// BCrypt Enhanced (post v3.5) +/// Created to be compatible with other programming-language implementations of pre-hashed keys +/// i.e. passlib in python / php bcrypt and sha +/// +/// +/// To hash a password using SHA384 pre-hashing for increased entropy see +/// +/// string pw_hash = BCryptExtendedV2.HashPassword(plain_password); +/// (To validate an enhanced hash you can pass true as the last parameter of Verify or use ) +/// +[System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Overloads are for different purposes")] +public sealed class BCryptExtendedV2 : BCryptCore +{ + private const HashType DefaultEnhancedHashType = HashType.SHA384; + + /// + /// Pre-hash a password with SHA384 then using the OpenBSD BCrypt scheme and a salt generated by . + /// + /// The password to hash. + /// + /// HashType used (default SHA384) + /// The hashed password. + /// Thrown when the salt could not be parsed. + public static string HashPassword(string inputKey, int workFactor = DefaultRounds, + HashType hashType = DefaultEnhancedHashType) => + CreatePasswordHash(inputKey, GenerateSalt(workFactor), hashType, + (s, type, version) => EnhancedHash(s, type, version)); + + /// + /// Pre-hash a password with SHA384 then using the OpenBSD BCrypt scheme with a manually supplied salt/>. + /// + /// + /// You should generally leave generating salts to the library. + /// + /// The password to hash. + /// + /// HashType used (default SHA384) + /// The hashed password. + /// Thrown when the salt could not be parsed. + public static string HashPassword(string inputKey, string salt, HashType hashType = DefaultEnhancedHashType) => + CreatePasswordHash(inputKey, salt, hashType, (s, type, version) => EnhancedHash(s, type, version)); + + /// + /// Hashes key, base64 encodes before returning byte array + /// + /// + /// (Default: 'a') + /// HashType used (default SHA384) + /// + private static byte[] EnhancedHash(string inputString, HashType hashType, char bcryptMinorRevision = 'a') + { + switch (hashType) + { + case HashType.SHA256: + using (var sha = SHA256.Create()) + return SafeUTF8.GetBytes(Convert.ToBase64String(sha.ComputeHash(SafeUTF8.GetBytes(inputString))) + + (bcryptMinorRevision >= 'a' ? Nul : EmptyString)); + case HashType.SHA384: + using (var sha = SHA384.Create()) + return SafeUTF8.GetBytes(Convert.ToBase64String(sha.ComputeHash(SafeUTF8.GetBytes(inputString))) + + (bcryptMinorRevision >= 'a' ? Nul : EmptyString)); + case HashType.SHA512: + using (var sha = SHA512.Create()) + return SafeUTF8.GetBytes(Convert.ToBase64String(sha.ComputeHash(SafeUTF8.GetBytes(inputString))) + + (bcryptMinorRevision >= 'a' ? Nul : EmptyString)); + default: + throw new ArgumentOutOfRangeException(nameof(hashType), hashType, null); + } + } + + /// + /// Compares the users stored hash with their password + /// in a time-safe manner + /// + /// + /// + /// HashType used (default SHA384) + /// + public static bool Verify(string text, string hash, HashType hashType = DefaultEnhancedHashType) + { + return SecureEquals(SafeUTF8.GetBytes(hash), + SafeUTF8.GetBytes(CreatePasswordHash(text, hash, hashType, + (s, type, version) => EnhancedHash(s, type, version)))); + } + + /// + /// Validate and Upgrade Hash + /// + /// + /// + /// + /// + /// + /// + /// + public static string ValidateAndUpgradeHash(string currentKey, + string currentHash, + string newKey, + HashType hashType = DefaultEnhancedHashType, + int workFactor = DefaultRounds, + bool forceWorkFactor = false) + { + return ValidateAndUpgradeHash(currentKey, currentHash, DefaultEnhancedHashType, newKey, hashType, workFactor, + forceWorkFactor); + } + + /// + /// Validate and Upgrade Hash + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static string ValidateAndUpgradeHash(string currentKey, + string currentHash, + HashType currentKeyHashType, + string newKey, + HashType hashType = DefaultEnhancedHashType, + int workFactor = DefaultRounds, + bool forceWorkFactor = false) + { + if (currentKey == null) + { + throw new ArgumentNullException(nameof(currentKey)); + } + + if (string.IsNullOrEmpty(currentHash) || currentHash.Length != 60) + throw new ArgumentException("Invalid Hash", nameof(currentHash)); + + // Throw if validation fails (password isn't valid for hash) + if (!Verify(currentKey, currentHash, currentKeyHashType)) + throw new BcryptAuthenticationException("Current credentials could not be authenticated"); + + // Throw if invalid BCrypt Version + if (currentHash[0] != '$' || currentHash[1] != '2') + throw new SaltParseException("Invalid bcrypt version"); + + // Throw if log rounds are out of range on hash, deals with custom salts + if (workFactor < 1 || workFactor > 31) + throw new SaltParseException("Work factor out of range"); + + // Determine the starting offset and validate the salt + int startingOffset = 3; + + if (currentHash[2] != '$') + { + char minor = currentHash[2]; + if (minor != 'a' && minor != 'b' && minor != 'x' && minor != 'y' || currentHash[3] != '$') + { + throw new SaltParseException("Invalid bcrypt revision"); + } + + startingOffset = 4; + } + + // Extract number of rounds + if (currentHash[startingOffset + 2] > '$') + { + throw new SaltParseException("Missing work factor"); + } + + // Extract details from salt + int currentWorkFactor = Convert.ToInt16(currentHash.Substring(startingOffset, 2), CultureInfo.InvariantCulture); + + // Never downgrade work-factor (unless forced) + if (!forceWorkFactor && currentWorkFactor > workFactor) + { + workFactor = currentWorkFactor; + } + + return CreatePasswordHash(newKey, GenerateSalt(workFactor), hashType, + (s, type, version) => EnhancedHash(s, type, version)); + } +} +#endif diff --git a/src/BCrypt.Net/BCryptExtendedV2.cs b/src/BCrypt.Net/BCryptExtendedV2.cs index 3936c17..dbbd2d3 100644 --- a/src/BCrypt.Net/BCryptExtendedV2.cs +++ b/src/BCrypt.Net/BCryptExtendedV2.cs @@ -1,4 +1,5 @@ -using System.Globalization; +#if NETCOREAPP +using System.Globalization; using System.Security.Cryptography; namespace BCryptNet; @@ -53,21 +54,21 @@ public static string HashPassword(string inputKey, string salt, HashType hashTyp /// (Default: 'a') /// HashType used (default SHA384) /// - private static byte[] EnhancedHash(string inputString, HashType hashType, char bcryptMinorRevision = 'a') + private static Span EnhancedHash(ReadOnlySpan inputString, HashType hashType, char bcryptMinorRevision = 'a') { switch (hashType) { case HashType.SHA256: using (var sha = SHA256.Create()) - return SafeUTF8.GetBytes(Convert.ToBase64String(sha.ComputeHash(SafeUTF8.GetBytes(inputString))) + + return SafeUTF8.GetBytes(Convert.ToBase64String(sha.ComputeHash(SafeUTF8.GetBytes(inputString.ToString()))) + (bcryptMinorRevision >= 'a' ? Nul : EmptyString)); case HashType.SHA384: using (var sha = SHA384.Create()) - return SafeUTF8.GetBytes(Convert.ToBase64String(sha.ComputeHash(SafeUTF8.GetBytes(inputString))) + + return SafeUTF8.GetBytes(Convert.ToBase64String(sha.ComputeHash(SafeUTF8.GetBytes(inputString.ToString()))) + (bcryptMinorRevision >= 'a' ? Nul : EmptyString)); case HashType.SHA512: using (var sha = SHA512.Create()) - return SafeUTF8.GetBytes(Convert.ToBase64String(sha.ComputeHash(SafeUTF8.GetBytes(inputString))) + + return SafeUTF8.GetBytes(Convert.ToBase64String(sha.ComputeHash(SafeUTF8.GetBytes(inputString.ToString()))) + (bcryptMinorRevision >= 'a' ? Nul : EmptyString)); default: throw new ArgumentOutOfRangeException(nameof(hashType), hashType, null); @@ -134,7 +135,9 @@ public static string ValidateAndUpgradeHash(string currentKey, bool forceWorkFactor = false) { if (currentKey == null) + { throw new ArgumentNullException(nameof(currentKey)); + } if (string.IsNullOrEmpty(currentHash) || currentHash.Length != 60) throw new ArgumentException("Invalid Hash", nameof(currentHash)); @@ -184,3 +187,4 @@ public static string ValidateAndUpgradeHash(string currentKey, (s, type, version) => EnhancedHash(s, type, version)); } } +#endif diff --git a/src/BCrypt.Net/BCryptExtendedV3.cs b/src/BCrypt.Net/BCryptExtendedV3.cs index 1cfb7e9..6a06ae6 100644 --- a/src/BCrypt.Net/BCryptExtendedV3.cs +++ b/src/BCrypt.Net/BCryptExtendedV3.cs @@ -71,27 +71,26 @@ public static string HashPassword(string hmacKey, ReadOnlySpan inputKey, R /// Hashes key, base64 encodes before returning byte array /// /// Key used in HMAC hashing - /// + /// /// (Default: 'a') /// HashType used (default SHA3 HMAC 384 - https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.hmacsha3_256) /// - private static byte[] EnhancedHash(string hmacKey, string inputString, HashType hashType, char bcryptMinorRevision = 'a') + private static Span EnhancedHash(ReadOnlySpan hmacKey, ReadOnlySpan inputKey, HashType hashType, char bcryptMinorRevision = 'a') { switch (hashType) { case HashType.SHA256: - using (var sha = new HMACSHA3_256(SafeUTF8.GetBytes(hmacKey))) - return SafeUTF8.GetBytes(Convert.ToBase64String(sha.ComputeHash(SafeUTF8.GetBytes(inputString))) + - (bcryptMinorRevision >= 'a' ? Nul : EmptyString)); + using (var sha = new HMACSHA3_256(BCryptCore.SafeUTF8.GetBytes(hmacKey.ToString()))) + return BCryptCore.SafeUTF8.GetBytes(Convert.ToBase64String(sha.ComputeHash(BCryptCore.SafeUTF8.GetBytes(inputKey.ToString()))) + + (bcryptMinorRevision >= 'a' ? BCryptCore.Nul : BCryptCore.EmptyString)).AsSpan(); case HashType.SHA384: - using (var sha = new HMACSHA3_384(SafeUTF8.GetBytes(hmacKey))) - return SafeUTF8.GetBytes(Convert.ToBase64String(sha.ComputeHash(SafeUTF8.GetBytes(inputString))) + - (bcryptMinorRevision >= 'a' ? Nul : EmptyString)); + using (var sha = new HMACSHA3_384(BCryptCore.SafeUTF8.GetBytes(hmacKey.ToString()))) + return BCryptCore.SafeUTF8.GetBytes(Convert.ToBase64String(sha.ComputeHash(BCryptCore.SafeUTF8.GetBytes(inputKey.ToString()))) + + (bcryptMinorRevision >= 'a' ? BCryptCore.Nul : BCryptCore.EmptyString)); case HashType.SHA512: - using (var sha = new HMACSHA3_512(SafeUTF8.GetBytes(hmacKey))) - return SafeUTF8.GetBytes(Convert.ToBase64String(sha.ComputeHash(SafeUTF8.GetBytes(inputString))) + - (bcryptMinorRevision >= 'a' ? Nul : EmptyString)); - + using (var sha = new HMACSHA3_512(BCryptCore.SafeUTF8.GetBytes(hmacKey.ToString()))) + return BCryptCore.SafeUTF8.GetBytes(Convert.ToBase64String(sha.ComputeHash(BCryptCore.SafeUTF8.GetBytes(inputKey.ToString()))) + + (bcryptMinorRevision >= 'a' ? BCryptCore.Nul : BCryptCore.EmptyString)); case HashType.None: default: throw new ArgumentOutOfRangeException(nameof(hashType), hashType, null); From 8892e941ecfedb5dce7f15f0c21ce9859bfb6db6 Mon Sep 17 00:00:00 2001 From: Chris McKee Date: Wed, 7 May 2025 11:46:32 +0100 Subject: [PATCH 09/27] Work in progress --- benchmark/DecodeB64/Decoder.cs | 3 +- benchmark/Program.cs | 15 +-- benchmark/TestB64Decoder.cs | 36 +++++-- benchmark/TestB64Encoder.cs | 1 + benchmark/TestEnhanced_V3_Hmac.cs | 124 ++++++++++++++++++++++ benchmark/TestVariantsOnStringBuilding.cs | 85 ++++++++++----- src/BCrypt.Net/BCryptBase.cs | 3 +- src/BCrypt.Net/BCryptExtendedV3.cs | 60 ++++++++++- tests/UnitTests/BCryptTestsExtendedv3.cs | 14 +-- 9 files changed, 289 insertions(+), 52 deletions(-) create mode 100644 benchmark/TestEnhanced_V3_Hmac.cs diff --git a/benchmark/DecodeB64/Decoder.cs b/benchmark/DecodeB64/Decoder.cs index 07bd388..f13c4a9 100644 --- a/benchmark/DecodeB64/Decoder.cs +++ b/benchmark/DecodeB64/Decoder.cs @@ -140,7 +140,6 @@ internal static byte[] DecodeBase64StringCreateSpan(string encodedString, int ma } return ret.ToArray(); - } internal static byte[] DecodeBase64StandardSized(string encodedString, int maximumBytes) { @@ -249,7 +248,7 @@ internal static byte[] DecodeBase64StandardUnSized(string encodedString, int max } - public static int DecodeBase64(ReadOnlySpan encodedSpan, Span destination) + public static int DecodeBase64SpanBuffer(ReadOnlySpan encodedSpan, Span destination) { int outputLength = 0; int position = 0; diff --git a/benchmark/Program.cs b/benchmark/Program.cs index d52f48e..fb249ae 100644 --- a/benchmark/Program.cs +++ b/benchmark/Program.cs @@ -20,13 +20,14 @@ static void Main(string[] args) .With(Job.Default.With(CoreRuntime.Core90)) ; - BenchmarkRunner.Run(config); - BenchmarkRunner.Run(config); - BenchmarkRunner.Run(config); - BenchmarkRunner.Run(config); - // - // // Tests for testing in isolation - BenchmarkRunner.Run(config); + // BenchmarkRunner.Run(config); + // BenchmarkRunner.Run(config); + // BenchmarkRunner.Run(config); + // BenchmarkRunner.Run(config); + // BenchmarkRunner.Run(config); + // // + // // // Tests for testing in isolation + // BenchmarkRunner.Run(config); BenchmarkRunner.Run(config); BenchmarkRunner.Run(config); BenchmarkRunner.Run(config); diff --git a/benchmark/TestB64Decoder.cs b/benchmark/TestB64Decoder.cs index a00d21c..8b0dcba 100644 --- a/benchmark/TestB64Decoder.cs +++ b/benchmark/TestB64Decoder.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using BCryptNet.BenchMarks.DecodeB64; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Order; @@ -15,12 +16,35 @@ namespace BCryptNet.BenchMarks [KeepBenchmarkFiles] [MarkdownExporterAttribute.GitHub] // [ReturnValueValidator(failOnError: true)] + [IterationTime(500)] public class TestB64Decoder { + public TestB64Decoder() + { + var salt = "DCq7YPn5Rq63x1Lad4cll."; + var original = Convert.ToBase64String(DecodeB64Methods.DecodeBase64StandardUnSized(salt, 16)); + var exceptions = new List(); + + if (!Convert.ToBase64String(DecodeB64Methods.DecodeBase64StandardSized(salt, 16)).Equals(original)) + exceptions.Add(new Exception($"DecodeBase64StandardSized failed: {original} vs {Convert.ToBase64String(DecodeB64Methods.DecodeBase64StandardSized(salt, 16))}")); + if (!Convert.ToBase64String(DecodeB64Methods.DecodeBase64StringCreateSpan(salt, 16)).Equals(original)) + exceptions.Add(new Exception($"DecodeBase64StringCreateSpan failed: {original} vs {Convert.ToBase64String(DecodeB64Methods.DecodeBase64StringCreateSpan(salt, 16))}")); + if (!Convert.ToBase64String(DecodeB64Methods.DecodeBase64ToBytes(salt, 16)).Equals(original)) + exceptions.Add(new Exception($"DecodeBase64ToBytes failed: {original} vs {Convert.ToBase64String(DecodeB64Methods.DecodeBase64ToBytes(salt, 16))}")); + + Span saltBuffer = stackalloc byte[16]; + int written = DecodeB64Methods.DecodeBase64SpanBuffer(salt, saltBuffer); + if (!Convert.ToBase64String(saltBuffer[..written].ToArray()).Equals(original)) + exceptions.Add(new Exception($"DecodeBase64SpanBuffer failed: {original} vs {Convert.ToBase64String(saltBuffer[..written].ToArray())}")); + + if (exceptions.Count > 0) + throw new AggregateException(exceptions); + } + [Benchmark(Baseline = true)] [Arguments("DCq7YPn5Rq63x1Lad4cll.")] [Arguments("HqWuK6/Ng6sg9gQzbLrgb.")] - public byte[] DecodeBase64StandardUnSized(string salt) + public byte[] UnsizedStringBuilderOriginal(string salt) { return DecodeB64Methods.DecodeBase64StandardUnSized(salt, 16); } @@ -28,7 +52,7 @@ public byte[] DecodeBase64StandardUnSized(string salt) [Benchmark] [Arguments("DCq7YPn5Rq63x1Lad4cll.")] [Arguments("HqWuK6/Ng6sg9gQzbLrgb.")] - public byte[] DecodeBase64StandardSized(string salt) + public byte[] SizedStringBuilderOriginal(string salt) { return DecodeB64Methods.DecodeBase64StandardSized(salt, 16); } @@ -36,7 +60,7 @@ public byte[] DecodeBase64StandardSized(string salt) [Benchmark] [Arguments("DCq7YPn5Rq63x1Lad4cll.")] [Arguments("HqWuK6/Ng6sg9gQzbLrgb.")] - public byte[] DecodeBase64StringCreateSpan(string salt) + public byte[] StringCreateWithSpanAndBuffer(string salt) { return DecodeB64Methods.DecodeBase64StringCreateSpan(salt, 16); } @@ -52,10 +76,10 @@ public byte[] DecodeBase64ToBytes(string salt) [Benchmark] [Arguments("DCq7YPn5Rq63x1Lad4cll.")] [Arguments("HqWuK6/Ng6sg9gQzbLrgb.")] - public byte[] DecodeBase64ToSpan(string salt) + public byte[] DecodeBase64SpanBuffer(string salt) { - Span saltBuffer = stackalloc byte[128 / 8]; - int written = DecodeB64Methods.DecodeBase64(salt, saltBuffer); + Span saltBuffer = stackalloc byte[16]; + int written = DecodeB64Methods.DecodeBase64SpanBuffer(salt, saltBuffer); return saltBuffer[..written].ToArray(); } } diff --git a/benchmark/TestB64Encoder.cs b/benchmark/TestB64Encoder.cs index 789cacc..ea8fa0c 100644 --- a/benchmark/TestB64Encoder.cs +++ b/benchmark/TestB64Encoder.cs @@ -13,6 +13,7 @@ namespace BCryptNet.BenchMarks [Orderer(SummaryOrderPolicy.Declared)] [KeepBenchmarkFiles] [MarkdownExporterAttribute.GitHub] + [IterationTime(500)] public class TestB64Encoder { private static readonly byte[] SaltBytes = BCryptBaseLine.BCrypt.DecodeBase64("sGBxdT2q8Qd84NyZEkwTY.", 16); diff --git a/benchmark/TestEnhanced_V3_Hmac.cs b/benchmark/TestEnhanced_V3_Hmac.cs new file mode 100644 index 0000000..187c2af --- /dev/null +++ b/benchmark/TestEnhanced_V3_Hmac.cs @@ -0,0 +1,124 @@ +// /* +// The MIT License (MIT) +// Copyright (c) 2006 Damien Miller djm@mindrot.org (jBCrypt) +// Copyright (c) 2013 Ryan D. Emerle (.Net port) +// Copyright (c) 2016/2025 Chris McKee (.Net-core port / patches / new features) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +// (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, +// merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished +// to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// */ + +using System; +using System.Security.Cryptography; +using System.Text; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Order; + +namespace BCryptNet.BenchMarks; + +[MemoryDiagnoser] +/*[RPlotExporter]*/ +[RankColumn] +//[GcServer(true)] +[Orderer(SummaryOrderPolicy.Declared)] +[KeepBenchmarkFiles] +[MarkdownExporterAttribute.GitHub] +// [ReturnValueValidator(failOnError: true)] +[IterationTime(500)] +public class TestEnhancedV3_Hmac +{ + // Store the strings as fields + private const string HmacKeyString = "SuperSecureHMACKey"; + private const string InputKeyString = "SensitiveDataToHash"; + private ReadOnlySpan HmacKey => HmacKeyString.AsSpan(); + private ReadOnlySpan InputKey => InputKeyString.AsSpan(); + + private const HashType hashType = HashType.SHA256; + private const char BcryptMinorRevision = 'a'; + + [Benchmark(Baseline = true)] + public byte[] OldMethod() => EnhancedHashOld(HmacKey, InputKey, hashType, BcryptMinorRevision).ToArray(); + + [Benchmark] + public byte[] NewMethod() => EnhancedHash(HmacKey, InputKey, hashType, BcryptMinorRevision); + + private static Span EnhancedHashOld(ReadOnlySpan hmacKey, ReadOnlySpan inputKey, HashType hashType, char bcryptMinorRevision) + { + switch (hashType) + { + case HashType.SHA256: + using (var sha = new HMACSHA3_256(Encoding.UTF8.GetBytes(hmacKey.ToString()))) + return Encoding.UTF8.GetBytes(Convert.ToBase64String(sha.ComputeHash(Encoding.UTF8.GetBytes(inputKey.ToString()))) + + (bcryptMinorRevision >= 'a' ? "\0" : "")); + case HashType.SHA384: + using (var sha = new HMACSHA3_384(Encoding.UTF8.GetBytes(hmacKey.ToString()))) + return Encoding.UTF8.GetBytes(Convert.ToBase64String(sha.ComputeHash(Encoding.UTF8.GetBytes(inputKey.ToString()))) + + (bcryptMinorRevision >= 'a' ? "\0" : "")); + case HashType.SHA512: + using (var sha = new HMACSHA3_512(Encoding.UTF8.GetBytes(hmacKey.ToString()))) + return Encoding.UTF8.GetBytes(Convert.ToBase64String(sha.ComputeHash(Encoding.UTF8.GetBytes(inputKey.ToString()))) + + (bcryptMinorRevision >= 'a' ? "\0" : "")); + default: + throw new ArgumentOutOfRangeException(nameof(hashType), hashType, null); + } + } + + private static byte[] EnhancedHash(ReadOnlySpan hmacKey, ReadOnlySpan inputKey, HashType hashType, char bcryptMinorRevision) + { + ushort hashLen = hashType switch + { + HashType.SHA256 => 32, + HashType.SHA384 => 48, + HashType.SHA512 => 64, + _ => throw new ArgumentOutOfRangeException(nameof(hashType)) + }; + + Span keyBytes = stackalloc byte[Encoding.UTF8.GetMaxByteCount(hmacKey.Length)]; + Span dataBytes = stackalloc byte[Encoding.UTF8.GetMaxByteCount(inputKey.Length)]; + Span hash = stackalloc byte[hashLen]; + + int keyByteLen = Encoding.UTF8.GetBytes(hmacKey, keyBytes); + int dataByteLen = Encoding.UTF8.GetBytes(inputKey, dataBytes); + + bool success = hashType switch + { + HashType.SHA256 => HMACSHA3_256.TryHashData(keyBytes[..keyByteLen], dataBytes[..dataByteLen], hash, out int len) && len == 32, + HashType.SHA384 => HMACSHA3_384.TryHashData(keyBytes[..keyByteLen], dataBytes[..dataByteLen], hash, out int len) && len == 48, + HashType.SHA512 => HMACSHA3_512.TryHashData(keyBytes[..keyByteLen], dataBytes[..dataByteLen], hash, out int len) && len == 64, + _ => throw new ArgumentOutOfRangeException(nameof(hashType)) + }; + + if (!success) + throw new Exception($"HMAC-{hashType} failed"); + + Span base64Chars = stackalloc char[(hashLen + 2) / 3 * 4]; + if (!Convert.TryToBase64Chars(hash, base64Chars, out int base64Len)) + throw new Exception("Base64 encoding failed in EnhancedHash"); + + Span finalBase64 = stackalloc char[base64Len + (bcryptMinorRevision >= 'a' ? 1 : 0)]; + base64Chars[..base64Len].CopyTo(finalBase64); + if (bcryptMinorRevision >= 'a') finalBase64[base64Len] = '\0'; + + Span utf8Buffer = stackalloc byte[Encoding.UTF8.GetMaxByteCount(finalBase64.Length)]; + int utf8Len = Encoding.UTF8.GetBytes(finalBase64, utf8Buffer); + + return utf8Buffer[..utf8Len].ToArray(); + } + + public enum HashType + { + None, + SHA256, + SHA384, + SHA512 + } +} diff --git a/benchmark/TestVariantsOnStringBuilding.cs b/benchmark/TestVariantsOnStringBuilding.cs index 1eb998c..1819b66 100644 --- a/benchmark/TestVariantsOnStringBuilding.cs +++ b/benchmark/TestVariantsOnStringBuilding.cs @@ -1,4 +1,6 @@ -using System.Globalization; +using System; +using System.Globalization; +using System.Runtime.CompilerServices; using System.Text; using BCryptNet.BenchMarks._3._2._1; using BCryptNet.BenchMarks.EncodeB64; @@ -16,6 +18,7 @@ namespace BCryptNet.BenchMarks [KeepBenchmarkFiles] [MarkdownExporterAttribute.GitHub] [ReturnValueValidator(failOnError: true)] + [IterationTime(500)] public class TestVariantsOnStringBuilding { private readonly string bcryptMinorRevision = "a"; @@ -45,7 +48,7 @@ public string Original_StrBuilder_SinEncoding() [Benchmark] [BenchmarkCategory("StringAppend", "AppendChar")] - public string Original_StrBuilder_SinEncoding_AppendChar() + public string StrBuilder_SinEncoding_UsingAppendFormatAndAppendChar() { // Generate result string StringBuilder result = new StringBuilder(); @@ -58,7 +61,7 @@ public string Original_StrBuilder_SinEncoding_AppendChar() [Benchmark] [BenchmarkCategory("StringAppend", "AppendChar")] - public string Original_StrBuilder_SinEncoding_AppendChar_Sized() + public string SizedStrBuilder_SinEncoding_UsingAppendFormatAndAppendChar() { // Generate result string StringBuilder result = new StringBuilder(60); @@ -71,10 +74,14 @@ public string Original_StrBuilder_SinEncoding_AppendChar_Sized() [Benchmark] [BenchmarkCategory("StringAppend", "AppendChar")] - public string Original_StrBuilder_SinEncoding_AppendChar_Sized_PRFmt() + public string SizedStrBuilder_SinEncoding_UsingAppendStringAndChar() { var result = new StringBuilder(60); - result.Append("$2").Append(bcryptMinorRevision).Append('$').Append(workFactor.ToString("D2")).Append('$'); + result.Append("$2"); + result.Append(bcryptMinorRevision); + result.Append('$'); + result.Append(workFactor.ToString("D2")); + result.Append('$'); result.Append(EncodedSaltAsChars); result.Append(EncodedHashAsChars); @@ -83,34 +90,47 @@ public string Original_StrBuilder_SinEncoding_AppendChar_Sized_PRFmt() [Benchmark] [BenchmarkCategory("StringAppend", "AppendChar")] - public string Original_StrBuilder_SinEncoding_AppendChar_Sized_PRFmt_MoreChar() + public string SizedStrBuilder_SinEncoding_UsingAppendStringAndChar_v2() { var result = new StringBuilder(60); - result.Append('$').Append('2').Append(bcryptMinorRevision).Append('$').Append(workFactor.ToString("D2", CultureInfo.InvariantCulture)).Append('$') - .Append(EncodedSaltAsChars) - .Append(EncodedHashAsChars); + result.Append('$'); + result.Append('2'); + result.Append(bcryptMinorRevision); + result.Append('$'); + result.Append(workFactor.ToString("D2", CultureInfo.InvariantCulture)); + result.Append('$'); + result.Append(EncodedSaltAsChars); + result.Append(EncodedHashAsChars); return result.ToString(); } [Benchmark] [BenchmarkCategory("StringAppend", "AppendChar")] - public string Original_StrBuilder_SinEncoding_AppendChar_Sized_PRFmt_MoreString() + public string SizedStrBuilder_SinEncoding_UsingAppendStrings_HashSaltAsChar() { var result = new StringBuilder(60); - result.Append("$2").Append(bcryptMinorRevision).Append("$").Append(workFactor.ToString("D2")).Append("$") - .Append(EncodedSaltAsChars) - .Append(EncodedHashAsChars); + result.Append("$2"); + result.Append(bcryptMinorRevision); + result.Append("$"); + result.Append(workFactor.ToString("D2")); + result.Append("$"); + result.Append(EncodedSaltAsChars); + result.Append(EncodedHashAsChars); return result.ToString(); } [Benchmark] [BenchmarkCategory("StringAppend", "AppendString")] - public string Original_StrBuilder_SinEncoding_AppendChar_Sized_PRFmt_StringNotChar() + public string SizedStrBuilder_SinEncoding_UsingAppendStrings_HashSaltAsString_WorkFactorToString() { var result = new StringBuilder(60); - result.Append("$2").Append(bcryptMinorRevision).Append("$").Append(workFactor.ToString("D2")).Append("$"); + result.Append("$2"); + result.Append(bcryptMinorRevision); + result.Append("$"); + result.Append(workFactor.ToString("D2")); + result.Append("$"); result.Append(salt); result.Append(hash); @@ -119,20 +139,21 @@ public string Original_StrBuilder_SinEncoding_AppendChar_Sized_PRFmt_StringNotCh [Benchmark] [BenchmarkCategory("StringAppend", "AppendString")] - public string Original_StrBuilder_SinEncoding_AppendChar_Sized_FROMSTRING_PRFmt_plusfmt() + public string SizedStrBuilder_SinEncoding_UsingAppendStrings_WorkFactorInterpolated() { var result = new StringBuilder(60); - result.Append("$2") - .Append(bcryptMinorRevision) - .Append("$") - .Append($"{workFactor:00}") - .Append("$") - .Append(salt) - .Append(hash); + result.Append("$2"); + result.Append(bcryptMinorRevision); + result.Append("$"); + result.Append($"{workFactor:00}"); + result.Append("$"); + result.Append(salt); + result.Append(hash); return result.ToString(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static char[] Concatenate(char[] array1, char[] array2) { char[] result = new char[array1.Length + array2.Length]; @@ -141,6 +162,15 @@ public static char[] Concatenate(char[] array1, char[] array2) return result; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string ConcatenateToString(char[] array1, char[] array2) + { + Span result = stackalloc char[array1.Length + array2.Length]; + array1.CopyTo(result); + array2.CopyTo(result.Slice(array1.Length)); + return new string(result); + } + [Benchmark] [BenchmarkCategory("StringFmt", "AppendChar")] public string StringInterpolation_WithChar() @@ -150,11 +180,18 @@ public string StringInterpolation_WithChar() [Benchmark] [BenchmarkCategory("StringFmt", "AppendChar")] - public string StringInterpolation_WithCharMerged() + public string StringInterpolation_WithCharsConcat() { return $"$2{bcryptMinorRevision}${workFactor:00}${new string(Concatenate(EncodedSaltAsChars, EncodedHashAsChars))}"; } + [Benchmark] + [BenchmarkCategory("StringFmt", "AppendChar")] + public string StringInterpolation_WithAllocConcat() + { + return $"$2{bcryptMinorRevision}${workFactor:00}${ConcatenateToString(EncodedSaltAsChars, EncodedHashAsChars)}"; + } + [Benchmark] [BenchmarkCategory("StringFmt", "AppendString")] public string StringInterpolation_WithString() diff --git a/src/BCrypt.Net/BCryptBase.cs b/src/BCrypt.Net/BCryptBase.cs index a59b050..e7400bc 100644 --- a/src/BCrypt.Net/BCryptBase.cs +++ b/src/BCrypt.Net/BCryptBase.cs @@ -359,7 +359,7 @@ internal static void CreatePasswordHash(ReadOnlySpan inputKey, ReadOnlySpa { case HashType.None: bool appendNul = bcryptMinorRevision >= 'a'; - Span utf8Buffer = stackalloc byte[Encoding.UTF8.GetMaxByteCount(inputKey.Length + (appendNul ? 1 : 0))]; + Span utf8Buffer = stackalloc byte[SafeUTF8.GetMaxByteCount(inputKey.Length + (appendNul ? 1 : 0))]; int bytesWritten = SafeUTF8.GetBytes(inputKey, utf8Buffer); if (appendNul) utf8Buffer[bytesWritten++] = 0; Span inputBytes = utf8Buffer[..bytesWritten]; @@ -529,6 +529,7 @@ internal static ReadOnlySpan EncodeBase64(ReadOnlySpan byteArray, in /// The string to decode. /// /// The decoded byte array. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int DecodeBase64(ReadOnlySpan encodedSpan, Span destination) { int outputLength = 0; diff --git a/src/BCrypt.Net/BCryptExtendedV3.cs b/src/BCrypt.Net/BCryptExtendedV3.cs index 6a06ae6..4d14549 100644 --- a/src/BCrypt.Net/BCryptExtendedV3.cs +++ b/src/BCrypt.Net/BCryptExtendedV3.cs @@ -1,4 +1,5 @@ #if NETCOREAPP +using System.Diagnostics; using System.Globalization; using System.Security.Cryptography; @@ -64,18 +65,67 @@ public static string HashPassword(string hmacKey, string inputKey, string salt, /// HashType used (default SHA384) /// The hashed password. /// Thrown when the salt could not be parsed. - public static string HashPassword(string hmacKey, ReadOnlySpan inputKey, ReadOnlySpan salt, HashType hashType = DefaultEnhancedHashType) => - CreatePasswordHash(inputKey, salt, hashType, (key, type, version) => EnhancedHash(hmacKey,key, type, version)); + public static string HashPassword(string hmacKey, ReadOnlySpan inputKey, ReadOnlySpan salt, HashType hashType = DefaultEnhancedHashType) + { + return CreatePasswordHash(inputKey, salt, hashType, (key, type, version) => EnhancedHash(hmacKey, key, type, version)); + } /// - /// Hashes key, base64 encodes before returning byte array + /// HMAC-SHA3 hashes input key before bcrypt hashing /// /// Key used in HMAC hashing /// - /// (Default: 'a') /// HashType used (default SHA3 HMAC 384 - https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.hmacsha3_256) + /// (Default: 'a') /// - private static Span EnhancedHash(ReadOnlySpan hmacKey, ReadOnlySpan inputKey, HashType hashType, char bcryptMinorRevision = 'a') + private static byte[] EnhancedHash(ReadOnlySpan hmacKey, ReadOnlySpan inputKey, HashType hashType, char bcryptMinorRevision = 'a') + { + ushort hashLen = hashType switch + { + HashType.SHA256 => 32, + HashType.SHA384 => 48, + HashType.SHA512 => 64, + _ => throw new ArgumentOutOfRangeException(nameof(hashType)) + }; + + Span keyBytes = stackalloc byte[SafeUTF8.GetMaxByteCount(hmacKey.Length)]; + Span dataBytes = stackalloc byte[SafeUTF8.GetMaxByteCount(inputKey.Length)]; + Span hash = stackalloc byte[hashLen]; + + // UTF8 encode key and data + int keyByteLen = SafeUTF8.GetBytes(hmacKey, keyBytes); + int dataByteLen = SafeUTF8.GetBytes(inputKey, dataBytes); + + // Perform HMAC + bool success = hashType switch + { + HashType.SHA256 => HMACSHA3_256.TryHashData(keyBytes[..keyByteLen], dataBytes[..dataByteLen], hash, out int len) && len == 32, + HashType.SHA384 => HMACSHA3_384.TryHashData(keyBytes[..keyByteLen], dataBytes[..dataByteLen], hash, out int len) && len == 48, + HashType.SHA512 => HMACSHA3_512.TryHashData(keyBytes[..keyByteLen], dataBytes[..dataByteLen], hash, out int len) && len == 64, + _ => throw new ArgumentOutOfRangeException(nameof(hashType)) + }; + + if (!success) + throw new BcryptAuthenticationException($"HMAC-{hashType} failed"); + + // Base64 encode + Span base64Chars = stackalloc char[(hashLen + 2) / 3 * 4]; + if (!Convert.TryToBase64Chars(hash, base64Chars, out int base64Len)) + throw new BcryptAuthenticationException("Base64 encoding failed in EnhancedHash"); + + // Append `Nul` only **after** base64 encoding + Span finalBase64 = stackalloc char[base64Len + (bcryptMinorRevision >= 'a' ? 1 : 0)]; + base64Chars[..base64Len].CopyTo(finalBase64); + if (bcryptMinorRevision >= 'a') finalBase64[base64Len] = '\0'; + + // UTF8 encode final base64 result + Span utf8Buffer = stackalloc byte[SafeUTF8.GetMaxByteCount(finalBase64.Length)]; + int utf8Len = SafeUTF8.GetBytes(finalBase64, utf8Buffer); + + return utf8Buffer[..utf8Len].ToArray(); + } + + private static Span EnhancedHashOld(ReadOnlySpan hmacKey, ReadOnlySpan inputKey, HashType hashType, char bcryptMinorRevision = 'a') { switch (hashType) { diff --git a/tests/UnitTests/BCryptTestsExtendedv3.cs b/tests/UnitTests/BCryptTestsExtendedv3.cs index c9651ba..4d929a4 100644 --- a/tests/UnitTests/BCryptTestsExtendedv3.cs +++ b/tests/UnitTests/BCryptTestsExtendedv3.cs @@ -161,12 +161,12 @@ public void TestValidateAndReplaceWithForceAndWorkloadSmallerThanCurrentEndsWith string currentKey = "~!@#$%^&*() ~!@#$%^&*()PNBFRD"; string salt = "$2a$12$WApznUOJfkEGSmYRfnkrPO"; - string currentHash = "$2a$12$WApznUOJfkEGSmYRfnkrPO/jMqrnJc5PFWasgccSlw6RlvYsWV4sS"; + string expectedHash = "$2a$12$WApznUOJfkEGSmYRfnkrPO/jMqrnJc5PFWasgccSlw6RlvYsWV4sS"; string newPassword = "my new password"; - string hashed = BCryptExtendedV3.HashPassword(hmacKey,currentKey, salt); - Assert.Equal(hashed, currentHash); - var replHash = BCryptExtendedV3.ValidateAndUpgradeHash(hmacKey, currentKey, currentHash, newPassword, workFactor: 5, forceWorkFactor: true); + string hashed = BCryptExtendedV3.HashPassword(hmacKey, currentKey, salt); + Assert.Equal(expectedHash, hashed); + var replHash = BCryptExtendedV3.ValidateAndUpgradeHash(hmacKey, currentKey, expectedHash, newPassword, workFactor: 5, forceWorkFactor: true); Assert.Contains("$05$", replHash); Trace.Write("."); } @@ -208,7 +208,7 @@ public void TestNaughtyStringsHash(string pw1) Trace.Write("BCrypt.HashPassword with naughty strings: "); var hmacKey = Guid.NewGuid().ToString(); - string h1 = BCryptExtendedV3.HashPassword(hmacKey,pw1, BCrypt.GenerateSalt()); + string h1 = BCryptExtendedV3.HashPassword(hmacKey,pw1, BCryptCore.GenerateSalt()); Assert.True(BCryptExtendedV3.Verify(hmacKey,pw1, h1)); Trace.Write("."); @@ -221,7 +221,7 @@ public void TestNaughtyStringsHash(string pw1) public void NullTerminationCausesBCryptToTerminateStringInSomeFrameworks(string password) { var hmacKey = Guid.NewGuid().ToString(); - var x = BCryptExtendedV3.GenerateSalt(); + var x = BCryptCore.GenerateSalt(); string hash = BCryptExtendedV3.HashPassword(hmacKey,password, x); var t1 = BCryptExtendedV3.Verify(hmacKey,password, hash); @@ -235,7 +235,7 @@ public void NullTerminationCausesBCryptToTerminateStringInSomeFrameworks(string public void NullTerminationCausesBCryptToTerminateStringInSomeFrameworksSetB(string password, string leader) { var hmacKey = Guid.NewGuid().ToString(); - var x = BCryptExtendedV3.GenerateSalt(); + var x = BCryptCore.GenerateSalt(); string hash = BCryptExtendedV3.HashPassword(hmacKey,password, x); Assert.False(ContainsNoNullBytes(SafeUtf8.GetBytes(password))); From 87c54dc31ae11cc82e5f6611710c763f6b5041e3 Mon Sep 17 00:00:00 2001 From: Chris McKee Date: Wed, 7 May 2025 12:34:26 +0100 Subject: [PATCH 10/27] chore: bulk package/actions update --- .github/workflows/ci-build.yml | 18 +-- .../workflows/ci-manual-build-test-sign.yml | 125 ++++++------------ .github/workflows/codeql-analysis.yml | 4 +- .github/workflows/dependency-review.yml | 2 +- .github/workflows/devskim.yml | 2 +- .github/workflows/generate-publish-docs.yml | 7 +- .github/workflows/markdown-link-check.yml | 2 +- .github/workflows/upload-coverage-report.yml | 6 +- Directory.Packages.props | 14 +- 9 files changed, 69 insertions(+), 111 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 2ad2234..7df381f 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -29,7 +29,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: 'Harden Runner' - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 with: egress-policy: audit @@ -39,7 +39,7 @@ jobs: fetch-depth: 0 # avoid shallow clone so nbgv can do its work. - name: 'Setup .NET SDK' - uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: dotnet-version: 9.0.x @@ -51,7 +51,7 @@ jobs: run: dotnet build --configuration Debug --no-restore - name: Upload Build Artifacts - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: build-artifacts path: | @@ -63,17 +63,17 @@ jobs: - name: 'Test' id: test - run: dotnet test --no-build --restore --collect:"XPlat Code Coverage" --logger junit tests/UnitTests/BCrypt.Net.UnitTests.csproj + run: dotnet test --restore --collect:"XPlat Code Coverage" --logger junit --configuration Debug - name: 'Create test summary' uses: test-summary/action@31493c76ec9e7aa675f1585d3ed6f1da69269a86 # v2.4 with: - paths: tests/**/TestResults.xml + paths: tests/UnitTests/**/TestResults.xml show: "fail, skip" if: always() - name: 'Generate Coverage Reports' - uses: danielpalme/ReportGenerator-GitHub-Action@c38c522d4b391c1b0da979cbb2e902c0a252a7dc # 5.4.3 + uses: danielpalme/ReportGenerator-GitHub-Action@cc137d2b561c02b63ae869ffbe8f68af9d904bf4 # 5.4.6 with: reports: "tests/**/coverage.cobertura.xml" targetdir: "${{ github.workspace }}" @@ -98,7 +98,7 @@ jobs: thresholds: "10 30" - name: Upload Code Coverage Results - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: coverage-results path: | @@ -107,13 +107,13 @@ jobs: retention-days: 5 - name: Publish Test Results - uses: EnricoMi/publish-unit-test-result-action@170bf24d20d201b842d7a52403b73ed297e6645b # v2.18.0 + uses: EnricoMi/publish-unit-test-result-action@afb2984f4d89672b2f9d9c13ae23d53779671984 # v2.19.0 if: always() with: files: "tests/**/TestResults.xml" - name: Upload Test Artifacts - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: test-results path: "tests/**/TestResults.xml" diff --git a/.github/workflows/ci-manual-build-test-sign.yml b/.github/workflows/ci-manual-build-test-sign.yml index bb00ce3..6557bbc 100644 --- a/.github/workflows/ci-manual-build-test-sign.yml +++ b/.github/workflows/ci-manual-build-test-sign.yml @@ -34,7 +34,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 with: egress-policy: audit @@ -44,7 +44,7 @@ jobs: fetch-depth: 0 # avoid shallow clone so nbgv can do its work. - name: 'Setup .NET SDK' - uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: dotnet-version: 9.0.x @@ -77,92 +77,45 @@ jobs: Get-ChildItem -Path ${{ env.nupkgDirectory }} -Recurse -Force - name: Upload unsigned nupkgs - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: build-artifacts path: ${{ env.nupkgDirectory }}/* retention-days: 7 - sign: - name: Sign - needs: build - runs-on: windows-latest - if: ${{ inputs.perform_sign }} - environment: release - permissions: - contents: read - id-token: write - steps: - - name: 'Setup .NET SDK' - uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0 - - - name: 'Install Sign CLI' - run: dotnet tool install --tool-path ./sign --prerelease sign - - - name: 'Gather nupkgs from build output' - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - with: - name: build-artifacts - path : ${{ env.nupkgDirectory }} - - - name: List assets to be signed - shell: pwsh - run: > - Get-ChildItem -Path ${{ env.nupkgDirectory }} -Include *.nupkg -Recurse -Force - - - name: Authenticate to Azure - uses: azure/login@a65d910e8af852a8061c627c456678983e180302 # 2.2.0 - with: - allow-no-subscriptions : true - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - - name: Sign - shell: pwsh - run: > - ./sign/sign code azure-key-vault *.nupkg --base-directory ${{ env.nupkgDirectory }} --azure-key-vault-url "${{ secrets.AZURE_KEY_VAULT_URL }}" --azure-key-vault-certificate "${{ secrets.AZURE_KEY_VAULT_CERTIFICATE }}" - - - name: Upload signed nupkgs - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 - with: - name: signed-artifacts - path: ${{ env.nupkgDirectory }}/* - retention-days: 7 - - publish: - name: Publish to nuget - needs: sign - runs-on: ubuntu-latest - if: ${{ inputs.perform_publish }} - environment: release - permissions: - id-token: write - steps: - - name: 'Harden Runner' - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 - with: - egress-policy: audit - - - name: 'Setup .NET SDK' - uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0 - - - name: 'Gather nupkgs from signing output' - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - with: - name: signed-artifacts - path : ${{ env.nupkgDirectory }} - - - name: List assets to be published - shell: pwsh - run: > - Get-ChildItem -Path ${{ env.nupkgDirectory }} -Filter *.nupkg -Recurse -Force - - # Use --skip-duplicate to prevent errors if a package with the same version already exists. - # This allows a retry of a failed workflow, already published packages will be skipped without error. - - name: Publish NuGet package - shell: pwsh - run: > - foreach($file in (Get-ChildItem "${{ env.nupkgDirectory }}" -Recurse -Filter *.nupkg)) { - dotnet nuget push $file --api-key "${{ secrets.NUGET_APIKEY }}" --source https://api.nuget.org/v3/index.json --skip-duplicate - } + # publish: + # name: Publish to nuget + # needs: sign + # runs-on: ubuntu-latest + # if: ${{ inputs.perform_publish }} + # environment: release + # permissions: + # id-token: write + # steps: + # - name: 'Harden Runner' + # uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 + # with: + # egress-policy: audit + + # - name: 'Setup .NET SDK' + # uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 + + # - name: 'Gather nupkgs from signing output' + # uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + # with: + # name: signed-artifacts + # path : ${{ env.nupkgDirectory }} + + # - name: List assets to be published + # shell: pwsh + # run: > + # Get-ChildItem -Path ${{ env.nupkgDirectory }} -Filter *.nupkg -Recurse -Force + + # # Use --skip-duplicate to prevent errors if a package with the same version already exists. + # # This allows a retry of a failed workflow, already published packages will be skipped without error. + # - name: Publish NuGet package + # shell: pwsh + # run: > + # foreach($file in (Get-ChildItem "${{ env.nupkgDirectory }}" -Recurse -Filter *.nupkg)) { + # dotnet nuget push $file --api-key "${{ secrets.NUGET_APIKEY }}" --source https://api.nuget.org/v3/index.json --skip-duplicate + # } diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index b303339..98a4b83 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -49,7 +49,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 with: egress-policy: audit @@ -59,7 +59,7 @@ jobs: fetch-depth: 0 # avoid shallow clone so nbgv can do its work. - name: 'Setup .NET SDK' - uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: dotnet-version: 9.0.x diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 51b724c..f3ab11f 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 with: egress-policy: audit diff --git a/.github/workflows/devskim.yml b/.github/workflows/devskim.yml index 2b3912c..3e31856 100644 --- a/.github/workflows/devskim.yml +++ b/.github/workflows/devskim.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 with: egress-policy: audit - uses: actions/checkout@v4 diff --git a/.github/workflows/generate-publish-docs.yml b/.github/workflows/generate-publish-docs.yml index bb170d7..971a58e 100644 --- a/.github/workflows/generate-publish-docs.yml +++ b/.github/workflows/generate-publish-docs.yml @@ -29,13 +29,18 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} steps: + - name: Harden Runner + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 + with: + egress-policy: audit + - name: 'Checkout' uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 # avoid shallow clone so nbgv can do its work. - name: 'Setup .NET SDK' - uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: dotnet-version: 9.0.x diff --git a/.github/workflows/markdown-link-check.yml b/.github/workflows/markdown-link-check.yml index 44bdff1..0eec6ce 100644 --- a/.github/workflows/markdown-link-check.yml +++ b/.github/workflows/markdown-link-check.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 with: egress-policy: audit diff --git a/.github/workflows/upload-coverage-report.yml b/.github/workflows/upload-coverage-report.yml index a9fc86d..c887166 100644 --- a/.github/workflows/upload-coverage-report.yml +++ b/.github/workflows/upload-coverage-report.yml @@ -18,17 +18,17 @@ jobs: github.event.workflow_run.conclusion == 'success' steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 with: egress-policy: audit - name: 'Download coverage results' - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: coverage-results - name: Add Code Coverage PR Comment - uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1 + uses: marocchino/sticky-pull-request-comment@67d0dec7b07ed060a405f9b2a64b8ab319fdd7db # v2.9.2 if: github.event_name == 'pull_request' with: recreate: true diff --git a/Directory.Packages.props b/Directory.Packages.props index 67c92ba..1dacc93 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,20 +7,20 @@ - + - - + + - - + + - - + + From e0172f610aa4fb4d68810bc923751d154eaada29 Mon Sep 17 00:00:00 2001 From: Chris McKee Date: Mon, 8 Sep 2025 18:09:53 +0100 Subject: [PATCH 11/27] feat: Memory safety changes * 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 --- BCrypt.Net.sln | 6 ++ Directory.Packages.props | 2 +- docs/docs/common-topics.md | 25 +++++ src/BCrypt.Net/BCrypt.Net.csproj | 5 + src/BCrypt.Net/BCrypt.cs | 3 +- src/BCrypt.Net/BCryptBase.Fwk.cs | 13 ++- src/BCrypt.Net/BCryptBase.cs | 50 +++++++--- src/BCrypt.Net/SafeStringExtension.Fwk.cs | 104 ++++++++++++++++++++ src/BCrypt.Net/SafeStringExtension.cs | 94 ++++++++++++++++++ tests/UnitTests/BCrypt.Net.UnitTests.csproj | 2 +- tests/UnitTests/BCryptTests.cs | 73 ++++++++++++++ 11 files changed, 355 insertions(+), 22 deletions(-) create mode 100644 docs/docs/common-topics.md create mode 100644 src/BCrypt.Net/SafeStringExtension.Fwk.cs create mode 100644 src/BCrypt.Net/SafeStringExtension.cs diff --git a/BCrypt.Net.sln b/BCrypt.Net.sln index cee3569..68a6653 100644 --- a/BCrypt.Net.sln +++ b/BCrypt.Net.sln @@ -55,14 +55,20 @@ Global {CD69F016-5940-4FCA-BCA1-9D1D87C6F873}.CodeQL|Any CPU.Build.0 = CodeQL|Any CPU {2078EB7B-7EDF-4B65-80F7-DB4D92E08CCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2078EB7B-7EDF-4B65-80F7-DB4D92E08CCD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2078EB7B-7EDF-4B65-80F7-DB4D92E08CCD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2078EB7B-7EDF-4B65-80F7-DB4D92E08CCD}.Release|Any CPU.Build.0 = Release|Any CPU {E75AA3B8-BF28-4366-B5C6-14AF342290C3}.Release|Any CPU.ActiveCfg = Release|Any CPU {E75AA3B8-BF28-4366-B5C6-14AF342290C3}.Release|Any CPU.Build.0 = Release|Any CPU {E75AA3B8-BF28-4366-B5C6-14AF342290C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E75AA3B8-BF28-4366-B5C6-14AF342290C3}.Debug|Any CPU.Build.0 = Debug|Any CPU {A24DEBF7-6ED9-4774-B67F-7A0EE18125C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A24DEBF7-6ED9-4774-B67F-7A0EE18125C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A24DEBF7-6ED9-4774-B67F-7A0EE18125C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A24DEBF7-6ED9-4774-B67F-7A0EE18125C5}.Release|Any CPU.Build.0 = Release|Any CPU {CE412A71-A08D-4263-8960-664827DA526B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CE412A71-A08D-4263-8960-664827DA526B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE412A71-A08D-4263-8960-664827DA526B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE412A71-A08D-4263-8960-664827DA526B}.Release|Any CPU.Build.0 = Release|Any CPU {70440057-8D4E-41B0-8DF7-8A67B4C0EE28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {70440057-8D4E-41B0-8DF7-8A67B4C0EE28}.Debug|Any CPU.Build.0 = Debug|Any CPU {70440057-8D4E-41B0-8DF7-8A67B4C0EE28}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/Directory.Packages.props b/Directory.Packages.props index 1dacc93..c4926c7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,7 +7,7 @@ - + diff --git a/docs/docs/common-topics.md b/docs/docs/common-topics.md new file mode 100644 index 0000000..beadd29 --- /dev/null +++ b/docs/docs/common-topics.md @@ -0,0 +1,25 @@ +--- +uid: common-topics +--- + + +## Secure String / In memory secrets / protection against memory dumps or RAM attacks + + + + +Refs: + +* [.net Remarks on SecureString](https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-security-securestring) +* [Dotnet Design Conversation around replacing SecureString](https://github.com/dotnet/designs/pull/147) +* [Microsoft Docs on SecureString](https://learn.microsoft.com/en-us/dotnet/api/system.security.securestring) + +Relevant .net Code + +* +* [](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Security/SecureString.cs) + +Related Issues + +* https://github.com/BcryptNet/bcrypt.net/issues/83 +* https://github.com/dotnet/runtime/issues/118484#issuecomment-3165098658 diff --git a/src/BCrypt.Net/BCrypt.Net.csproj b/src/BCrypt.Net/BCrypt.Net.csproj index 4204ba5..c0257c9 100644 --- a/src/BCrypt.Net/BCrypt.Net.csproj +++ b/src/BCrypt.Net/BCrypt.Net.csproj @@ -8,6 +8,7 @@ BCryptNet false true + true Debug;Release;CodeQL @@ -24,6 +25,10 @@ + + true + + diff --git a/src/BCrypt.Net/BCrypt.cs b/src/BCrypt.Net/BCrypt.cs index 5609075..16dd7a4 100644 --- a/src/BCrypt.Net/BCrypt.cs +++ b/src/BCrypt.Net/BCrypt.cs @@ -167,8 +167,7 @@ public static string HashPassword(string inputKey, int workFactor = DefaultRound /// You should generally leave generating salts to the library. /// /// The password to hash. - /// The log2 of the number of rounds of hashing to apply - the work - /// factor therefore increases as 2^workFactor. Default is 11 + /// the salt to hash with (best generated using ) /// The hashed password. /// Thrown when the salt could not be parsed. public static string HashPassword(string inputKey, string salt) diff --git a/src/BCrypt.Net/BCryptBase.Fwk.cs b/src/BCrypt.Net/BCryptBase.Fwk.cs index 56f5d43..2bbbbe1 100644 --- a/src/BCrypt.Net/BCryptBase.Fwk.cs +++ b/src/BCrypt.Net/BCryptBase.Fwk.cs @@ -29,18 +29,21 @@ namespace BCryptNet; public partial class BCryptCore { #if !NETCOREAPP + + internal delegate byte[] EnhancedHashDelegate(string inputKey, HashType hashType, char bcryptMinorRevision); + /// /// Create Password Hash Base /// - /// - /// - /// - /// + /// The password or string to hash. + /// the salt to hash with (best generated using ) + /// SHA Based Hash to use + /// Delegate /// /// /// /// - internal static string CreatePasswordHash(string inputKey, string salt, HashType hashType = HashType.None, Func enhancedHashKeyGen = null) + internal static string CreatePasswordHash(string inputKey, string salt, HashType hashType = HashType.None, EnhancedHashDelegate enhancedHashKeyGen = null) { if (string.IsNullOrEmpty(salt)) { diff --git a/src/BCrypt.Net/BCryptBase.cs b/src/BCrypt.Net/BCryptBase.cs index e7400bc..738f28a 100644 --- a/src/BCrypt.Net/BCryptBase.cs +++ b/src/BCrypt.Net/BCryptBase.cs @@ -212,8 +212,32 @@ public partial class BCryptCore private uint[] _p; private uint[] _s; + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + internal static void ZeroMemory(Span buffer) + { + // NoOptimize to prevent the optimizer from deciding this call is unnecessary + // NoInlining to prevent the inliner from forgetting that the method was no-optimize + buffer.Clear(); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + internal static void ZeroMemory(Span buffer) + { + // NoOptimize to prevent the optimizer from deciding this call is unnecessary + // NoInlining to prevent the inliner from forgetting that the method was no-optimize + buffer.Clear(); + } + // Compares two byte arrays for equality. The method is specifically written so that the loop is not optimised. [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] +#if NETCOREAPP + internal static bool SecureEquals(ReadOnlySpan a, ReadOnlySpan b) + { + if (a.Length != b.Length) + { + return false; + } +#else internal static bool SecureEquals(byte[] a, byte[] b) { if (a == null && b == null) @@ -225,9 +249,10 @@ internal static bool SecureEquals(byte[] a, byte[] b) { return false; } - +#endif + int length = a.Length; int diff = 0; - for (var i = 0; i < a.Length; i++) + for (var i = 0; i < length; i++) { diff |= (a[i] ^ b[i]); } @@ -257,6 +282,8 @@ private void InitializeKey() } #if NETCOREAPP + internal delegate Span EnhancedHashDelegate(ReadOnlySpan inputKey, HashType hashType, char bcryptMinorRevision); + /// /// Create Password Hash Base /// @@ -275,12 +302,6 @@ internal static string CreatePasswordHash(ReadOnlySpan inputKey, ReadOnlyS return new string(outputBuffer[..outputBufferWritten]); } - #if NETCOREAPP - internal delegate Span EnhancedHashDelegate(ReadOnlySpan inputKey, HashType hashType, char bcryptMinorRevision); - #else - internal delegate byte[] EnhancedHashDelegate(string inputKey, HashType hashType, char bcryptMinorRevision); - #endif - /// /// Create Password Hash Base /// @@ -294,7 +315,9 @@ internal static string CreatePasswordHash(ReadOnlySpan inputKey, ReadOnlyS /// /// /// - internal static void CreatePasswordHash(ReadOnlySpan inputKey, ReadOnlySpan salt, Span outputBuffer, out int outputBufferWritten, HashType hashType = HashType.None, + internal static void CreatePasswordHash(ReadOnlySpan inputKey, ReadOnlySpan salt, + Span outputBuffer, out int outputBufferWritten, + HashType hashType = HashType.None, EnhancedHashDelegate enhancedHashKeyGen = null) { if (salt.IsEmpty) @@ -363,10 +386,10 @@ internal static void CreatePasswordHash(ReadOnlySpan inputKey, ReadOnlySpa int bytesWritten = SafeUTF8.GetBytes(inputKey, utf8Buffer); if (appendNul) utf8Buffer[bytesWritten++] = 0; Span inputBytes = utf8Buffer[..bytesWritten]; - if (!HashBytes(inputBytes, salt.Slice(startingOffset + 3, 22), bcryptMinorRevision, workFactor, outputBuffer, out int hashBytesWriten)) + if (!HashBytes(inputBytes, salt.Slice(startingOffset + 3, 22), bcryptMinorRevision, workFactor, outputBuffer, out int hashBytesWritten)) throw new BcryptAuthenticationException("Couldn't hash input"); - - outputBufferWritten = hashBytesWriten; + ZeroMemory(inputBytes); + outputBufferWritten = hashBytesWritten; return; default: @@ -378,7 +401,7 @@ internal static void CreatePasswordHash(ReadOnlySpan inputKey, ReadOnlySpa Span eInputBytes = enhancedHashKeyGen(inputKey, hashType, bcryptMinorRevision); if (!HashBytes(eInputBytes, salt.Slice(startingOffset + 3, 22), bcryptMinorRevision, workFactor, outputBuffer, out int written)) throw new BcryptAuthenticationException("Couldn't hash input"); - + ZeroMemory(eInputBytes); outputBufferWritten = written; return; @@ -442,6 +465,7 @@ internal static bool HashBytes( pos += hashEncoded.Length; charsWritten = pos; + return true; } diff --git a/src/BCrypt.Net/SafeStringExtension.Fwk.cs b/src/BCrypt.Net/SafeStringExtension.Fwk.cs new file mode 100644 index 0000000..b9c0804 --- /dev/null +++ b/src/BCrypt.Net/SafeStringExtension.Fwk.cs @@ -0,0 +1,104 @@ +// /* +// The MIT License (MIT) +// Copyright (c) 2006 Damien Miller djm@mindrot.org (jBCrypt) +// Copyright (c) 2013 Ryan D. Emerle (.Net port) +// Copyright (c) 2016/2025 Chris McKee (.Net-core port / patches / new features) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +// (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, +// merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished +// to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// */ + +#if !NETCOREAPP +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security; + +namespace BCryptNet; + +public sealed class BCryptSafeString : BCryptCore +{ + /// + /// Hash a password using the OpenBSD BCrypt scheme with a manually supplied salt/>. + /// + /// + /// You should generally leave generating salts to the library. + /// + /// The password to hash in a SafeString type. + /// The log2 of the number of rounds of hashing to apply - the work + /// factor therefore increases as 2^workFactor. Default is 11 + /// The hashed password. + /// Thrown when the salt could not be parsed. + public static string HashPassword(SecureString safeString, string salt) + { + return GetBCryptHashFromSecureString(safeString, key => HashPassword(key, salt)); + } + + public static string HashPassword(SecureString inputKey, int workFactor = DefaultRounds) + { + return GetBCryptHashFromSecureString(inputKey, key => HashPassword(key, workFactor)); + } + + private static string HashPassword(string inputKey, int workFactor = DefaultRounds) => + HashPassword(inputKey, GenerateSalt(workFactor)); + + private static string HashPassword(string inputKey, string salt) + { + return CreatePasswordHash(inputKey, salt); + } + + private delegate string SandboxedSecureString(string inputKey); + + private static unsafe string GetBCryptHashFromSecureString(SecureString secureString, SandboxedSecureString func) + { + if (secureString == null) + throw new ArgumentNullException(nameof(secureString)); + if (func == null) + throw new ArgumentNullException(nameof(func)); + + int length = secureString.Length; + if (length == 0) + throw new ArgumentException("SecureString cannot be empty", nameof(secureString)); + + if(!secureString.IsReadOnly()) + secureString.MakeReadOnly(); + + IntPtr sourceStringPointer = IntPtr.Zero; + + try + { + // Create an unmanaged copy of the secure string. + sourceStringPointer = Marshal.SecureStringToBSTR(secureString); + + if (sourceStringPointer == IntPtr.Zero) + throw new InvalidOperationException("Failed to convert SecureString to BSTR"); + + // Convert BSTR pointer to a managed string + // Note: We need to use Marshal.PtrToStringUni for wide character strings + string inputKey = Marshal.PtrToStringUni(sourceStringPointer); + + if (inputKey == null) + throw new InvalidOperationException("Failed to convert BSTR to string"); + + return func(inputKey); + } + finally + { + // Zero and free the unmanaged string - this is the correct complement to SecureStringToBSTR https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-security-securestring#securestring-and-interop + if (sourceStringPointer != IntPtr.Zero) + { + Marshal.ZeroFreeBSTR(sourceStringPointer); + } + } + } +} + +#endif diff --git a/src/BCrypt.Net/SafeStringExtension.cs b/src/BCrypt.Net/SafeStringExtension.cs new file mode 100644 index 0000000..92b2c50 --- /dev/null +++ b/src/BCrypt.Net/SafeStringExtension.cs @@ -0,0 +1,94 @@ +// /* +// The MIT License (MIT) +// Copyright (c) 2006 Damien Miller djm@mindrot.org (jBCrypt) +// Copyright (c) 2013 Ryan D. Emerle (.Net port) +// Copyright (c) 2016/2025 Chris McKee (.Net-core port / patches / new features) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +// (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, +// merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished +// to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// */ + +#if NET5_0_OR_GREATER +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security; + +namespace BCryptNet; + +public sealed class BCryptSafeString : BCryptCore +{ + public static string HashPassword(SecureString inputKey, string salt) + { + return GetBCryptHashFromSecureString(inputKey, key => HashPassword(key, salt)); + } + + public static string HashPassword(SecureString inputKey, int workFactor = DefaultRounds) + { + return GetBCryptHashFromSecureString(inputKey, key => HashPassword(key, workFactor)); + } + + private static string HashPassword(ReadOnlySpan inputKey, int workFactor = DefaultRounds) => + HashPassword(inputKey, GenerateSalt(workFactor)); + + private static string HashPassword(ReadOnlySpan inputKey, ReadOnlySpan salt) + { + Span outputBuffer = stackalloc char[60]; + HashPassword(inputKey, salt, outputBuffer, out var outputBufferWritten); + return new string(outputBuffer[..outputBufferWritten]); + } + + private static void HashPassword(ReadOnlySpan inputKey, ReadOnlySpan salt, Span outputBuffer, out int outputBufferWritten) => CreatePasswordHash(inputKey, salt, outputBuffer, out outputBufferWritten); + + private delegate string BCryptDelegate(ReadOnlySpan inputKey); + + private static unsafe string GetBCryptHashFromSecureString(SecureString secureString, BCryptDelegate func) + { + if (secureString == null) + throw new ArgumentNullException(nameof(secureString)); + if (func == null) + throw new ArgumentNullException(nameof(func)); + + int length = secureString.Length; + if (length == 0) + throw new ArgumentException("SecureString cannot be empty", nameof(secureString)); + + if(!secureString.IsReadOnly()) + secureString.MakeReadOnly(); + + IntPtr sourceStringPointer = IntPtr.Zero; + + try + { + // Create an unmanaged copy of the secure string. + sourceStringPointer = Marshal.SecureStringToBSTR(secureString); + + if (sourceStringPointer == IntPtr.Zero) + throw new InvalidOperationException("Failed to convert SecureString to BSTR"); + + // Convert the BSTR pointer directly to ReadOnlySpan + // Note: This assumes the BSTR is null-terminated and we're working with the actual content + ReadOnlySpan inputSpan = new ReadOnlySpan(sourceStringPointer.ToPointer(), length); + + return func(inputSpan); + } + finally + { + // Zero and free the unmanaged string - this is the correct complement to SecureStringToBSTR + if (sourceStringPointer != IntPtr.Zero) + { + Marshal.ZeroFreeBSTR(sourceStringPointer); + } + } + } + +} +#endif diff --git a/tests/UnitTests/BCrypt.Net.UnitTests.csproj b/tests/UnitTests/BCrypt.Net.UnitTests.csproj index efa64c4..5b45bad 100644 --- a/tests/UnitTests/BCrypt.Net.UnitTests.csproj +++ b/tests/UnitTests/BCrypt.Net.UnitTests.csproj @@ -3,7 +3,7 @@ BCrypt.Net.UnitTests BCryptNet.UnitTests net9.0;net48 - Debug + Debug;Release default true diff --git a/tests/UnitTests/BCryptTests.cs b/tests/UnitTests/BCryptTests.cs index a58356c..5a4ac6f 100644 --- a/tests/UnitTests/BCryptTests.cs +++ b/tests/UnitTests/BCryptTests.cs @@ -20,6 +20,7 @@ IN THE SOFTWARE. using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Security; using System.Security.Cryptography; using System.Text; using Xunit; @@ -134,6 +135,78 @@ public void GithubIssue119_WoltLabForumPHPBcrypt() Assert.True(BCrypt.Verify(pass, hash)); } + + + private static SecureString AsSecureString(string text) + { + var result = new SecureString(); + foreach (var c in text) result.AppendChar(c); + result.MakeReadOnly(); + return result; + } + + [Fact()] + public void TestSecureHashPassword() + { + Trace.Write("BCryptSafeString.HashPassword()[Secure]: "); + var sw = Stopwatch.StartNew(); + + for (int i = 0; i < _testVectors.Length / 3; i++) + { + var pass = _testVectors[i, 0]; + var secureString = AsSecureString(pass); + if(string.IsNullOrEmpty(pass)) continue; + var hash = BCryptSafeString.HashPassword(secureString); + var doesValidate = BCrypt.Verify(pass, hash); + Assert.True(doesValidate); + Trace.Write("."); + } + + + Trace.WriteLine(sw.ElapsedMilliseconds); + Trace.WriteLine(""); + } + + [Fact()] + public void TestSecureStringHashPassword() + { + Trace.Write("BCryptSafeString.HashPassword(): "); + var sw = Stopwatch.StartNew(); + for (var r = 0; r < _revisions.Length; r++) + { + for (int i = 0; i < _testVectors.Length / 3; i++) + { + string plain = _testVectors[i, 0]; + if(string.IsNullOrEmpty(plain)) continue; + string salt; + if (r > 0) + { + //Check hash that goes in one end comes out the next the same + salt = _testVectors[i, 1].Replace("2a", "2" + _revisions[r]); + + string hashed = BCryptSafeString.HashPassword(AsSecureString(plain), salt); + + Assert.StartsWith("$2" + _revisions[r], hashed); + Trace.WriteLine(hashed); + } + else + { + salt = _testVectors[i, 1]; + var expected = _testVectors[i, 2]; + + string hashed = BCryptSafeString.HashPassword(AsSecureString(plain), salt); + Assert.Equal(hashed, expected); + } + + + Trace.Write("."); + } + } + + Trace.WriteLine(sw.ElapsedMilliseconds); + Trace.WriteLine(""); + } + [Fact] // If you're using WoldLabForum just use BCrypt and an appropriate level of cost; // DoublebCrypt implementation in the codebase simply hashes with the same salt which is pointless. From edbb46f4f8430b153ada6723133be8b5d76a293f Mon Sep 17 00:00:00 2001 From: Chris McKee Date: Tue, 11 Nov 2025 18:32:32 +0000 Subject: [PATCH 12/27] .net 10 --- .github/workflows/ci-build.yml | 8 +- .../workflows/ci-manual-build-test-sign.yml | 121 - .github/workflows/codeql-analysis.yml | 10 +- .github/workflows/dependency-review.yml | 6 +- .github/workflows/devskim.yml | 2 +- .github/workflows/generate-publish-docs.yml | 8 +- .github/workflows/markdown-link-check.yml | 37 - .github/workflows/upload-coverage-report.yml | 4 +- BCrypt.Net.sln | 60 +- Directory.Packages.props | 6 +- benchmark/4.0.3/BCrypt403.cs | 16 +- benchmark/BCryptNet.BenchMarks.csproj | 10 +- benchmark/DecodeB64/Decoder.cs | 4 +- benchmark/Program.cs | 24 +- benchmark/TestB64Decoder.cs | 4 +- .../TestBcryptHashingEnhancedValidation.cs | 5 +- benchmark/TestBcrypt_Hashing_Enhanced.cs | 2 + benchmark/TestEnhanced_V3_Hmac.cs | 3 +- benchmark/TestVariantsOnStringBuilding.cs | 4 + benchmark/bench.cmd | 1 + examples/AndroidApp/AndroidApp.csproj | 17 + examples/AndroidApp/AndroidManifest.xml | 5 + examples/AndroidApp/MainActivity.cs | 13 + .../AndroidApp/Resources/AboutResources.txt | 44 + .../Resources/layout/activity_main.xml | 13 + .../Resources/mipmap-anydpi-v26/appicon.xml | 4 + .../mipmap-anydpi-v26/appicon_round.xml | 4 + .../Resources/mipmap-hdpi/appicon.png | Bin 0 -> 2178 bytes .../mipmap-hdpi/appicon_background.png | Bin 0 -> 97 bytes .../mipmap-hdpi/appicon_foreground.png | Bin 0 -> 1276 bytes .../Resources/mipmap-mdpi/appicon.png | Bin 0 -> 1524 bytes .../mipmap-mdpi/appicon_background.png | Bin 0 -> 92 bytes .../mipmap-mdpi/appicon_foreground.png | Bin 0 -> 1273 bytes .../Resources/mipmap-xhdpi/appicon.png | Bin 0 -> 3098 bytes .../mipmap-xhdpi/appicon_background.png | Bin 0 -> 100 bytes .../mipmap-xhdpi/appicon_foreground.png | Bin 0 -> 1805 bytes .../Resources/mipmap-xxhdpi/appicon.png | Bin 0 -> 4674 bytes .../mipmap-xxhdpi/appicon_background.png | Bin 0 -> 108 bytes .../mipmap-xxhdpi/appicon_foreground.png | Bin 0 -> 1926 bytes .../Resources/mipmap-xxxhdpi/appicon.png | Bin 0 -> 6832 bytes .../mipmap-xxxhdpi/appicon_background.png | Bin 0 -> 117 bytes .../mipmap-xxxhdpi/appicon_foreground.png | Bin 0 -> 2801 bytes .../values/ic_launcher_background.xml | 4 + .../AndroidApp/Resources/values/strings.xml | 4 + examples/AndroidTest/AndroidTest.csproj | 71 + examples/AndroidTest/App.xaml | 14 + examples/AndroidTest/App.xaml.cs | 31 + examples/AndroidTest/AppShell.xaml | 15 + examples/AndroidTest/AppShell.xaml.cs | 29 + examples/AndroidTest/MainPage.xaml | 43 + examples/AndroidTest/MainPage.xaml.cs | 42 + examples/AndroidTest/MauiProgram.cs | 44 + .../Platforms/Android/AndroidManifest.xml | 6 + .../Platforms/Android/MainActivity.cs | 30 + .../Platforms/Android/MainApplication.cs | 35 + .../Android/Resources/values/colors.xml | 6 + .../Platforms/MacCatalyst/AppDelegate.cs | 29 + .../Platforms/MacCatalyst/Entitlements.plist | 14 + .../Platforms/MacCatalyst/Info.plist | 38 + .../Platforms/MacCatalyst/Program.cs | 35 + examples/AndroidTest/Platforms/Tizen/Main.cs | 36 + .../Platforms/Tizen/tizen-manifest.xml | 15 + .../AndroidTest/Platforms/Windows/App.xaml | 8 + .../AndroidTest/Platforms/Windows/App.xaml.cs | 44 + .../Platforms/Windows/Package.appxmanifest | 46 + .../Platforms/Windows/app.manifest | 15 + .../AndroidTest/Platforms/iOS/AppDelegate.cs | 29 + examples/AndroidTest/Platforms/iOS/Info.plist | 32 + examples/AndroidTest/Platforms/iOS/Program.cs | 35 + .../iOS/Resources/PrivacyInfo.xcprivacy | 51 + .../Properties/launchSettings.json | 8 + .../AndroidTest/Resources/AppIcon/appicon.svg | 36 + .../Resources/AppIcon/appiconfg.svg | 36 + .../Resources/Fonts/OpenSans-Regular.ttf | Bin 0 -> 107276 bytes .../Resources/Fonts/OpenSans-Semibold.ttf | Bin 0 -> 111184 bytes .../Resources/Images/dotnet_bot.png | Bin 0 -> 69811 bytes .../AndroidTest/Resources/Raw/AboutAssets.txt | 15 + .../AndroidTest/Resources/Splash/splash.svg | 36 + .../AndroidTest/Resources/Styles/Colors.xaml | 45 + .../AndroidTest/Resources/Styles/Styles.xaml | 434 + .../Api/WebApi/Views/Shared/_Layout.cshtml | 4 +- .../Shared/_ValidationScriptsPartial.cshtml | 4 +- examples/Api/WebApi/WebApi.csproj | 35 +- examples/Api/WebApi/app.db-shm | Bin 32768 -> 32768 bytes examples/Api/WebApi/app.db-wal | Bin 32992 -> 49472 bytes .../lib/bootstrap/dist/js/bootstrap.bundle.js | 6780 ---------- .../bootstrap/dist/js/bootstrap.bundle.js.map | 1 - .../bootstrap/dist/js/bootstrap.bundle.min.js | 7 - .../dist/js/bootstrap.bundle.min.js.map | 1 - .../lib/bootstrap/dist/js/bootstrap.esm.js | 4977 ------- .../bootstrap/dist/js/bootstrap.esm.js.map | 1 - .../bootstrap/dist/js/bootstrap.esm.min.js | 7 - .../dist/js/bootstrap.esm.min.js.map | 1 - .../lib/bootstrap/dist/js/bootstrap.js | 5026 ------- .../lib/bootstrap/dist/js/bootstrap.js.map | 1 - .../lib/bootstrap/dist/js/bootstrap.min.js | 7 - .../bootstrap/dist/js/bootstrap.min.js.map | 1 - .../jquery-validation-unobtrusive/LICENSE.txt | 23 - .../jquery.validate.unobtrusive.js | 435 - .../jquery.validate.unobtrusive.min.js | 8 - .../wwwroot/lib/jquery-validation/LICENSE.md | 22 - .../dist/additional-methods.js | 1512 --- .../dist/additional-methods.min.js | 4 - .../jquery-validation/dist/jquery.validate.js | 1661 --- .../dist/jquery.validate.min.js | 4 - .../Api/WebApi/wwwroot/lib/jquery/LICENSE.txt | 21 - .../WebApi/wwwroot/lib/jquery/dist/jquery.js | 10881 ---------------- .../wwwroot/lib/jquery/dist/jquery.min.js | 2 - .../wwwroot/lib/jquery/dist/jquery.min.map | 1 - .../ExampleConsoleApp.csproj | 4 +- global.json | 2 +- .../BCrypt.Net.IdentityExtensions.csproj | 5 +- src/BCrypt.Net/BCrypt.Net.csproj | 7 +- src/BCrypt.Net/BCrypt.cs | 6 +- src/BCrypt.Net/BCryptBase.Fwk.cs | 11 +- src/BCrypt.Net/BCryptBase.cs | 13 +- src/BCrypt.Net/BCryptExtendedV2.cs | 2 +- src/BCrypt.Net/BCryptExtendedV3.cs | 2 +- src/Directory.Build.props | 4 - tests/UnitTests/BCrypt.Net.UnitTests.csproj | 4 +- tests/UnitTests/BCryptTests.cs | 6 +- tests/UnitTests/BCryptTestsExtendedv3.cs | 4 +- tests/UnitTests/Base64Tests.cs | 2 +- 123 files changed, 1682 insertions(+), 31663 deletions(-) delete mode 100644 .github/workflows/ci-manual-build-test-sign.yml delete mode 100644 .github/workflows/markdown-link-check.yml create mode 100644 benchmark/bench.cmd create mode 100644 examples/AndroidApp/AndroidApp.csproj create mode 100644 examples/AndroidApp/AndroidManifest.xml create mode 100644 examples/AndroidApp/MainActivity.cs create mode 100644 examples/AndroidApp/Resources/AboutResources.txt create mode 100644 examples/AndroidApp/Resources/layout/activity_main.xml create mode 100644 examples/AndroidApp/Resources/mipmap-anydpi-v26/appicon.xml create mode 100644 examples/AndroidApp/Resources/mipmap-anydpi-v26/appicon_round.xml create mode 100644 examples/AndroidApp/Resources/mipmap-hdpi/appicon.png create mode 100644 examples/AndroidApp/Resources/mipmap-hdpi/appicon_background.png create mode 100644 examples/AndroidApp/Resources/mipmap-hdpi/appicon_foreground.png create mode 100644 examples/AndroidApp/Resources/mipmap-mdpi/appicon.png create mode 100644 examples/AndroidApp/Resources/mipmap-mdpi/appicon_background.png create mode 100644 examples/AndroidApp/Resources/mipmap-mdpi/appicon_foreground.png create mode 100644 examples/AndroidApp/Resources/mipmap-xhdpi/appicon.png create mode 100644 examples/AndroidApp/Resources/mipmap-xhdpi/appicon_background.png create mode 100644 examples/AndroidApp/Resources/mipmap-xhdpi/appicon_foreground.png create mode 100644 examples/AndroidApp/Resources/mipmap-xxhdpi/appicon.png create mode 100644 examples/AndroidApp/Resources/mipmap-xxhdpi/appicon_background.png create mode 100644 examples/AndroidApp/Resources/mipmap-xxhdpi/appicon_foreground.png create mode 100644 examples/AndroidApp/Resources/mipmap-xxxhdpi/appicon.png create mode 100644 examples/AndroidApp/Resources/mipmap-xxxhdpi/appicon_background.png create mode 100644 examples/AndroidApp/Resources/mipmap-xxxhdpi/appicon_foreground.png create mode 100644 examples/AndroidApp/Resources/values/ic_launcher_background.xml create mode 100644 examples/AndroidApp/Resources/values/strings.xml create mode 100644 examples/AndroidTest/AndroidTest.csproj create mode 100644 examples/AndroidTest/App.xaml create mode 100644 examples/AndroidTest/App.xaml.cs create mode 100644 examples/AndroidTest/AppShell.xaml create mode 100644 examples/AndroidTest/AppShell.xaml.cs create mode 100644 examples/AndroidTest/MainPage.xaml create mode 100644 examples/AndroidTest/MainPage.xaml.cs create mode 100644 examples/AndroidTest/MauiProgram.cs create mode 100644 examples/AndroidTest/Platforms/Android/AndroidManifest.xml create mode 100644 examples/AndroidTest/Platforms/Android/MainActivity.cs create mode 100644 examples/AndroidTest/Platforms/Android/MainApplication.cs create mode 100644 examples/AndroidTest/Platforms/Android/Resources/values/colors.xml create mode 100644 examples/AndroidTest/Platforms/MacCatalyst/AppDelegate.cs create mode 100644 examples/AndroidTest/Platforms/MacCatalyst/Entitlements.plist create mode 100644 examples/AndroidTest/Platforms/MacCatalyst/Info.plist create mode 100644 examples/AndroidTest/Platforms/MacCatalyst/Program.cs create mode 100644 examples/AndroidTest/Platforms/Tizen/Main.cs create mode 100644 examples/AndroidTest/Platforms/Tizen/tizen-manifest.xml create mode 100644 examples/AndroidTest/Platforms/Windows/App.xaml create mode 100644 examples/AndroidTest/Platforms/Windows/App.xaml.cs create mode 100644 examples/AndroidTest/Platforms/Windows/Package.appxmanifest create mode 100644 examples/AndroidTest/Platforms/Windows/app.manifest create mode 100644 examples/AndroidTest/Platforms/iOS/AppDelegate.cs create mode 100644 examples/AndroidTest/Platforms/iOS/Info.plist create mode 100644 examples/AndroidTest/Platforms/iOS/Program.cs create mode 100644 examples/AndroidTest/Platforms/iOS/Resources/PrivacyInfo.xcprivacy create mode 100644 examples/AndroidTest/Properties/launchSettings.json create mode 100644 examples/AndroidTest/Resources/AppIcon/appicon.svg create mode 100644 examples/AndroidTest/Resources/AppIcon/appiconfg.svg create mode 100644 examples/AndroidTest/Resources/Fonts/OpenSans-Regular.ttf create mode 100644 examples/AndroidTest/Resources/Fonts/OpenSans-Semibold.ttf create mode 100644 examples/AndroidTest/Resources/Images/dotnet_bot.png create mode 100644 examples/AndroidTest/Resources/Raw/AboutAssets.txt create mode 100644 examples/AndroidTest/Resources/Splash/splash.svg create mode 100644 examples/AndroidTest/Resources/Styles/Colors.xaml create mode 100644 examples/AndroidTest/Resources/Styles/Styles.xaml delete mode 100644 examples/Api/WebApi/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js delete mode 100644 examples/Api/WebApi/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map delete mode 100644 examples/Api/WebApi/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js delete mode 100644 examples/Api/WebApi/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map delete mode 100644 examples/Api/WebApi/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js delete mode 100644 examples/Api/WebApi/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map delete mode 100644 examples/Api/WebApi/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js delete mode 100644 examples/Api/WebApi/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map delete mode 100644 examples/Api/WebApi/wwwroot/lib/bootstrap/dist/js/bootstrap.js delete mode 100644 examples/Api/WebApi/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map delete mode 100644 examples/Api/WebApi/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js delete mode 100644 examples/Api/WebApi/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map delete mode 100644 examples/Api/WebApi/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt delete mode 100644 examples/Api/WebApi/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js delete mode 100644 examples/Api/WebApi/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js delete mode 100644 examples/Api/WebApi/wwwroot/lib/jquery-validation/LICENSE.md delete mode 100644 examples/Api/WebApi/wwwroot/lib/jquery-validation/dist/additional-methods.js delete mode 100644 examples/Api/WebApi/wwwroot/lib/jquery-validation/dist/additional-methods.min.js delete mode 100644 examples/Api/WebApi/wwwroot/lib/jquery-validation/dist/jquery.validate.js delete mode 100644 examples/Api/WebApi/wwwroot/lib/jquery-validation/dist/jquery.validate.min.js delete mode 100644 examples/Api/WebApi/wwwroot/lib/jquery/LICENSE.txt delete mode 100644 examples/Api/WebApi/wwwroot/lib/jquery/dist/jquery.js delete mode 100644 examples/Api/WebApi/wwwroot/lib/jquery/dist/jquery.min.js delete mode 100644 examples/Api/WebApi/wwwroot/lib/jquery/dist/jquery.min.map diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 7df381f..939d23c 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -29,17 +29,17 @@ jobs: runs-on: ubuntu-24.04 steps: - name: 'Harden Runner' - uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit - name: 'Checkout' - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 # avoid shallow clone so nbgv can do its work. - name: 'Setup .NET SDK' - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 + uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 with: dotnet-version: 9.0.x @@ -73,7 +73,7 @@ jobs: if: always() - name: 'Generate Coverage Reports' - uses: danielpalme/ReportGenerator-GitHub-Action@cc137d2b561c02b63ae869ffbe8f68af9d904bf4 # 5.4.6 + uses: danielpalme/ReportGenerator-GitHub-Action@c4c5175a441c6603ec614f5084386dabe0e2295b # v5.4.12 with: reports: "tests/**/coverage.cobertura.xml" targetdir: "${{ github.workspace }}" diff --git a/.github/workflows/ci-manual-build-test-sign.yml b/.github/workflows/ci-manual-build-test-sign.yml deleted file mode 100644 index 6557bbc..0000000 --- a/.github/workflows/ci-manual-build-test-sign.yml +++ /dev/null @@ -1,121 +0,0 @@ -name: Manual Build, Test, Sign, Publish -on: - workflow_dispatch: - inputs: - public_release: - description: 'Public Release' - type: boolean - required: true - default: true - perform_sign: - description: 'Sign' - type: boolean - required: true - default: true - perform_publish: - description: 'nuget publish' - type: boolean - required: true - default: false - -env: - DOTNET_NOLOGO: true - DOTNET_GENERATE_ASPNET_CERTIFICATE: false - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true - DOTNET_CLI_TELEMETRY_OPTOUT: true - nupkgDirectory: ${{ github.workspace}}/dists - -jobs: - build: - permissions: - contents: read - - name: Build release - runs-on: ubuntu-latest - steps: - - name: Harden Runner - uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 - with: - egress-policy: audit - - - name: 'Checkout repository' - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 0 # avoid shallow clone so nbgv can do its work. - - - name: 'Setup .NET SDK' - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 - with: - dotnet-version: 9.0.x - - - name: 'Build' - run: dotnet build --configuration Release --property:PublicRelease=${{ inputs.public_release }} - - - name: 'Test' - run: dotnet test --configuration Release --no-restore --no-build --property:PublicRelease=${{ inputs.public_release }} tests/UnitTests/BCrypt.Net.UnitTests.csproj - - - name: 'Pack release' - run: dotnet pack --configuration Release --no-restore --no-build --output ${{ env.nupkgDirectory }} --property:PublicRelease=${{ inputs.public_release }} - - - name: 'List artifact directory' - shell: pwsh - run: > - Get-ChildItem -Path ${{ env.nupkgDirectory }} -Recurse -Force - - - name: 'Extract SBOMs' - shell: pwsh - run: > - Get-ChildItem -Path ${{ env.nupkgDirectory }} -Filter *.nupkg -Force | ForEach-Object { - Expand-Archive $_.FullName "$($_.DirectoryName)/$($_.Basename)" -Force - Copy-Item "$($_.DirectoryName)/$($_.Basename)/_manifest/spdx_2.2/manifest.spdx.json" -Destination "${{ env.nupkgDirectory }}/$($_.Basename).spdx.json" - Copy-Item "$($_.DirectoryName)/$($_.Basename)/_manifest/spdx_2.2/manifest.spdx.json.sha256" -Destination "${{ env.nupkgDirectory }}/$($_.Basename).spdx.json.sha256" - Remove-Item "$($_.DirectoryName)/$($_.Basename)" -Force -Recurse } - - - name: 'List artifact directory' - shell: pwsh - run: > - Get-ChildItem -Path ${{ env.nupkgDirectory }} -Recurse -Force - - - name: Upload unsigned nupkgs - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: build-artifacts - path: ${{ env.nupkgDirectory }}/* - retention-days: 7 - - # publish: - # name: Publish to nuget - # needs: sign - # runs-on: ubuntu-latest - # if: ${{ inputs.perform_publish }} - # environment: release - # permissions: - # id-token: write - # steps: - # - name: 'Harden Runner' - # uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 - # with: - # egress-policy: audit - - # - name: 'Setup .NET SDK' - # uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 - - # - name: 'Gather nupkgs from signing output' - # uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - # with: - # name: signed-artifacts - # path : ${{ env.nupkgDirectory }} - - # - name: List assets to be published - # shell: pwsh - # run: > - # Get-ChildItem -Path ${{ env.nupkgDirectory }} -Filter *.nupkg -Recurse -Force - - # # Use --skip-duplicate to prevent errors if a package with the same version already exists. - # # This allows a retry of a failed workflow, already published packages will be skipped without error. - # - name: Publish NuGet package - # shell: pwsh - # run: > - # foreach($file in (Get-ChildItem "${{ env.nupkgDirectory }}" -Recurse -Filter *.nupkg)) { - # dotnet nuget push $file --api-key "${{ secrets.NUGET_APIKEY }}" --source https://api.nuget.org/v3/index.json --skip-duplicate - # } diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 98a4b83..2d846cb 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -49,23 +49,23 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit - name: 'Checkout repository' - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 # avoid shallow clone so nbgv can do its work. - name: 'Setup .NET SDK' - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 + uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 with: dotnet-version: 9.0.x # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@d68b2d4edb4189fd2a5366ac14e72027bd4b37dd # v3.28.2 + uses: github/codeql-action/init@d3678e237b9c32a6c9bffb3315c335f976f3549f # v3.30.2 with: languages: ${{ matrix.language }} @@ -75,4 +75,4 @@ jobs: - run: dotnet build --configuration CodeQL /p:UseSharedCompilation=false /t:rebuild - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@d68b2d4edb4189fd2a5366ac14e72027bd4b37dd # v3.28.2 + uses: github/codeql-action/analyze@d3678e237b9c32a6c9bffb3315c335f976f3549f ## v3.30.2 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index f3ab11f..e99cf71 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -14,12 +14,12 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Harden Runner - uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit - name: 'Checkout Repository' - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: 'Dependency Review' - uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0 + uses: actions/dependency-review-action@595b5aeba73380359d98a5e087f648dbb0edce1b # v4.7.3 diff --git a/.github/workflows/devskim.yml b/.github/workflows/devskim.yml index 3e31856..e4f4ed1 100644 --- a/.github/workflows/devskim.yml +++ b/.github/workflows/devskim.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Harden Runner - uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit - uses: actions/checkout@v4 diff --git a/.github/workflows/generate-publish-docs.yml b/.github/workflows/generate-publish-docs.yml index 971a58e..353bc23 100644 --- a/.github/workflows/generate-publish-docs.yml +++ b/.github/workflows/generate-publish-docs.yml @@ -30,17 +30,17 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit - name: 'Checkout' - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 0 # avoid shallow clone so nbgv can do its work. - name: 'Setup .NET SDK' - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 + uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 with: dotnet-version: 9.0.x @@ -48,7 +48,7 @@ jobs: - run: docfx ./docs/docfx.json - name: Upload Pages Artifact - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1 + uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0 with: # Upload entire repository path: './docs/_site' diff --git a/.github/workflows/markdown-link-check.yml b/.github/workflows/markdown-link-check.yml deleted file mode 100644 index 0eec6ce..0000000 --- a/.github/workflows/markdown-link-check.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Markdown Links Check - -on: - workflow_dispatch: - push: - branches: - - 'main' - paths: - - '**.md' - pull_request: - branches: - - main - paths: - - '**.md' - #schedule: - # Run every-day at 9:00 AM (See https://pubs.opengroup.org/onlinepubs/9699919799/utilities/crontab.html#tag_20_25_07) - #- cron: "0 9 * * *" -permissions: - contents: read - -jobs: - markdown-link-check: - runs-on: ubuntu-24.04 - steps: - - name: Harden Runner - uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 - with: - egress-policy: audit - - - name: 'Checkout Repository' - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - - name: 'Check for dead links in markdown files' - uses: gaurav-nelson/github-action-markdown-link-check@1b916f2cf6c36510a6059943104e3c42ce6c16bc # 1.0.16 - with: - use-quiet-mode: 'yes' - use-verbose-mode: 'no' diff --git a/.github/workflows/upload-coverage-report.yml b/.github/workflows/upload-coverage-report.yml index c887166..843d98b 100644 --- a/.github/workflows/upload-coverage-report.yml +++ b/.github/workflows/upload-coverage-report.yml @@ -18,7 +18,7 @@ jobs: github.event.workflow_run.conclusion == 'success' steps: - name: Harden Runner - uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit @@ -28,7 +28,7 @@ jobs: name: coverage-results - name: Add Code Coverage PR Comment - uses: marocchino/sticky-pull-request-comment@67d0dec7b07ed060a405f9b2a64b8ab319fdd7db # v2.9.2 + uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4 if: github.event_name == 'pull_request' with: recreate: true diff --git a/BCrypt.Net.sln b/BCrypt.Net.sln index 68a6653..b1376c3 100644 --- a/BCrypt.Net.sln +++ b/BCrypt.Net.sln @@ -1,20 +1,21 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29709.97 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36414.22 MinimumVisualStudioVersion = 15.0.26228.4 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BCrypt.Net", "src\BCrypt.Net\BCrypt.Net.csproj", "{CD69F016-5940-4FCA-BCA1-9D1D87C6F873}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests", "tests\UnitTests\BCrypt.Net.UnitTests.csproj", "{2078EB7B-7EDF-4B65-80F7-DB4D92E08CCD}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BCrypt.Net.UnitTests", "tests\UnitTests\BCrypt.Net.UnitTests.csproj", "{2078EB7B-7EDF-4B65-80F7-DB4D92E08CCD}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build-tools", "build-tools", "{6F3D5F8B-CD73-474C-BA79-D4A17E9F106D}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig - Dockerfile = Dockerfile Directory.Packages.props = Directory.Packages.props + Dockerfile = Dockerfile + Directory.Build.props = Directory.Build.props EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmark", "benchmark\BCryptNet.BenchMarks.csproj", "{E75AA3B8-BF28-4366-B5C6-14AF342290C3}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BCryptNet.BenchMarks", "benchmark\BCryptNet.BenchMarks.csproj", "{E75AA3B8-BF28-4366-B5C6-14AF342290C3}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{DBAB6D93-7DE4-4A70-89E7-1EE784C1A466}" EndProject @@ -26,65 +27,74 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BCrypt.Net.IdentityExtensio EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "github", "github", "{93729F4E-5F15-4769-8385-19176C7AAA2C}" ProjectSection(SolutionItems) = preProject - .github\dependabot.yml = .github\dependabot.yml .github\ISSUE_TEMPLATE\bug_report.md = .github\ISSUE_TEMPLATE\bug_report.md - .github\ISSUE_TEMPLATE\docs-issue.md = .github\ISSUE_TEMPLATE\docs-issue.md - .github\ISSUE_TEMPLATE\feature_request.md = .github\ISSUE_TEMPLATE\feature_request.md - .github\stale.yml = .github\stale.yml .github\workflows\ci-build.yml = .github\workflows\ci-build.yml - .github\workflows\ci-manual-build-test-sign.yml = .github\workflows\ci-manual-build-test-sign.yml .github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml + .github\dependabot.yml = .github\dependabot.yml .github\workflows\dependency-review.yml = .github\workflows\dependency-review.yml + .github\ISSUE_TEMPLATE\docs-issue.md = .github\ISSUE_TEMPLATE\docs-issue.md + .github\ISSUE_TEMPLATE\feature_request.md = .github\ISSUE_TEMPLATE\feature_request.md .github\workflows\generate-publish-docs.yml = .github\workflows\generate-publish-docs.yml - .github\workflows\markdown-link-check.yml = .github\workflows\markdown-link-check.yml + .github\stale.yml = .github\stale.yml .github\workflows\upload-coverage-report.yml = .github\workflows\upload-coverage-report.yml EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AndroidTest", "examples\AndroidTest\AndroidTest.csproj", "{AEA6712D-0FAE-4031-8FA7-FDD2A026855A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU - CodeQL|Any CPU = CodeQL|Any CPU + All|Any CPU = All|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {CD69F016-5940-4FCA-BCA1-9D1D87C6F873}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CD69F016-5940-4FCA-BCA1-9D1D87C6F873}.Debug|Any CPU.Build.0 = Debug|Any CPU {CD69F016-5940-4FCA-BCA1-9D1D87C6F873}.Release|Any CPU.ActiveCfg = Release|Any CPU {CD69F016-5940-4FCA-BCA1-9D1D87C6F873}.Release|Any CPU.Build.0 = Release|Any CPU - {CD69F016-5940-4FCA-BCA1-9D1D87C6F873}.CodeQL|Any CPU.ActiveCfg = CodeQL|Any CPU - {CD69F016-5940-4FCA-BCA1-9D1D87C6F873}.CodeQL|Any CPU.Build.0 = CodeQL|Any CPU + {CD69F016-5940-4FCA-BCA1-9D1D87C6F873}.All|Any CPU.ActiveCfg = All|Any CPU + {CD69F016-5940-4FCA-BCA1-9D1D87C6F873}.All|Any CPU.Build.0 = All|Any CPU {2078EB7B-7EDF-4B65-80F7-DB4D92E08CCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2078EB7B-7EDF-4B65-80F7-DB4D92E08CCD}.Debug|Any CPU.Build.0 = Debug|Any CPU {2078EB7B-7EDF-4B65-80F7-DB4D92E08CCD}.Release|Any CPU.ActiveCfg = Release|Any CPU {2078EB7B-7EDF-4B65-80F7-DB4D92E08CCD}.Release|Any CPU.Build.0 = Release|Any CPU - {E75AA3B8-BF28-4366-B5C6-14AF342290C3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E75AA3B8-BF28-4366-B5C6-14AF342290C3}.Release|Any CPU.Build.0 = Release|Any CPU + {2078EB7B-7EDF-4B65-80F7-DB4D92E08CCD}.All|Any CPU.ActiveCfg = All|Any CPU + {2078EB7B-7EDF-4B65-80F7-DB4D92E08CCD}.All|Any CPU.Build.0 = All|Any CPU {E75AA3B8-BF28-4366-B5C6-14AF342290C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E75AA3B8-BF28-4366-B5C6-14AF342290C3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E75AA3B8-BF28-4366-B5C6-14AF342290C3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E75AA3B8-BF28-4366-B5C6-14AF342290C3}.Release|Any CPU.Build.0 = Release|Any CPU + {E75AA3B8-BF28-4366-B5C6-14AF342290C3}.All|Any CPU.ActiveCfg = All|Any CPU + {E75AA3B8-BF28-4366-B5C6-14AF342290C3}.All|Any CPU.Build.0 = All|Any CPU {A24DEBF7-6ED9-4774-B67F-7A0EE18125C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A24DEBF7-6ED9-4774-B67F-7A0EE18125C5}.Debug|Any CPU.Build.0 = Debug|Any CPU {A24DEBF7-6ED9-4774-B67F-7A0EE18125C5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A24DEBF7-6ED9-4774-B67F-7A0EE18125C5}.Release|Any CPU.Build.0 = Release|Any CPU + {A24DEBF7-6ED9-4774-B67F-7A0EE18125C5}.All|Any CPU.ActiveCfg = All|Any CPU + {A24DEBF7-6ED9-4774-B67F-7A0EE18125C5}.All|Any CPU.Build.0 = All|Any CPU {CE412A71-A08D-4263-8960-664827DA526B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CE412A71-A08D-4263-8960-664827DA526B}.Debug|Any CPU.Build.0 = Debug|Any CPU {CE412A71-A08D-4263-8960-664827DA526B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CE412A71-A08D-4263-8960-664827DA526B}.Release|Any CPU.Build.0 = Release|Any CPU + {CE412A71-A08D-4263-8960-664827DA526B}.All|Any CPU.ActiveCfg = All|Any CPU + {CE412A71-A08D-4263-8960-664827DA526B}.All|Any CPU.Build.0 = All|Any CPU {70440057-8D4E-41B0-8DF7-8A67B4C0EE28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {70440057-8D4E-41B0-8DF7-8A67B4C0EE28}.Debug|Any CPU.Build.0 = Debug|Any CPU {70440057-8D4E-41B0-8DF7-8A67B4C0EE28}.Release|Any CPU.ActiveCfg = Release|Any CPU {70440057-8D4E-41B0-8DF7-8A67B4C0EE28}.Release|Any CPU.Build.0 = Release|Any CPU - {70440057-8D4E-41B0-8DF7-8A67B4C0EE28}.CodeQL|Any CPU.ActiveCfg = CodeQL|Any CPU - {70440057-8D4E-41B0-8DF7-8A67B4C0EE28}.CodeQL|Any CPU.Build.0 = CodeQL|Any CPU + {70440057-8D4E-41B0-8DF7-8A67B4C0EE28}.All|Any CPU.ActiveCfg = All|Any CPU + {70440057-8D4E-41B0-8DF7-8A67B4C0EE28}.All|Any CPU.Build.0 = All|Any CPU + {AEA6712D-0FAE-4031-8FA7-FDD2A026855A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AEA6712D-0FAE-4031-8FA7-FDD2A026855A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AEA6712D-0FAE-4031-8FA7-FDD2A026855A}.All|Any CPU.ActiveCfg = All|Any CPU + {AEA6712D-0FAE-4031-8FA7-FDD2A026855A}.All|Any CPU.Build.0 = All|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {D5ADD1E2-61BB-4575-B5D8-78FB549A4DBC} - EndGlobalSection GlobalSection(NestedProjects) = preSolution {A24DEBF7-6ED9-4774-B67F-7A0EE18125C5} = {DBAB6D93-7DE4-4A70-89E7-1EE784C1A466} {CE412A71-A08D-4263-8960-664827DA526B} = {DBAB6D93-7DE4-4A70-89E7-1EE784C1A466} {93729F4E-5F15-4769-8385-19176C7AAA2C} = {6F3D5F8B-CD73-474C-BA79-D4A17E9F106D} + {AEA6712D-0FAE-4031-8FA7-FDD2A026855A} = {DBAB6D93-7DE4-4A70-89E7-1EE784C1A466} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D5ADD1E2-61BB-4575-B5D8-78FB549A4DBC} EndGlobalSection EndGlobal diff --git a/Directory.Packages.props b/Directory.Packages.props index c4926c7..71bca91 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -16,12 +16,16 @@ - + + + + + diff --git a/benchmark/4.0.3/BCrypt403.cs b/benchmark/4.0.3/BCrypt403.cs index 96b8a6b..f1a356b 100644 --- a/benchmark/4.0.3/BCrypt403.cs +++ b/benchmark/4.0.3/BCrypt403.cs @@ -751,7 +751,7 @@ private static int Char64(char character) /// Blowfish encipher a single 64-bit block encoded as two 32-bit halves. /// An array containing the two 32-bit half blocks. /// The position in the array of the blocks. -#if NETCOREAPP +#if NET5_0_OR_GREATER private void Encipher(Span blockArray, int offset) #else private void Encipher(uint[] blockArray, int offset) @@ -791,7 +791,7 @@ private void Encipher(uint[] blockArray, int offset) /// The string to extract the data from. /// [in,out] The current offset. /// The next word of material from data. -#if NETCOREAPP +#if NET5_0_OR_GREATER private static uint StreamToWord(ReadOnlySpan data, ref int offset) #else private static uint StreamToWord(byte[] data, ref int offset) @@ -820,7 +820,7 @@ private void InitializeKey() /// Key the Blowfish cipher. /// The key byte array. -#if NETCOREAPP +#if NET5_0_OR_GREATER private void Key(ReadOnlySpan keyBytes) #else private void Key(byte[] keyBytes) @@ -828,7 +828,7 @@ private void Key(byte[] keyBytes) { int i; int koffp = 0; -#if NETCOREAPP +#if NET5_0_OR_GREATER Span lr = stackalloc uint[2] { 0, 0 }; #else uint[] lr = { 0, 0 }; @@ -862,7 +862,7 @@ private void Key(byte[] keyBytes) /// Salt byte array. /// Input byte array. // ReSharper disable once InconsistentNaming -#if NETCOREAPP +#if NET5_0_OR_GREATER private void EKSKey(ReadOnlySpan saltBytes, ReadOnlySpan inputBytes) #else private void EKSKey(byte[] saltBytes, byte[] inputBytes) @@ -871,7 +871,7 @@ private void EKSKey(byte[] saltBytes, byte[] inputBytes) int i; int passwordOffset = 0; int saltOffset = 0; -#if NETCOREAPP +#if NET5_0_OR_GREATER Span lr = stackalloc uint[2] { 0, 0 }; #else uint[] lr = { 0, 0 }; @@ -909,7 +909,7 @@ private void EKSKey(byte[] saltBytes, byte[] inputBytes) /// The salt byte array to hash with. /// The binary logarithm of the number of rounds of hashing to apply. /// A byte array containing the hashed result. -#if NETCOREAPP +#if NET5_0_OR_GREATER internal byte[] CryptRaw(ReadOnlySpan inputBytes, ReadOnlySpan saltBytes, int workFactor) #else internal byte[] CryptRaw(byte[] inputBytes, byte[] saltBytes, int workFactor) @@ -918,7 +918,7 @@ internal byte[] CryptRaw(byte[] inputBytes, byte[] saltBytes, int workFactor) int i; int j; -#if NETCOREAPP +#if NET5_0_OR_GREATER Span cdata = stackalloc uint[BfCryptCiphertext.Length]; BfCryptCiphertext.CopyTo(cdata); #else diff --git a/benchmark/BCryptNet.BenchMarks.csproj b/benchmark/BCryptNet.BenchMarks.csproj index d119224..b57c953 100644 --- a/benchmark/BCryptNet.BenchMarks.csproj +++ b/benchmark/BCryptNet.BenchMarks.csproj @@ -3,9 +3,9 @@ BCryptNet.BenchMarks BCryptNet.BenchMarks Exe - net9.0 + net10.0;net48 true - Release;Debug + Release;Debug;All AnyCPU false @@ -33,12 +33,12 @@ - + - - + + diff --git a/benchmark/DecodeB64/Decoder.cs b/benchmark/DecodeB64/Decoder.cs index f13c4a9..9c47e2f 100644 --- a/benchmark/DecodeB64/Decoder.cs +++ b/benchmark/DecodeB64/Decoder.cs @@ -1,4 +1,5 @@ -using System; +#if NET5_0_OR_GREATER +using System; using System.Runtime.CompilerServices; using System.Text; @@ -279,3 +280,4 @@ public static int DecodeBase64SpanBuffer(ReadOnlySpan encodedSpan, Span(new DebugInProcessConfig().AddValidator(ExecutionValidator.FailOnError)); + BenchmarkRunner.Run(new DebugInProcessConfig().AddValidator(ExecutionValidator.FailOnError)); #else var config = DefaultConfig.Instance - .With(Job.Default.With(CoreRuntime.Core90)) + .AddJob(Job.Default.WithRuntime(CoreRuntime.Core10_0)) + .AddJob(Job.Default.WithRuntime(ClrRuntime.Net481)) ; - - // BenchmarkRunner.Run(config); - // BenchmarkRunner.Run(config); - // BenchmarkRunner.Run(config); - // BenchmarkRunner.Run(config); - // BenchmarkRunner.Run(config); - // // - // // // Tests for testing in isolation - // BenchmarkRunner.Run(config); +#if NET5_0_OR_GREATER + BenchmarkRunner.Run(config); +#endif + BenchmarkRunner.Run(config); + BenchmarkRunner.Run(config); + BenchmarkRunner.Run(config); + BenchmarkRunner.Run(config); + BenchmarkRunner.Run(config); +#if NET5_0_OR_GREATER BenchmarkRunner.Run(config); BenchmarkRunner.Run(config); +#endif BenchmarkRunner.Run(config); #endif diff --git a/benchmark/TestB64Decoder.cs b/benchmark/TestB64Decoder.cs index 8b0dcba..aaf336e 100644 --- a/benchmark/TestB64Decoder.cs +++ b/benchmark/TestB64Decoder.cs @@ -1,4 +1,5 @@ -using System; +#if NET5_0_OR_GREATER +using System; using System.Collections.Generic; using BCryptNet.BenchMarks.DecodeB64; using BenchmarkDotNet.Attributes; @@ -84,3 +85,4 @@ public byte[] DecodeBase64SpanBuffer(string salt) } } } +#endif diff --git a/benchmark/TestBcryptHashingEnhancedValidation.cs b/benchmark/TestBcryptHashingEnhancedValidation.cs index 4e4cd6d..3acb38c 100644 --- a/benchmark/TestBcryptHashingEnhancedValidation.cs +++ b/benchmark/TestBcryptHashingEnhancedValidation.cs @@ -47,7 +47,9 @@ public IEnumerable Data() private readonly string _bCryptV4PerfMerge1EnhancedHash = BCryptV4.BCrypt.EnhancedHashPassword("~!@#$%^&*() ~!@#$%^&*()PNBFRD", BCryptV4.HashType.SHA384, 12); private readonly string _bCryptExtendedV2EnhancedHash = BCryptExtendedV2.HashPassword("~!@#$%^&*() ~!@#$%^&*()PNBFRD", 12, HashType.SHA384); private static readonly string Hmackey = Guid.NewGuid().ToString(); +#if NET5_0_OR_GREATER private readonly string _bCryptExtendedV3EnhancedHash = BCryptExtendedV3.HashPassword(Hmackey, "~!@#$%^&*() ~!@#$%^&*()PNBFRD", 12, HashType.SHA384); +#endif [Benchmark(Baseline = true)] [ArgumentsSource(nameof(Data))] @@ -76,11 +78,12 @@ public bool TestHashValidateEnhancedCurrent(string key) { return BCryptExtendedV2.Verify(key, _bCryptExtendedV2EnhancedHash); } - +#if NET5_0_OR_GREATER [Benchmark] [ArgumentsSource(nameof(Data))] public bool TestHashValidateEnhancedNet8Plus(string key) { return BCryptExtendedV3.Verify(Hmackey, key, _bCryptExtendedV3EnhancedHash); } +#endif } diff --git a/benchmark/TestBcrypt_Hashing_Enhanced.cs b/benchmark/TestBcrypt_Hashing_Enhanced.cs index dad7b34..89d794c 100644 --- a/benchmark/TestBcrypt_Hashing_Enhanced.cs +++ b/benchmark/TestBcrypt_Hashing_Enhanced.cs @@ -54,11 +54,13 @@ public string TestHashValidateEnhancedCurrent(string key, string salt) private static readonly string Hmackey = Guid.NewGuid().ToString(); +#if NET5_0_OR_GREATER [Benchmark] [ArgumentsSource(nameof(Data))] public string TestHashValidateEnhancedNet8Plus(string key, string salt) { return BCryptExtendedV3.HashPassword(Hmackey, key, salt); } +#endif } } diff --git a/benchmark/TestEnhanced_V3_Hmac.cs b/benchmark/TestEnhanced_V3_Hmac.cs index 187c2af..dcef7fd 100644 --- a/benchmark/TestEnhanced_V3_Hmac.cs +++ b/benchmark/TestEnhanced_V3_Hmac.cs @@ -16,7 +16,7 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. // */ - +#if NET5_0_OR_GREATER using System; using System.Security.Cryptography; using System.Text; @@ -122,3 +122,4 @@ public enum HashType SHA512 } } +#endif diff --git a/benchmark/TestVariantsOnStringBuilding.cs b/benchmark/TestVariantsOnStringBuilding.cs index 1819b66..bd868ed 100644 --- a/benchmark/TestVariantsOnStringBuilding.cs +++ b/benchmark/TestVariantsOnStringBuilding.cs @@ -162,6 +162,7 @@ public static char[] Concatenate(char[] array1, char[] array2) return result; } +#if NET5_0_OR_GREATER [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string ConcatenateToString(char[] array1, char[] array2) { @@ -170,6 +171,7 @@ public static string ConcatenateToString(char[] array1, char[] array2) array2.CopyTo(result.Slice(array1.Length)); return new string(result); } +#endif [Benchmark] [BenchmarkCategory("StringFmt", "AppendChar")] @@ -185,12 +187,14 @@ public string StringInterpolation_WithCharsConcat() return $"$2{bcryptMinorRevision}${workFactor:00}${new string(Concatenate(EncodedSaltAsChars, EncodedHashAsChars))}"; } +#if NET5_0_OR_GREATER [Benchmark] [BenchmarkCategory("StringFmt", "AppendChar")] public string StringInterpolation_WithAllocConcat() { return $"$2{bcryptMinorRevision}${workFactor:00}${ConcatenateToString(EncodedSaltAsChars, EncodedHashAsChars)}"; } +#endif [Benchmark] [BenchmarkCategory("StringFmt", "AppendString")] diff --git a/benchmark/bench.cmd b/benchmark/bench.cmd new file mode 100644 index 0000000..401efaa --- /dev/null +++ b/benchmark/bench.cmd @@ -0,0 +1 @@ +dotnet run --configuration Release --framework net48 --runtimes net48 netcoreapp90 --filter * --join diff --git a/examples/AndroidApp/AndroidApp.csproj b/examples/AndroidApp/AndroidApp.csproj new file mode 100644 index 0000000..4c23b1e --- /dev/null +++ b/examples/AndroidApp/AndroidApp.csproj @@ -0,0 +1,17 @@ + + + net9.0-android + 21 + Exe + enable + enable + com.companyname.AndroidApp + 1 + 1.0 + + full + + \ No newline at end of file diff --git a/examples/AndroidApp/AndroidManifest.xml b/examples/AndroidApp/AndroidManifest.xml new file mode 100644 index 0000000..bbe84d6 --- /dev/null +++ b/examples/AndroidApp/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/examples/AndroidApp/MainActivity.cs b/examples/AndroidApp/MainActivity.cs new file mode 100644 index 0000000..ebd36dc --- /dev/null +++ b/examples/AndroidApp/MainActivity.cs @@ -0,0 +1,13 @@ +namespace AndroidApp; + +[Activity(Label = "@string/app_name", MainLauncher = true)] +public class MainActivity : Activity +{ + protected override void OnCreate(Bundle? savedInstanceState) + { + base.OnCreate(savedInstanceState); + + // Set our view from the "main" layout resource + SetContentView(Resource.Layout.activity_main); + } +} diff --git a/examples/AndroidApp/Resources/AboutResources.txt b/examples/AndroidApp/Resources/AboutResources.txt new file mode 100644 index 0000000..219f425 --- /dev/null +++ b/examples/AndroidApp/Resources/AboutResources.txt @@ -0,0 +1,44 @@ +Images, layout descriptions, binary blobs and string dictionaries can be included +in your application as resource files. Various Android APIs are designed to +operate on the resource IDs instead of dealing with images, strings or binary blobs +directly. + +For example, a sample Android app that contains a user interface layout (main.xml), +an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png) +would keep its resources in the "Resources" directory of the application: + +Resources/ + drawable/ + icon.png + + layout/ + main.xml + + values/ + strings.xml + +In order to get the build system to recognize Android resources, set the build action to +"AndroidResource". The native Android APIs do not operate directly with filenames, but +instead operate on resource IDs. When you compile an Android application that uses resources, +the build system will package the resources for distribution and generate a class called "Resource" +(this is an Android convention) that contains the tokens for each one of the resources +included. For example, for the above Resources layout, this is what the Resource class would expose: + +public class Resource { + public class Drawable { + public const int icon = 0x123; + } + + public class Layout { + public const int main = 0x456; + } + + public class Strings { + public const int first_string = 0xabc; + public const int second_string = 0xbcd; + } +} + +You would then use Resource.Drawable.icon to reference the drawable/icon.png file, or +Resource.Layout.main to reference the layout/main.xml file, or Resource.Strings.first_string +to reference the first string in the dictionary file values/strings.xml. \ No newline at end of file diff --git a/examples/AndroidApp/Resources/layout/activity_main.xml b/examples/AndroidApp/Resources/layout/activity_main.xml new file mode 100644 index 0000000..f949852 --- /dev/null +++ b/examples/AndroidApp/Resources/layout/activity_main.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/examples/AndroidApp/Resources/mipmap-anydpi-v26/appicon.xml b/examples/AndroidApp/Resources/mipmap-anydpi-v26/appicon.xml new file mode 100644 index 0000000..7751f69 --- /dev/null +++ b/examples/AndroidApp/Resources/mipmap-anydpi-v26/appicon.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/examples/AndroidApp/Resources/mipmap-anydpi-v26/appicon_round.xml b/examples/AndroidApp/Resources/mipmap-anydpi-v26/appicon_round.xml new file mode 100644 index 0000000..7751f69 --- /dev/null +++ b/examples/AndroidApp/Resources/mipmap-anydpi-v26/appicon_round.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/examples/AndroidApp/Resources/mipmap-hdpi/appicon.png b/examples/AndroidApp/Resources/mipmap-hdpi/appicon.png new file mode 100644 index 0000000000000000000000000000000000000000..0abfc1b581c52b479e48d5a33b56e27e03eaa867 GIT binary patch literal 2178 zcma*pX*|^19|!OmMpZ1UX4wv}K|=z=Lls{e_C z$08JNAQVE~&ENakd?Xsw0|(Dv+Pwhib_xK~PO$=XP;5GU0k$13X3wm(%dvg(5uD)^s5Q_)kK48qG7eeFSQOAs~s-ZIsuTiNacqnu!qLVnZ~e8 z6fD!zjOt<1?Qci*$5Jus^_CjXFzU5dnsr!>Zk#$5r#^twpyD*U@#-|Z1{JSC!)sCr z8Z-jXnlu7@fB+vPYSM^s0Nox=vj_St&HgOi0iZ>9(55?T(H((4M|T3~02m}4Itge5 zorGYJbQ#VFhO;i+S(o7g&||oqXSn{tbk%3L>NCmu3^LFL46*^!4RC?!4lrbT0E_@E zkBdxC0208ujAVHMj9K0&mbVFj?QP1Yn6N0O0JaanjOAm-_BChwTClI6+5VOz{#GM_ zHh|GPA7-Vq}`fHyFHs3`SnrMY~H=O zyx6(?xVeJ(xzf~cWhvjv(-&%T78@QfHWn{7l>nBSN|#<$F1J>5-Zia!c)t4K#cKE4 zA3be9s2_d|efml3;nI4!^jRQ|23GN<& zfzi?N|IYp72kGe<85s}%o!s1_qN0}8*4G^!ot>D&hHpV2k&~9@CibNEWs+NsgTo=Y zkiHr}H$1J}H0fDD9LgNBAtoX%1>wnXCB7GNKNHXX!#x5ygySCbv=Z|*eWW%UV(uqT zZs=i0q#S=X=Mu3Tve9~4iaXu?&35rG?>h<>a0#9hai9#U6TA=`x9&Ky0G!@L^yzYHcU&K{Py)G zLq6-*fxXOv!?welF6qsPbDF(gEd^wBf!Q)^vjnHg8~Ix(t)yvMb6b7l6vl4p)cW*HIltJum<+)k*W zHo}Wa;Ud;GFS_NvDJQl`#2U0Up;gDNu?dp?;<26U5-7LH#_qqd)p- zccS_T=v4HYS5U6dq&S$j8z64f{A%x?&{$P;0be3gPK*4UOaK~>3XW)4%VgA!XybAAt)HYL)Mt+#=a6cFhdtQt${mqN+7U|-? z_<`)<(amvE8yV5+glVrQs5&WuHb%=TDYtS;UXAR5N%s8SJT!818z=O2wyVGjT(FF?Zz#n*UxoPaRsDGjGdR)4pr=q zbo*&o@a`c9p^aR2!Q=q+Rh8JGY`8RZ7;XkX8j+*-pMO>Ur*l(7W9nboXX3Buji8fyZ#)?YxEgF%SbJ1`@Ck~237#Gy zqjHz3vOcQMqZ=48<>a|n^#c=i`lC|wDB4*(J+{WIOSfttk<#l+jO;?n3=__bCj1d` zrz@E>t@IUl@+~{jRsykzb`J3O9O*Fx4W>$deB*HP(Y(7cH86jgRVSUs-(KE*8ho5p z9;IHuE*zR-u{~~wGda|(edsRgOR10HCC$=lecZ};te@ZP=pC8&#+*LinUJ>4&Du64 z!-ywSoaWpIoxlHHP$4IruD6G2AM>dkAb~bJ<2CN>KW#LW7qKgG>0QFT%)aE!U!Ma@ M3#@s)spp^n1!tBY8UO$Q literal 0 HcmV?d00001 diff --git a/examples/AndroidApp/Resources/mipmap-hdpi/appicon_background.png b/examples/AndroidApp/Resources/mipmap-hdpi/appicon_background.png new file mode 100644 index 0000000000000000000000000000000000000000..513e69d8412c314411a5940370e552c59fd6bbef GIT binary patch literal 97 zcmeAS@N?(olHy`uVBq!ia0vp^i$Iu>8A$%HX^8+*%mF?ju7TQDY)?)43FJz8x;TbZ s#JxSh$P45hQuw`m+i~{Oj0_A9c;7HE3Rk-M0EHPmUHx3vIVCg!0A>pr+5i9m literal 0 HcmV?d00001 diff --git a/examples/AndroidApp/Resources/mipmap-hdpi/appicon_foreground.png b/examples/AndroidApp/Resources/mipmap-hdpi/appicon_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..99d3a291bc382dd1afc9c28845623ef77fc14c15 GIT binary patch literal 1276 zcmeAS@N?(olHy`uVBq!ia0vp^i$Iuz8AyKBd8Wz0z&InoC&U%V{XYr@UkDtSd^H0Y zH|-@se!&ckOe}2d9GqO-ynOrufrHF3KJ>D89HqxGL}xr=zmy;bgN zx63@@DR=GI;fEeA@((>McJ%&v=#iPY_{nRVh-}?F*E4fh-A!6HUF8_7wWpq>$Q$eb zGvBM+7LJSjp`O&CvhJGlqrE!UE6z$51}^S!p8q4E?0VmdjK%%jWlvuC+wI9(FS;?@ zvAgVu-D5HNP2z91cjpQI&J3T`fBN>@j#;@S_L6}I(`qJFo4AMEQIg#JC*|*!Ev{b$ zS3cIZuYNv#>x_7hn^B@UB^Ote7Ve2PT{Jn~v-`@gYg((_A9s{IVCO&C_y5O|$Ke|; ze*1rF?rOP>6O7N!Il14}T4bw7tft&F=1_AhYpE@jujC3FbdGTzjLW-T^PtT2qgMN4d(KVlG}dif;>xv_d)b$mJKy&y969dXCzfl@ zbv4RFef<>4qVmP}Ildjx&YXDbnU>O~nssN4!=rNqrtzQMB$jFMxF_Oa>uNz;?Iu;} zRNYDWw+i*Vly+^ucGcy!(CrVA15qy0|sZ#H3{cP!MwQcHR-A5lY7I7SXbX5G@ zv}OA$dX9SA+e~G*%GkSQy4hv1_~z->Ea*vvfuZ5Po%ec+%^8`mKyse0elF{r5}E)4 Cmmqim literal 0 HcmV?d00001 diff --git a/examples/AndroidApp/Resources/mipmap-mdpi/appicon.png b/examples/AndroidApp/Resources/mipmap-mdpi/appicon.png new file mode 100644 index 0000000000000000000000000000000000000000..7b5a2e2bf8dc4d63e6490db8b5e269a63b05841e GIT binary patch literal 1524 zcmbuF_#Nit)318A1FIm-vIV1Z-!VgR-uhUEv+lI3T~_P1pDgS29kfun4) zHNXk5W(R<@VF%c70&UoVAdj&Fk8y$kTTU=w2XN2YbH0@V?Lec`VHdE%OU zab4j|U7@6|NJ1--y!=DbP%3FClQaV5(k7rn+FU7Zu9CJ?OFJ9px|-*EUM=+ewfMGe zVW4a2L(dmhFU0DD*g!wT8Gv|$(D2am7<*-WY;{7gD*C)OBU+b;)}_n{Pf%{HFkL4?W4!xeTYB9xR>RMmFv(bC@g z-S@^OC=}WpN2hmmba%gd_x0-r*!iEiqvFU1*LMStbt1XmX64W?AaR;$?U>`6PBnCe zF&rYilOdOs+J-eWOv3i{F38)%m3ZC8S*i%deF}rMd+PTNQ%0Xw+bWD=I2?;OXXn#Y z)@ZYsSxpQ&KEI_eUrhY9GJ>ynkTKT148NB-W0~Icyf>uDUZq&Efd*YO%F1Rgg^W;W-f!C|S3yjxGz9~$7(;As32%PB~J%k0Rd zY2u(Vm(doK*81`p4M*q9UF4=hj8=D+VM(&kwq-`Eyf%4|%Nct8q7q@Gt(~cmambzh zmaM5ZMea;3JD`z_oR-gI;H@mbO0J-_u9e|80b+qHnxT zBA@)pC^*`wzLs$3ko#{+5qW-hgQ={G<%I0q`O8NvNkv??BPHF+KVKFww{?N zV_dmhf#8N_$bsjoK3837EBIDTRo9N}F*B`0B|iAb zD|r4iIIv4mPBvTl!X@AD{DagK?Gk;njB53r23AiF`$Ve`nFdvkFJn4W2%o8KNX2GE zkW@&bBIWBA+Kpm|4I9+M&S{KZ+Gi%<7Cw_?u;H|UDCA6hQ4W1)MYEWV&Vm%*8Pu6eERqL0_sU3kz5}2=pso}G6 eExkeq-7T16_EhYylCAjde>WcIfvt6>-24YipY?nI literal 0 HcmV?d00001 diff --git a/examples/AndroidApp/Resources/mipmap-mdpi/appicon_background.png b/examples/AndroidApp/Resources/mipmap-mdpi/appicon_background.png new file mode 100644 index 0000000000000000000000000000000000000000..9e2d1e4d8d4ec9945deffbde04bfd430830a62b1 GIT binary patch literal 92 zcmeAS@N?(olHy`uVBq!ia0vp^IUvl)3?#R%INArKm;-!5Tm!YQ*q)m76UY_uba4!+ nh)Z4|B*D5kL8PZC(1449Erfy5Q}4VcP@cil)z4*}Q$iB}vRW1{ literal 0 HcmV?d00001 diff --git a/examples/AndroidApp/Resources/mipmap-mdpi/appicon_foreground.png b/examples/AndroidApp/Resources/mipmap-mdpi/appicon_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..a28d342c1c7106283d3e5b6f03f2d761a9d8a28c GIT binary patch literal 1273 zcmeAS@N?(olHy`uVBq!ia0vp^IUvlz3?z5#SpJ`Zf$@5PPlzj!`+pP+yAX(+=)M~m zwR1{>{DK)CH+t|g2}(z@oQn2o^7X!YXa9vg@0d-0zwy+(`*QL zb&^Bzm*{6M>%wmi)7h^dw>a|AmaSIJJaEBx&MQCczN%ZDWwmkI>DOt$Mc#Oy-23uf zCMP5|3r#vWe?zfLO2D%x8ZQ>MOWu?{bID(M+GXbtTgA@l{JG!X+v7G{TyJks?$dk0 z528YJj}_FVcN~&ZS}!BJ#dk3S1Cyzzi(^Pd+}kTp%V#Hu9C+xx=Ik+#?P(^W8E%_& zw0!UGUH$sbT_A{kFK?|;l7Hjn&2x<(QlHz!Klt#p{N8ESANvKJI22n1K*WSIJm)|6 z>P&e5@MoQUL(H+&Ox-_xz3!}y>zel=bgx)awRy43^nFa;QO_*id9WO)o|D{kv$5>l z8=pH)YjRjuMcqCS5a0RY(~6BnDeJ!W-*wnLd%Mo@G9Eqg-Luz3Z29(;vgO@&8Se7jc(rQ+rLUz#?+3p9rPo!Fba3s>TiJ$XRy}9qU6i-U70wlme$8Zj zU+c(K?Z%A)))uV~cU6fMTZ(xrzw_!%%8QpXdAI*q%s2mSzLpIUdxQH!|2#t$n3dPvGv`=h)ae{F+UU^E$3K3FBI ztDWytI(Bc9;j2x~oMPISLN=Hl`z{~ARrXKv^EDsEXn|+R2d_zRC(r*CCDPz?evg6X zVWIO43LL^&G5^%gaPO}bcQm$DSQc)I$z JtaD0e0s!klQ0D*u literal 0 HcmV?d00001 diff --git a/examples/AndroidApp/Resources/mipmap-xhdpi/appicon.png b/examples/AndroidApp/Resources/mipmap-xhdpi/appicon.png new file mode 100644 index 0000000000000000000000000000000000000000..b28b73c65453fdaf28b076d881696793e8c9bbee GIT binary patch literal 3098 zcmV+#4CV8QP)mAG*{xi(%e~m`(X|XO>D66P-k|3s4Kl5?*=M3 zEP%0Yz8&A1hxd8rZeVT)MrV30-q&XKEY?_Vp+p=B(}vuLzXmPLbwsVN)k z?*lL;h3a_Rn?g%j+>=GEh0uN|NZCJc`NpNExqT^{gYE3(B7_qR9}5 z2p0K7)z5EuIMKD@fp!PsAo0@x;-|61rzr6$N_@(IL3SBKx`2E_G(`T=dy|xSy5EyD zP-~&o4v=(!{4D7SN_@f~zXtCHiV|oE{<`HeTe}EXOeOdhfW*#?YZt!0%XED^==?{f zE4qK0-2Isfm};+p`l;&f7gYe{eo+ND+z(YiagvIV1rcG{%)jm=oKfz4Rlzm1oW`!6 zb0h_k-y|U(BEQi=K3f4m>}Xd2Pkxe;pM=Thi5Cb!2wyC$fTU3YRwdMzzm+7MF;V2% z{i88Hi}A%355So(%l)9-pU?SktAHXJMfmR%&M0@Iy;gxGz~?Z&u%Xs3;6|=w0 zp8X9ToL}e2{!}*mxof}@)#$;Fw>h8gHAV#_Y}WvSY6`*`<<6G?W9S9tHf86# zeRe;Q<-eo?YD@ucquh=0E5J<{uVDD_$aJo(}-h^v6{K?-oY ztLt@gKcwsrGymN?pafq5w^Qzd5kNWfMg2aQ{cXzG&sG3TJmY*(1yIuUx}SuS^MMM8 zMgU>vi*i3t+#hECM$Dyd+z^> z6X93DGG)hm%zRnbqmunzXFskNUT6PbBkBLE3rz%H0W!yHI$xIidF6h^`A;Mf-%llAJXh6nhMZ#yxsL} z+5&*NpI7b|=YKdAAaFW6&u^4uy<6r>-|5nOy`pKSYu~RrauCjb60Wl!i}+9@$j$LK z*Oy6LAC&X+)cxUAK$%;YBPZQU<~xa3yW=J5`~78qGKUqQWPb0b z8Q?RXXvfMv^Y_n%_g3r`wt)O%WVa?5wFzSQ?$N$0a+`31)<;X zJ7td}Dgx=a{(bN~V%Lt9y@mjrs=LsccP=5Fwde8UsK4`LZbxGO%p$bs%A>;Wj zr}fPt+lwPaZm8&VWWFK5-#>5&Kr(+*l?MSHJC1rg{0Sg+el9AYUO@yt@7W5lR!8fH z=AScJFRB2N`JM#870}n=zXC)egc?CA*{<5<@D*UK7k0jb|2*66kPAOp0j16g_~5-^ zv}08-`c8e14cqJRUT5yhXSAcu)!oLgw=X?r?A!2u@Xv#p?{oQb8b{(uw5|4-fvt4k zT+^+T{ox7Vf&6;Gn=7aS$QtPW1g)9fnXP~_K?MK;ymxxoIJczJ^;n(H%fC636<|1x zezNtrcIU&35K09INtVs;f!4D1UqixDfAx1~jjtg&Pu^$n6+k*DNPu&v>;lMUJ^Y<6 zUVvwb07V*oe zoR6>qB=dez_D53zQ`Ftgk}nc~yumtrXvjvwi+fH=1<>h&2G0(ot@vUKp78`KfEB=t z`$G{x-Ra5fJ6XXjhZo>S>-!P;?Ah6qSwcU$a0P8y(v4aG0Z6>yPiCUiIs(*o3uiqq z|C$qE3sV7wdd|d{Vp25l#Qa~e=eZ6TP_F%U@Ktg{mba5Py8=C zw%D8n`TkAQ+l8|phX7m!2)mxbTnYeWJ2*H4PA@j5gGh`q)MW$#xY1NEbxr zL#}5#6)T{ctpJYn{Hg#Kw?k7*SO9aXxxMZf`f%uqFiwO5oQ4Ger<~lRQxrj}J%1zu zu$c~KJ9JKmJF98U@p%Dut?RdqkYD=wDPu?nt?w^~uK}3~5Y6``pX~xGfXjYW=Z9AT zI!?#HALB=VeEu{#@Y}QK7f+rrZ>$R{fE9p%d=g(Yvk_Im;F)36T-jxeoI&9K59!)0 z=d*KuQ3UC9J&{y^-SH`*;|bZXy>!ko2(xaSxK#iFxsXaLpi+Mrw|Dz-@NB2B0t)k} z0+uS~I{`UG$G?LmGk+`RRRkcve*I5_#ye$O1z-V4#CR0|y2GU+fR6ja5x{l=EeW2> z$?UjF{_wMtwio|IV^4RvU0y1|Y84caPC13l!re5^iw&NL)TarWoa>0FS z!vK12tNA@ZY#Fd&4ZgNO6HYQ}eir{<2}tyj>xEb6hbBO+&GGg<`#D_r<~V<&Ty|D1}d+2^|hOU3OAU z+41ntZZI4V_4$gYnZ0&95k`=@!~Hm3MFgSi^T7GxRzQG`XUSI0did~^kIdJ&!#vR-<2GY6R;z5u+#HC?XlcsJKe^FDXZ z54QqzNcKWDnDyeHRw6sS9Cn|d_X23UUB&Tku9pcAk@G_rfX#Akf_bMypUIMZMoY`_ z5odl*D?nQW9mjicy;AfPjxYiII^I3w#eKcAzlRCWW#&gBfG?DIbG#?l=g|4l3qWQw zT&6EBQOtHn#wT3+dWYi+6&xR-^K)APC7MoGcKpb2eYgSu84i=25d7Yr?MfN%C+j25 z{AdL5>TrqMbsewj`dm3bGy$|S-O=dt4>5Wv6F#cmJA@ws+=G!-ClI+W?c z`Mhq%k09rVUjfBxsPX7_o!H4Y*M}*<`0AooxKEd5x{Jd-J3S8xXR+w+)*9*}1+Vs3PHMrVw5mJ&Q|NR)^A{Ghpg9x&(oZ;|7vP)yK1GRtk`ht$D;Om+Chprm zx8Oi`#a#yxj|Bl>zJ-Y+19K)G=)Ql#zWsz7%F#H#Pf+4TxR&M_`Nm(yD8QJEF$Moo z8NSYxU!5SYop4}qQ9!vFN4W~*NqUw5*5eGx)8WQq+=_7rRr^kiuTo67N?^U-J1GZu zP)^2DjzW8&Cuym!r~k-J^dG#1;tN+1ZecK%a&aT&CX&b6jvi%yrtsAa>si91!z3OO oB8C(3idVei6|Z>3Yox6I1HnHZ)6o}3k^lez07*qoM6N<$f&~%~2mk;8 literal 0 HcmV?d00001 diff --git a/examples/AndroidApp/Resources/mipmap-xhdpi/appicon_background.png b/examples/AndroidApp/Resources/mipmap-xhdpi/appicon_background.png new file mode 100644 index 0000000000000000000000000000000000000000..658be3fb6a3bf5656c0a725e6a5fca3dc6cfb7e5 GIT binary patch literal 100 zcmeAS@N?(olHy`uVBq!ia0vp^H$a$?8Axt^w|^awVh->LaShbIVtZ=RPas#u)5S5Q sBJS-0Lq;I)kizff+m7l*@B!Hj3>M-G7#L-mj@N)Vp00i_>zopr0REvH0{{R3 literal 0 HcmV?d00001 diff --git a/examples/AndroidApp/Resources/mipmap-xhdpi/appicon_foreground.png b/examples/AndroidApp/Resources/mipmap-xhdpi/appicon_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..70a542ac03c49ac1b30da6135d8b48016e1017fe GIT binary patch literal 1805 zcmeAS@N?(olHy`uVBq!ia0vp^H$a$!8A$H?clSO61Cx1xPlzj!`+pRSh5(5nkh%M5 z5isfODGBlmW?*DuW?^Mx=iubx=HcZR5E2#<6%&_`l9rK`S5Q<|QB~K_)Y8_`)zddH zG%_|ZHM6j^ws&xJa&~cbbNBG{_VM-e4+snj4hanlkBUxAN={AB$jr*k%P%Y{E-A05 ztg5c5Z)|FAY3u0h>h9^AFmck<=`&`|nLB^sqQy&AtX#8h{l-n3x9!-qci+K7hmRaP ze)9CW^A|2%zIy$}&0Dwc-n;+c@sp>|p1*kc>h+tq?>>C|^!dxzZ{L6X{Pp|K-+x{m zR}C2$m`{4TIEGZjy*a?h3G@}y0=X~$bFG=q8Us0~V8P>KclIyrT4b^B`9TwA?-nlw zuU-Zv*UZpmOT;}?Lnj0UtEM>9Nm0d5rd-|$=+PS|`*DYV~ zPQNzm{I7X8v*(}GD}J%+@y&zL{5rbDs}!b~g&ddBciU)Qd61z{T(Z4>^0KDhK##X) zCe5mNZ8$mPW35&0kL%|@tTvW=aOgr$!>PZC$s)h5>{B_+xvy%?M6J5BOL`l`toO&M zGmH4&WGzWQ-PU}w{cnS|Df^n478{PTEBT(Wo2?aO;NAcJSO21)?`5LT3p0LlpC(zQ zy-DJNLnzx0hBJ2Mv3e6En7?gvS$y*3#f%U8AA5df=CIy$W~X&yLaDkY`~16H;SD$K z)Yn;W+?F9QA&ui8*HWG8=7T<07UkdCV`O+(>+Is(>9d<(3cNY2w6#aPVvR=+^EDr_ zh_4-IcSckvxlW6C$!ol=X05#b4Mnz|HScen&5hk5X`}kowU(}3w_j{M|43$iYleZ54`@ZPsiM0n0?5+^}tno=mT*75r)I=4(O}7G7gngVi&N_MB@mZa^J^j5_|J|lf(~|%0%QN+Z5Px#L#d(jT4PT$7Zxqoj zFE=><`Tm7n%LPt)xMo+sWx8i+e8iB&I4(*)F5wRA2YF$?=GGR@xT6Ig$$x zdgOoC$b4s!D!uFDJIhI)A9!b}uD4nxd&`IW$^8X4xumxww$6CA@RY{RIZEOzljqGn z&ZXITwB&`Z*ynBsMS}^Yw>-RbHp$xO^)DBYXgR-b>WsRgoYtuCI|Y1#KArA6CCt+7 zJ3uQ9SY-sPE*kW{P3+Pq{}zwl&c|3xntK9yRvd$@?2>^=dTh9Oh literal 0 HcmV?d00001 diff --git a/examples/AndroidApp/Resources/mipmap-xxhdpi/appicon.png b/examples/AndroidApp/Resources/mipmap-xxhdpi/appicon.png new file mode 100644 index 0000000000000000000000000000000000000000..f9af1173ead9da4de599b034ca8c81aab47fda54 GIT binary patch literal 4674 zcmaJ_c{~%2``4_6=8lZ8awds6iiTm?Rv1~13AwK*$7Hlvg*jp}a)jm1S?Hipx#pJK ziQJ6P=2m?B{QrCX{&?Qcf6wcA-haHG=XuANo1u7l#CbS4ICwE=Ac%|Maucl|GU%a!o zv-x{|GxJx)-cVlfGq<#Zo}$LT%d6WxI-Q!sHv_x>KX~+3(9EhcbS7R_e9kom_SZQp z{bTFr3qjz1n|yz?o7oGWQ(tZRFXr7h=J^5NX^EG=@M(Ica_8?c1hy zU*G6OZ(Z7WhvhZA9dIwhv6P44VSH|q&g3i0gy27aWj)v0-z>OK8Z6BO9p4Zk`#H9L z?f*P0pPCAmzo>Hg?w>8U{d*H{XXgf+aW1~9*IVHugKiR5x|hT4_CHDeY4yVgsMb#H zakg(B55RA?jHi_BE-jyDL+Vt@50*hy!w>FfE!6g0rryk~8jqxFDWuX6RhxuO3A6Tv})X1#J_MyV8W9uX!-e|`U;?wACpZW!S4mvp3=i;6& zY4Py!bO*fy&t&Rp<$}tq%nX!K&%Sq{GDB> zSiSpn23aK#cxw^NZ6+6p2+s@*I zuJHQ?V)Eh!Fn;5oJD06n58g^yc$}BgtLXNO@R1eXSW13@wYFR6<&P#8piUu7V5k|`#77K7maAlT-ADqL^G;CNXuCF|j+~4Jmo4FHUDx>XMZUHkp zO%8#ek#s3;z;76pg2-|J44&n)uL0w_dl3!*r%kP2hYG9L`isc!r`OgAQvI^<86gPz z^0w!%rX+MOi5?6{C>2^EC=2NMzYK@_4h+y=GS_VBZy>7PQtk;^16+-pjWIG@pb`!FIw(tIG)X` z5xL=I+XoUiWr9vZO;bwPjIO#9%X+2i=_uUp;28lAQ7bA57-@8b90HmaRy5;k&WRKh za^f_V11Vi7BrDN8>;zbfxGdAJMa9*XP!@k8;sy?IgQ|C)KS17Cvrin-L{aMg`9}Wg zk0Nd=Db|))aSWHe9K@I@gtjo@lC-XySQJ$a980bz)MiU zC*yI@Jb9&B4Q8)EkapiXYqKBQG}KVT)K@!AM2srPlyYmlqs&f~5z{k>qZ<1#TDbBV zfk-$$9pzu%X){+VXuh3fv)?aIZ9d4+Ivlem(71pFd^C7QEhY>-~(*9sh|KdDUHl@F<;*W3z`IfE1>DQGgDp6yo z&UI^FahQa|+TX3l#%GhDK8_n5fhwFg#yp`nn0Fb~b!oE@hmF^t5P?Wrg(-DYz}rfc z9D8w=vR=<{9ioPN}H^#4cID&w-PPl!Ra<`eET3b^yri1WX zPHWrbA(9RuHcFFRP8vng2L;;A+hmCVhOQhk=PoY}A$H3ib#|HID)s4hTBbl~g2}biLOIOvLOVhU)Px8{8L>ozzIX;Ura) zdwI>~M|3TDLW>W0GOpa&m=isez?%*7$d=^K$v;T=?sq8-MYm6ENY?l| z0pC+&6EZ#d%k;&a0B6M%6E9WSMt>9hzMuFI#{V7hh*;|}TsfiH|5+j7s1N~*f$EZ<+D8sfmv2t%j<>Go-J{-THDrMEIZc2oh$Fkg18fnV0YN<`pQSMHil zK}9&g>#OmrN9F@&hilaXx2bO$-Wa(;ozIAp8#d=$UbbC*P5a;zj^Ei($-Xj2s)vN@ zI>rp^Det_~`_l{2P?kpQch`4J?#6id3El9}CwL56$w&J7@;%pa)D=*#^ zhc8&&hV~9*rjS>EY3Evbi4Kq)Cie|KH*Ow*3HVf2x0ZEQ3h=r<)Oaf5hTdQ45(v%x zdD=ULczAA(Qh1^bAxIxC&sninWzSXd8^{Y?Z zxF-DtO*pqJbcO+Tgn_>NLfwbukol>}$!H9earU?%aVM6GjC(R$s%ZJ-;l$#z zgOJ$0`jf#8Io3{5d``-Mzb?JU+I4$#I+&HGrpAOef?lQ})ZF&UN@_+B@aaWr^xM=H zjlP%ai($WK+lrPH_qNL0s=_kTK`epdydD!das7agrXkAO^^MWj=t?gG9R%gg^W?q6 zK7i^Y4N}RfP7T$+Z2~+g4!o=0CK2F)bj`jG#$cfC_Z~U(lveMsf))^SNG|?PBj3GA zzS^oPv-$BMGD-S+`20J4(q%V32N27TOC1Fr8QmIooD){$U?B$(i0N0syezKAZ$LLf z{WZrX^>#xPYl*uOHvzz3iRD zN%x_s_rN;52|X+WFXg_b@JmX$Smen@kf_#2|QP?H8^{m5I-&TeYQ>O%nh7vDS3Jm`-7 z+(FoFr&^D+>+O~yZ?jU!c-cbc(@_Ng(7P{>_g6kJ4pEaV{2?b)`ZiRNyecS&eyW$Q z!~Yl&)$SrlexGhEtLOh`q2@i1#XTG~ae4Qp6F~Avg@1e2vXBrO=bs=;7T=*Io#9=$ zElNSeoA3Yp(f0Dq=^5a4hHg0m@CJzbCyWCH8D2bSVXU9;Wm?vQ@W)CW+gnmSjne!{ z6oe!#&@Sv=4nSDZg-evYx_o)1w5F|{`s>5fm1sxx*_$Vm5%cPa_CRL8;ERr-RHK2O zz0l#QhodRV3k-s?)(F21A3 z_nBCI4g%O+{l;dm<)|7$Z*az}&cKzqU6sE76bZzK&0&jp+^zcre(9Gcbl;Qp&$vU! zSCYGgi&LsUP_NN%F`KU~ukq_|#7 z3}BG$bVQD7z_Kx>hiW=i{Y+uykS>N(h#Q~ZT{k%44PBIkW$(0KJ}rj+SecHY-&zu` zA~SclE!^#oXP276`1AmeE(UM*O56fD55-vb@q)PuJg;C6SHGCC0>6ZcE;JaX`y?Rz z=MS=jK?X+_>T>;{m%QGW%uMdag<+)@B~@L`%osx)VVuMTWeJdwZwh z*tmObl@FaTgNi~?UY!#~wr;-Xu9B%s95B^=ufc1?rH$K)AhIU%HugY%6<#hp&5CrJ4_ z-=F5#W+Tj~SF;vij%zW6?$ESty=6NyEbB)?I}hvett`xue8GGG?urtqUix&7`JQoqA-KY zE9k$C_<)28E_CB2uBcKi)b9?LIKR^2n<3iO)R|?P4JC=?D$6AN0P=L1>y4p>6+D^9 zMEI11q74f@N>P@>BEh(()5mA^I>ddf>j;u^ z)5vJJoFQG@sK!DrfnH&mJMel0VK|Lg)?z}U_ihp*>&!lThu**>DHs~1VLG@xW#r8S zi)X+<4eC|*q;`#XUB%58{beMU17LN#G;km1KN#*TK!b%Kp65 zh-%ZDaqzn8vQ*FHHc?KIL=Dw3PXUh|^R_dbH#jM0WWjVlBN?zNSRG@TLC6wPFxR{( z@8xo4f%dO)St_)N;U?bt;1p)MxU1^@tUr*4f*53ar#wC7Ycl&>s31WST;QL!t;x{C~8yfw@2Kb zJ9&y%iekgO_5XXSW-yZ=Z+a5ih1zqaPGG63B=$*tXGQ4iyE`SYyi^WP_3;2c(`!X* zW>zvNH?b} zC*aU({9Zo4!k+srvN)HFpK?F3x%Sza?u>&-J3re?zb?Mc6nNs7y{W{kA-OB&VKSzk z`@=0aXIZ5VR|be`wd=8tZSE!x!-@Ox8$Tjlkr0X5^*QNQ zP08hdu{Agqo^EMDIQtdXfAVB;WX uNJZS+1B{$N-XVkE%gv4_M#})%aBzUpRD*$Wxm}zqNXXOG&t;ucLK6Vqh8R8o literal 0 HcmV?d00001 diff --git a/examples/AndroidApp/Resources/mipmap-xxhdpi/appicon_foreground.png b/examples/AndroidApp/Resources/mipmap-xxhdpi/appicon_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..cb63bfb9f8e0d0578ca53d46cf8cfeac7cb44689 GIT binary patch literal 1926 zcmeAS@N?(olHy`uVBq!ia0y~yU~~at4rZW8hROCX3=B+S0X`wFK<@uhFd71=At2GU zlnIzJHkJhW1v4-*F|)9;v2$>8ar5x<3kV7ci-?MeOUlT~$tx%-scC3x>FDVj7#f?J zSy)Xy8X(l0c=;%+LxT+jdb^;JJsEX%lga^2i-CW5Ys40gF2FR(T>X$vT8DJ6s~=+f!R z7I8G;($dK2@rn&#jC6H~DRg&r)mR$Trn&U#Kj+{{RmTjb+>3V7DZcmR^p?5${`bE5 zR4;4QRGNA~>E_eKgC&RTJ(C}*C^8gu7Cw*qqOfUx=jq0Z!c^r=)yMt|Z?u~w5X&m4 z93*^mbEu%Q(4zHz5)%ai4ULmroKn_)1c|(NIMT71JB@E+)KgZmUtct4o}2Mr+EpM} z@L>M_4FNrj4Lv{4$Jh0@HETUjb(L1C77>mV-dlMsvucn;-G3{r^l_Q;xtX~+^ zvV=@lZqHIO4b<}e(IId+^HqnU{kbbHKP~3WbL%|r_V1um$GpiCZZr#>Q@X0w=Az<~ zFX=FAQ5Q3lugZ}kh81BN_qrbDDk+`|VL0%rZcD+_BJ+s?=UMu?YdbC`KFU#2{H4Ww zyp8S13_am2B}Jz<3|S|nTLd@-{d|<%C%67PD(GZl@7Q;r)v4o(ON4osT)r;TYsUPE zuBK^o%LEoCaXCuy{OR~NeeSBt9c53I=kZn@`L58RUH>v{SzidRso*ycjtjEdEgl`e zv{*ibaVZKZ9kF$1IG?8I;?&YC%yA({)n(qnrc76cn`$i{zAdKB93K=tIVK7`oY2D1 zYXwxbWt(zCvHkUbaF4V6j}6D^c0UO_gyz9-xj8$EqO*P z0;%`Zgcki|ywoGg;P6dE9~cUgxO0CocLSh7P16Add z&ZZucJKxW-w1tDgytckNDEWbV%eR}rL?`j=^=#H+tuDdMDJ&Cmm^yAMcbgfmx#D7& zv|ng}^`C=HJJ(jaoL(XHpxCHh_~56g*BzREss|oaF}j?PylkRgc;e cRV*+t$ftaLdFqvjxFSfsr>mdKI;Vst0Fk9dIsgCw literal 0 HcmV?d00001 diff --git a/examples/AndroidApp/Resources/mipmap-xxxhdpi/appicon.png b/examples/AndroidApp/Resources/mipmap-xxxhdpi/appicon.png new file mode 100644 index 0000000000000000000000000000000000000000..1d948d6b5d88179d9aaae8cdfc063f9fe8e08807 GIT binary patch literal 6832 zcmb7J`9IX(_kT^Jv5hE|eXS7hLP!)dL`uDll!~ksW6e@_W(E}{8EquYByUSul6{#O zQK2x%5;0>Z!wkmEV75>F1>eW_hjY)pkHU%k1Mq+HE*4+sx%Lm_5VK6d)JxvGxOagNfT3jnR^A8 z09(*=-P}>GFK%}H4rg?MTFGqUdeS_VY^^J{gbXgbURaIDP}RDSZQ$K@Ph9r@wOL;D zA1Fl3+xByg`41eB{Q<2>f+gPyv11#)8u?wev)OXIN9Nabl$?d2HQ^%r^eAo?covfv zsj_jPw!fnvS`~ON|M_%%8c`VWf}BPDSjOu8_j?qnA?8bddwqfr!$0b_Yd_)GotTBD zutvnu@4Zsr+4x!ai^t8b*i!;3)0cT{r;etYJOBBfUWh&4uJp=thPA(S`$&zi4*@a! z*C?p+eACq{g5V>rfhVt?r9AJNNFljQ2&P%3^Cz?Pqv8-pDlaOyZTbn3B4BWX_;AA&t8oMvSHu4iv z4kdXww!@V|7(O}N+a|i#0>2sN{Y$TKGm!9zbZ~;;`=}*uMR#*gWdF!dTe6PeOJAEl z-R*tLBT`9Yob4A}c-i5P(>U7p{pPX=NnwaZg@)7lgo1Gq`gBL$$;J617;53`JJT|s zhSimp&%Ky_lJzdWQglBq8T2yL0DeRiY@^6mfqEgoz0g~TxJJ|UY*rcBQ+l{rT=P&$ z?qZ0XaPsw%2LZ(|a2MD*SfQvla9wbo@@$C47Ybos9o-lgtkFre#^T%;kFSrshYNn+ zp4Ap(cvNFy|6*}?f)UtG;On$0@_Ql>vi7=KUR_U8MekdsrppE^(1Vz7Yz5Oy(X7@E z-H#4gm)hlunyZsgn3b~ znFX4He>Tp)8$mw|OdGsL)mawx8$fnGJ&!g<_V9Lj&xYibdU6A!&^L;NfhYP%vG%J_ zjI1zMu4l7f+3n^NK73JX(guA2fh>31Y)epoctA6vp$4sYolEx8K&Q#CpH{$M9#O@A ziIR5w_a?BjlQ9myyhR6%(=#}#_<^e93BMcTE~e)x?Asq2AQTiD*_F9`xD#i(bVnVA zFcL`>ylH~PlnZ~D9V_wgWz^3o-uVFciv!j36yO`93#7;xT3p|aEFZvw3VSW?nox^Q zUg4&E161QMdl(k*^f+mJ=gu3);f5Wm_|z&$!)oWE>}CDfUwrh4jxR^FKxJMQTk_0BPGTv*$On zt1S`@05~%)A87VPX&Q2$LCAtzjc^GCOcbQ#QJtvHbZFu!d5m$Z{PP3ndRQ5*;K$BO zOxhs-T|evrT{=ssrUH>aA8_QUl!3$kwNEHg@!(XyAe@LM1kCFD_@U>!DoN~Od(}fm zaOF5mf$?>2I~&Lh27(ajmx?$snVy-nw@UnoCRkOUFliSy8T2*NHSIDOTCD^Z2Vd?q zz-+dlD;U(J9stxWh^B+=H^{=P?{kC;W8gv!s6lVoei;G;dnQ3{J#9%~^A_DoM_k@O z|Dy=s>N?s{ma)-7T9Wm|vJr9W#GEZ*fV(#7(usLt+7wgh(dlVs<}Zb}X41kF0Cq9` z88!X{e{M+F<%f|Nf=j$$(>9@lSylqP8?FT|1jcc*My=4M;f2_utgB4BM=%v3-3)qED}9wN?Pj`Hm6Zut%U$D zVm(E+B7UQ+M(m`v?FPA5?{2>ig{-nUJ|^hic-X1}HP#9u^O_^K#t&Xmqc3MH@`V#n z?1M)5!&?}IJtdV2TYvn>{D%l|XF&YqDOl&i!VpgvnQ(A{ALtU5@L*{h?u*WO(IYsR z0`e6}-u6}}S)uuKM2ox^-z&890fHu{sx4HN zi`pb#pnE$8dj?fYgjEVjI+1ahaH&-N)a|g-s7zU^P66rLGwMA93=YRI`QASD#}yR@ z!QxnJEmWLlLbQUqG8I-H^oN>TH#4xT z>@%zM#IQ}}GwHUR<+?~xGIf~0gK`}Ff$l}a8i_Z~l zv=QJqxM3n+T^^wJ?KXj6t%SD>Fl8C!L|zLN1D2bYDxDBm!d&Xh_IY9aKN-ONq$Y0w z`^I3IP`vVrLUGe;0NTd_$FDTh21P1$E>L&;1P_KDYenMn>z=4t9C|_8^hSc?S1HAK zp0Pm_#sAIPuZpfN(?VQ{a^nC_FjvFs3l2$)N3E*1&M~UxCo}Zb zs&9L471eI(#Ti8w-T89i0eXT!@8XRSD%?NlF*!kgnnKi8ez!0={rSFyC{iQUoSn(x z)SySShrjy7HH9bN?6KeAOgPqFH(T$>0WIAvAb{6giknXTVPTT^Okw}($*KD$aQZp@ zG!Tl5vpa@6|I1&0?sHU6(k@y%4862v5wUzqgIANKyFm_t=tw;QEl2+*!EDg_admd0 z;JYSpOtod;e7%tpKA}UL5hHB#4JM*bDXFK<=2B*TFpu}!HOZX^4+lwSgN#iuI_u2~ z3B%nfhH(3tA0DVh+%g1f6HRha-3|GmlCCvd9M{9<@dMys$0BoEC4hJ9YHu0B=*}?G z1OsaDO|Y5gE8Gu}hu`w>blNex_E~yYoI^yCyqBxUK`gmY`yQyU$YLKJt*PWC-q#_Q z^D9c(uge&y6AcL-Kb^ObVF})B`H_|e8u)fQc4kPh^#%BEJ(Y1=f&@7cKY5VJ@r+wL z@ZSH*^;`1r`}C}vnEBRz*h?wA^3*5J8WK0VZPLmiCbVwz;34jM$sX!9tix)Xy@k#MFeX^^qL_CLPm#MD|(5y_w@GyKM2%zNq@{4 zV{^=$sNO0=zkD&DHu4E@lEScc@o@>c43-oYq|1B?vNzEnNG~yu183MT#HL)pw{UoB z`%Q@gs26OyBoFk+ET7R6l#~dSNup<{fJ=@n8=sxwmMdjdDNWLq*ilIe9cg8Zps>)P7YdsBeE|*mN$vr(pX7a{oN12^A;k>D28PhLUv|J|SVcJ#0>;{Z z<8)vQeAWQ&Lo|T*`AIiriJAhU;GajyveGT>JR_12mjQ>i{sM&DvYU!~bJ$y$NrL4u zXloj;U=jBilI=hWyz>y;Z)o@M~i*(n0LOu^&aIe0h8|C>%H5*4_Hlx~#aQNgh)TLP`1q zz0W*I*C%Y`@yEP2GLYa4$jVx%Fh7QBNjvKBZQSxPXpzGtt&gWzD-BdwVC*3}4=6J@ zUjsNjFW(6YkExV&G7J zL05nbEFWr+2MgSHfM@`Yc5_O@Nu&5t9c@`~%}(ito*34?P>7f-r{giG#pG0Hy=arU z$NLdJ_ahW@l{b0f=u)TxW=4Q|5*4igar^=_IZ`q=|GbCwRaTxJ-1I=4B4?zP6U$!2 zN|XG+4YJg4j~OfdT|lK^{s4{UNqJ#4=iHZ@l{exs)*F!lYXBKL#A^9UhC^W;Vq6&FZoeiJ2T!gBtDavDfo3a>fV*UtM; zUthd+&{3=jRs`*9W6T5clU*sJGvb@Gb#;TReE0i*!LssE%kIHYyr%0p@T{NUAIMp~ zn;83c;3?E`$NBg;BfWu2)zeZ~Ze7G~NQrMtTGZS)yE`J$5hB(E%7tQgj4xEXUIZ`N z{H>9)RfjDte$^wm#7mMV4X@FD>|jU=SUAcXdwCvSOpo4#f{ipctd7KQvg`RuK-Zz1 z7o~ZizMZ5~4Ehd3I8bfb<%8L?RkNGF0E^bPOTMWs7NWdXT`s>iA=P>y5g+134*JvU z&wmHJjp5g`M3M-Euv#y$$bH`0EdY-|#HC#6jk|lk$t0ou@qQX=L-RP#@!NS}#OLiE z5*p(X8Fk{o`Ih5V4Th+-)n*-Pmn-fd&2<0DJn?beC5ebz#52f(N{-ByiR;=4R#RZ) zMWspP;-T1A!eMRU{g1wL$bt2Nygy|t-)$`J4k*NdlknEur<@%c5uJ8@ z_QCO`BR!V@BMVhTxN{7lIBT9Gr`z)V67yV$hao)utbKt+Wv}!eR;?z9>~87Z!_j^Y zTFl9Ba++t{5x6`BT~;UUhhQkbX1)UgOHw#Q#{k-T3!^xC8M9%&+f#yN%R=qVM1S=-_@Xug`Qx;A3Ve7@F{$}OlsvGma{rq_uyje&zVgz`3e5(PV2KKX#@~pY~cys8|tdC=8EUd9p zO&FS-)&hPm1fAg?}iIL8)KXbucosv4F$#j=jge2 zOY&JduPPWglv?RDqZb)-jl3Mr<=M96!15ZNQCg4~^-Pb$(}C6rE*YNU>q_DxA(*3u z4>p;K60aYvN@V6=pGmGE3JDg8+q~U20-NBZc6iK@RQPVKnE!S1yjj= z8!nc_$6q-OXtiY;7&mH?`lrCd2Hu~EKRhVy!06b$b@K=C<^XZYMMWbkWr_waHnU>P zq)(k9c>^j;_8z}V*)`_^qSLIY6h0HwrsKD-+I$aldlDIucCZ$;qsWZ;0JJ0I9$p6a zpadr}YYGL{R;(0Z<=&AgRT7)qf3NMBv|-=pAwI1K=bs9XU4IWs)n~AO`!l|3flIldOgm!>p*>Z5Y-A$$S6w1aJMeJFZhll}lU-~SMJZ78A zQ1K`?C5&i)qlv{Np&^M#*i#uYaED8upN`X-RvI*w!i1Mh059&Jv`E<0_2APNen_g?hXrn1o82=-5p&$4 z`yG&AXu2yne%IX3_Yx=$>ce8i{@)4x6m^te%-rWJN{7jrfFn6lAG`*PDvP4yqj%#x zGK)r*W|Yz^PgQ1bcI#aX#%So+6)4{sEmVU&cY3t6W<V;2&RV?Z5#)R~=tENfB(b6hM z7AXtbrefCpX@^`v?y(ig`Vc~V+ojX8t9ZT=Mc;k(xrxtU@bxgMOjq-a z!C?c@ZQN8zyQ;TI;_ICcWb+;%mbIp&Lj^9*ETay=>itI9{JQ<>E_eU^^t~iH^4*=V z$M0pivXu8)grwPCX2~k9PPaF1^S&YSNX3CKu!n;KW`%W{iGqLj%Uk;e?ES{<64g25 zVovYQ`TDUx%ECNw91$5G#+y-di~RKLs|**q=C<4mTs$&=+pZLIXkOvedZ zEvA;X*}Aopq#SnaM8@E^vrZ>ZpFPr;26K{{91@9k@RRvHIW>DaAwM1-;sC?%8zivBJ8gXYQHC~3SXyW_NqE828(oMYB7)6VHM>a z0&d8cW?B{cZnG9Ct7nLtgq%MhWR4%0ls=A)@BDNyCA<534O)STf42Ku|4xXRDwiU$20t#X-Ffg)jWj6rHdAjue;+thpzP$lKtdowSwy_E(2Z1DN>y#joA1IClC&(@#=tFgWz~9q0Y)Bw;o zH#ce$bM3RRisA!(s;XAUb0$d0aK+AA`Q2CB$DI-TL`K34UbI(M_DeZIQw?tKA)@1R z(OQkTc_iMaD&y@(jF>LOZ<^IjRqr{o{i%qAl$ESWvz56Q^vFsjISK!iHONx4bvoAa zO5^V7yC7jQ`YqQ{E{IBNlbL@o0TaEigi(R_L{{veJ1(>N;)&sFzOiBD+2uM>hC17e zdSlb^V{21Wvp5`|(k?=JO6D8?+IuMHrQ`=y-S*GSZO>+We}{u&rfzZCFfZ!>8WiG& z?QiCHi+c5HDuFrvJ_^!kk*DlpVR$Hi5Y%C{H|cYZ8^3_#^BWe$p#}l~xQ~T6styT(p~czt}~l(sV0&<-rMb3%^JR_NXCZ#>Cs$ z@G9`LMnp>(cb;qfgnbegKSyBS?`lj5d#+&Z?tgC!l*qu`Sp2lRB-BVW5Ae`d=zDq4 zZVpJtOcLTBj)2(V4`go!Yq{x^bVLuLY^+P5ut#%{+ZRH-M4+VYbw~XXH}{uGOvSpd zc>#ODDvp5`&y$wXg44ot!j4*`Vn zUox@48f^!B35aotnYA3Y>_t6sY#uN|&AC?kUwx%p#K3l`u1gg9F!$X>|TKm?}|ZF7O7 ztZ@k%{_$MVylb3l21YI(4lJ^_i}NMuWTPj2k|6mG#j%dV1W`7h6D@x7nTP1oXcZ~c z6`Zht$zZ-L8uVsDTP;@Nh2Tz=YOg`mx3(i<3<;yTX2)3z8jbcovzuW%^s$c_bsMup zgQC1(AQ(5Qp^7)z@ts|MqK*gXb+8&<#Wxq;D-WFV?dZ}(u=iAf0437y75T8|SEr3v z9>6;1c%-I99utwFS(>|i9wvHz|E5FOQH8Wnb=S@IJ!j9QAV_Zmb)YSSG96qE8gu}5 z=XhhSjXSYl5XFoVrs=4RMK9}mag#4F;NO94EyTyfReL&+lv!J#c;>Kfd3UnQQGLN@ zs3l~$RWPkWu@P>6PEp17l@#Un?4#G)FWgZQZy#Cc; z#mtGA6jf4|ltGQ-G`W2BUNe;&}YTFv|OqGEz?_Q^>3s5iBhc|OtZW;Dm<>-J{ws=wxR*ZtW zF{t#<)Sw}T`3$6UsMfM@t~ClZt`?Ao`QGdRe6U~W5j!}4fuxYqYE>#jYUajGNnfO7 zDt#1S6+Y#tf3a70W^MEPTlvSQMTa&n=6Ob#bg|yK4S(TmkW#cO?W5T~<$t#IKD7x* zDx`32a~pzJsX?ty<2pOv?QqIXXGP;f)7BcIr-xyt2neKb!tax!QfcBe<}y5eZ}w73 z@oG|d!sW@43O%b1TRyfhlAb@&);%utH*7XYBjlO3y{)%-@%r!EY%B@bN9KM>{{=>_ BO?ChP literal 0 HcmV?d00001 diff --git a/examples/AndroidApp/Resources/values/ic_launcher_background.xml b/examples/AndroidApp/Resources/values/ic_launcher_background.xml new file mode 100644 index 0000000..6ec24e6 --- /dev/null +++ b/examples/AndroidApp/Resources/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #2C3E50 + \ No newline at end of file diff --git a/examples/AndroidApp/Resources/values/strings.xml b/examples/AndroidApp/Resources/values/strings.xml new file mode 100644 index 0000000..57eb010 --- /dev/null +++ b/examples/AndroidApp/Resources/values/strings.xml @@ -0,0 +1,4 @@ + + AndroidApp + Hello, Android! + diff --git a/examples/AndroidTest/AndroidTest.csproj b/examples/AndroidTest/AndroidTest.csproj new file mode 100644 index 0000000..9b70c61 --- /dev/null +++ b/examples/AndroidTest/AndroidTest.csproj @@ -0,0 +1,71 @@ + + + + net10.0-android;net10.0-ios;net10.0-maccatalyst + $(TargetFrameworks);net10.0-windows10.0.19041.0 + + + + + + + Exe + AndroidTest + true + true + enable + enable + + + AndroidTest + + + com.bcryptnext.androidtest + + + 1.0 + 1 + + 11.0 + 13.1 + 21.0 + 10.0.17763.0 + 10.0.17763.0 + 6.5 + Debug;Release;All + AnyCPU + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/AndroidTest/App.xaml b/examples/AndroidTest/App.xaml new file mode 100644 index 0000000..af08f2b --- /dev/null +++ b/examples/AndroidTest/App.xaml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/examples/AndroidTest/App.xaml.cs b/examples/AndroidTest/App.xaml.cs new file mode 100644 index 0000000..b3f0e1f --- /dev/null +++ b/examples/AndroidTest/App.xaml.cs @@ -0,0 +1,31 @@ +// /* +// The MIT License (MIT) +// Copyright (c) 2006 Damien Miller djm@mindrot.org (jBCrypt) +// Copyright (c) 2013 Ryan D. Emerle (.Net port) +// Copyright (c) 2016/2025 Chris McKee (.Net-core port / patches / new features) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +// (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, +// merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished +// to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// */ + +namespace AndroidTest +{ + public partial class App : Application + { + public App() + { + InitializeComponent(); + + MainPage = new AppShell(); + } + } +} diff --git a/examples/AndroidTest/AppShell.xaml b/examples/AndroidTest/AppShell.xaml new file mode 100644 index 0000000..0f22c85 --- /dev/null +++ b/examples/AndroidTest/AppShell.xaml @@ -0,0 +1,15 @@ + + + + + + diff --git a/examples/AndroidTest/AppShell.xaml.cs b/examples/AndroidTest/AppShell.xaml.cs new file mode 100644 index 0000000..2a5de42 --- /dev/null +++ b/examples/AndroidTest/AppShell.xaml.cs @@ -0,0 +1,29 @@ +// /* +// The MIT License (MIT) +// Copyright (c) 2006 Damien Miller djm@mindrot.org (jBCrypt) +// Copyright (c) 2013 Ryan D. Emerle (.Net port) +// Copyright (c) 2016/2025 Chris McKee (.Net-core port / patches / new features) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files +// (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, +// merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished +// to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// */ + +namespace AndroidTest +{ + public partial class AppShell : Shell + { + public AppShell() + { + InitializeComponent(); + } + } +} diff --git a/examples/AndroidTest/MainPage.xaml b/examples/AndroidTest/MainPage.xaml new file mode 100644 index 0000000..2799e0b --- /dev/null +++ b/examples/AndroidTest/MainPage.xaml @@ -0,0 +1,43 @@ + + + + + + + +