From bec47c748e997520a609c6e890f2c1476cbf1f40 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Wed, 23 Apr 2025 20:07:02 +0100 Subject: [PATCH] Bring back support for netstandard2.0 --- src/Directory.Build.props | 2 +- src/MongoDB.Bson/TargetFramework.cs | 2 + .../SigningRSAESPKCSCallback.cs | 2 +- .../ScramSha/ScramSha1Algorithm.cs | 8 + .../ScramSha/ScramSha256Algorithm.cs | 8 + .../Vendored/CryptographyHelpers.cs | 175 +++++++++ .../Vendored/Rfc2898DerivedBytes.cs | 336 ++++++++++++++++++ src/MongoDB.Driver/TargetFramework.cs | 2 + tests/BuildProps/Tests.Build.props | 1 + 9 files changed, 534 insertions(+), 2 deletions(-) create mode 100644 src/MongoDB.Driver/Authentication/Vendored/CryptographyHelpers.cs create mode 100644 src/MongoDB.Driver/Authentication/Vendored/Rfc2898DerivedBytes.cs diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 8f6aedf3237..26c45effadf 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -4,7 +4,7 @@ - netstandard2.1;net472;net6.0 + netstandard2.0;netstandard2.1;net6.0 12.0 true ..\..\MongoDB.ruleset diff --git a/src/MongoDB.Bson/TargetFramework.cs b/src/MongoDB.Bson/TargetFramework.cs index 17ed87658f7..d8396e0b632 100644 --- a/src/MongoDB.Bson/TargetFramework.cs +++ b/src/MongoDB.Bson/TargetFramework.cs @@ -22,6 +22,8 @@ internal static class TargetFramework "net472"; #elif NETSTANDARD2_1 "netstandard21"; +#elif NETSTANDARD2_0 + "netstandard20"; #elif NET6_0 "net60"; #endif diff --git a/src/MongoDB.Driver.Encryption/SigningRSAESPKCSCallback.cs b/src/MongoDB.Driver.Encryption/SigningRSAESPKCSCallback.cs index 62401350732..ba74bef6446 100644 --- a/src/MongoDB.Driver.Encryption/SigningRSAESPKCSCallback.cs +++ b/src/MongoDB.Driver.Encryption/SigningRSAESPKCSCallback.cs @@ -58,7 +58,7 @@ public static bool RsaSign( #pragma warning disable CA1801 public static byte[] HashAndSignBytes(byte[] dataToSign, byte[] key) { -#if !NET472 +#if !NETSTANDARD2_0 using (var rsaProvider = new RSACryptoServiceProvider()) { rsaProvider.ImportPkcs8PrivateKey(key, out _); diff --git a/src/MongoDB.Driver/Authentication/ScramSha/ScramSha1Algorithm.cs b/src/MongoDB.Driver/Authentication/ScramSha/ScramSha1Algorithm.cs index 943cd7d78ce..3775bfa27cc 100644 --- a/src/MongoDB.Driver/Authentication/ScramSha/ScramSha1Algorithm.cs +++ b/src/MongoDB.Driver/Authentication/ScramSha/ScramSha1Algorithm.cs @@ -16,6 +16,14 @@ using System.Security.Cryptography; using System.Text; +// Use our vendored version of Rfc2898DeriveBytes for .NET Standard 2.0 +// because this target does not support a version of Rfc2898DeriveBytes that allows to specify the hash algorithm +#if NETSTANDARD2_0 +using Rfc2898DeriveBytes = MongoDB.Driver.Authentication.Vendored.Rfc2898DeriveBytes; +#else +using Rfc2898DeriveBytes = System.Security.Cryptography.Rfc2898DeriveBytes; +#endif + namespace MongoDB.Driver.Authentication.ScramSha { internal sealed class ScramSha1Algorithm : IScramShaAlgorithm diff --git a/src/MongoDB.Driver/Authentication/ScramSha/ScramSha256Algorithm.cs b/src/MongoDB.Driver/Authentication/ScramSha/ScramSha256Algorithm.cs index 28cdc99f655..8608929c841 100644 --- a/src/MongoDB.Driver/Authentication/ScramSha/ScramSha256Algorithm.cs +++ b/src/MongoDB.Driver/Authentication/ScramSha/ScramSha256Algorithm.cs @@ -19,6 +19,14 @@ using System.Text; using MongoDB.Bson.IO; +// Use our vendored version of Rfc2898DeriveBytes for .NET Standard 2.0 +// because this target does not support a version of Rfc2898DeriveBytes that allows to specify the hash algorithm +#if NETSTANDARD2_0 +using Rfc2898DeriveBytes = MongoDB.Driver.Authentication.Vendored.Rfc2898DeriveBytes; +#else +using Rfc2898DeriveBytes = System.Security.Cryptography.Rfc2898DeriveBytes; +#endif + namespace MongoDB.Driver.Authentication.ScramSha { internal sealed class ScramSha256Algorithm : IScramShaAlgorithm diff --git a/src/MongoDB.Driver/Authentication/Vendored/CryptographyHelpers.cs b/src/MongoDB.Driver/Authentication/Vendored/CryptographyHelpers.cs new file mode 100644 index 00000000000..37cc1a8b031 --- /dev/null +++ b/src/MongoDB.Driver/Authentication/Vendored/CryptographyHelpers.cs @@ -0,0 +1,175 @@ +/* Original work: + * Copyright 2016 .NET Foundation and Contributors + * + * The MIT License (MIT) + * + * Copyright (c) .NET Foundation and Contributors + * + * All rights reserved. + * + * 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. + * + * Modified work: + * Copyright 2018–present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +using System.Diagnostics; +using System.Security.Cryptography; + +namespace MongoDB.Driver.Authentication.Vendored +{ + internal static class CryptographyHelpers + { + + private static readonly string Cryptography_MissingIV + = "The cipher mode specified requires that an initialization vector (IV) be used."; + + public static byte[] CloneByteArray(this byte[] src) + { + if (src == null) + { + return null; + } + + return (byte[])(src.Clone()); + } + + public static KeySizes[] CloneKeySizesArray(this KeySizes[] src) + { + return (KeySizes[])(src.Clone()); + } + + public static bool UsesIv(this CipherMode cipherMode) + { + return cipherMode != CipherMode.ECB; + } + + public static byte[] GetCipherIv(this CipherMode cipherMode, byte[] iv) + { + if (cipherMode.UsesIv()) + { + if (iv == null) + { + throw new CryptographicException(Cryptography_MissingIV); + } + + return iv; + } + + return null; + } + + public static bool IsLegalSize(this int size, KeySizes[] legalSizes) + { + for (int i = 0; i < legalSizes.Length; i++) + { + KeySizes currentSizes = legalSizes[i]; + + // If a cipher has only one valid key size, MinSize == MaxSize and SkipSize will be 0 + if (currentSizes.SkipSize == 0) + { + if (currentSizes.MinSize == size) + return true; + } + else if (size >= currentSizes.MinSize && size <= currentSizes.MaxSize) + { + // If the number is in range, check to see if it's a legal increment above MinSize + int delta = size - currentSizes.MinSize; + + // While it would be unusual to see KeySizes { 10, 20, 5 } and { 11, 14, 1 }, it could happen. + // So don't return false just because this one doesn't match. + if (delta % currentSizes.SkipSize == 0) + { + return true; + } + } + } + return false; + } + + public static byte[] GenerateRandom(int count) + { + byte[] buffer = new byte[count]; + using (RandomNumberGenerator rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(buffer); + } + return buffer; + } + + // encodes the integer i into a 4-byte array, in big endian. + public static void WriteInt(uint i, byte[] arr, int offset) + { + unchecked + { + Debug.Assert(arr != null); + Debug.Assert(arr.Length >= offset + sizeof(uint)); + + arr[offset] = (byte)(i >> 24); + arr[offset + 1] = (byte)(i >> 16); + arr[offset + 2] = (byte)(i >> 8); + arr[offset + 3] = (byte)i; + } + } + + public static byte[] FixupKeyParity(this byte[] key) + { + byte[] oddParityKey = new byte[key.Length]; + for (int index = 0; index < key.Length; index++) + { + // Get the bits we are interested in + oddParityKey[index] = (byte)(key[index] & 0xfe); + + // Get the parity of the sum of the previous bits + byte tmp1 = (byte)((oddParityKey[index] & 0xF) ^ (oddParityKey[index] >> 4)); + byte tmp2 = (byte)((tmp1 & 0x3) ^ (tmp1 >> 2)); + byte sumBitsMod2 = (byte)((tmp2 & 0x1) ^ (tmp2 >> 1)); + + // We need to set the last bit in oddParityKey[index] to the negation + // of the last bit in sumBitsMod2 + if (sumBitsMod2 == 0) + oddParityKey[index] |= 1; + } + return oddParityKey; + } + + internal static void ConvertIntToByteArray(uint value, byte[] dest) + { + Debug.Assert(dest != null); + Debug.Assert(dest.Length == 4); + dest[0] = (byte)((value & 0xFF000000) >> 24); + dest[1] = (byte)((value & 0xFF0000) >> 16); + dest[2] = (byte)((value & 0xFF00) >> 8); + dest[3] = (byte)(value & 0xFF); + } + } +} diff --git a/src/MongoDB.Driver/Authentication/Vendored/Rfc2898DerivedBytes.cs b/src/MongoDB.Driver/Authentication/Vendored/Rfc2898DerivedBytes.cs new file mode 100644 index 00000000000..6d489823fa0 --- /dev/null +++ b/src/MongoDB.Driver/Authentication/Vendored/Rfc2898DerivedBytes.cs @@ -0,0 +1,336 @@ +/* Original work: + * Copyright 2016 .NET Foundation and Contributors + * + * The MIT License (MIT) + * + * Copyright (c) .NET Foundation and Contributors + * + * All rights reserved. + * + * 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. + * + * Modified work: + * Copyright 2018–present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Diagnostics; +using System.Security.Cryptography; +using System.Text; + +namespace MongoDB.Driver.Authentication.Vendored +{ + internal class Rfc2898DeriveBytes : DeriveBytes + { + private static readonly string Cryptography_PasswordDerivedBytes_FewBytesSalt + = "Salt is not at least eight bytes."; + private static readonly string ArgumentOutOfRange_NeedPosNum + = "Positive number required."; + + private static readonly string Cryptography_HashAlgorithmNameNullOrEmpty + = "The hash algorithm name cannot be null or empty."; + + private static readonly string ArgumentOutOfRange_NeedNonNegNum + = "Non-negative number required."; + + private static readonly string Cryptography_UnknownHashAlgorithm + = "'{0}' is not a known hash algorithm."; + + private const int MinimumSaltSize = 8; + + private readonly byte[] _password; + private byte[] _salt; + private uint _iterations; + private HMAC _hmac; + private int _blockSize; + + private byte[] _buffer; + private uint _block; + private int _startIndex; + private int _endIndex; + + public HashAlgorithmName HashAlgorithm { get; } + + public Rfc2898DeriveBytes(byte[] password, byte[] salt, int iterations) + : this(password, salt, iterations, HashAlgorithmName.SHA1) + { + } + + public Rfc2898DeriveBytes(byte[] password, byte[] salt, int iterations, HashAlgorithmName hashAlgorithm) + { + if (salt == null) + throw new ArgumentNullException(nameof(salt)); + if (salt.Length < MinimumSaltSize) + throw new ArgumentException(Cryptography_PasswordDerivedBytes_FewBytesSalt, nameof(salt)); + if (iterations <= 0) + throw new ArgumentOutOfRangeException(nameof(iterations), ArgumentOutOfRange_NeedPosNum); + if (password == null) + throw new NullReferenceException(); // This "should" be ArgumentNullException but for compat, we throw NullReferenceException. + + _salt = salt.CloneByteArray(); + _iterations = (uint)iterations; + _password = password.CloneByteArray(); + HashAlgorithm = hashAlgorithm; + _hmac = OpenHmac(); + // _blockSize is in bytes, HashSize is in bits. + _blockSize = _hmac.HashSize >> 3; + + Initialize(); + } + + public Rfc2898DeriveBytes(string password, byte[] salt) + : this(password, salt, 1000) + { + } + + public Rfc2898DeriveBytes(string password, byte[] salt, int iterations) + : this(password, salt, iterations, HashAlgorithmName.SHA1) + { + } + + public Rfc2898DeriveBytes(string password, byte[] salt, int iterations, HashAlgorithmName hashAlgorithm) + : this(Encoding.UTF8.GetBytes(password), salt, iterations, hashAlgorithm) + { + } + + public Rfc2898DeriveBytes(string password, int saltSize) + : this(password, saltSize, 1000) + { + } + + public Rfc2898DeriveBytes(string password, int saltSize, int iterations) + : this(password, saltSize, iterations, HashAlgorithmName.SHA1) + { + } + + public Rfc2898DeriveBytes(string password, int saltSize, int iterations, HashAlgorithmName hashAlgorithm) + { + if (saltSize < 0) + throw new ArgumentOutOfRangeException(nameof(saltSize), ArgumentOutOfRange_NeedNonNegNum); + if (saltSize < MinimumSaltSize) + throw new ArgumentException(Cryptography_PasswordDerivedBytes_FewBytesSalt, nameof(saltSize)); + if (iterations <= 0) + throw new ArgumentOutOfRangeException(nameof(iterations), ArgumentOutOfRange_NeedPosNum); + + _salt = CryptographyHelpers.GenerateRandom(saltSize); + _iterations = (uint)iterations; + _password = Encoding.UTF8.GetBytes(password); + HashAlgorithm = hashAlgorithm; + _hmac = OpenHmac(); + // _blockSize is in bytes, HashSize is in bits. + _blockSize = _hmac.HashSize >> 3; + + Initialize(); + } + + public int IterationCount + { + get + { + return (int)_iterations; + } + + set + { + if (value <= 0) + throw new ArgumentOutOfRangeException(nameof(value), ArgumentOutOfRange_NeedPosNum); + _iterations = (uint)value; + Initialize(); + } + } + + public byte[] Salt + { + get + { + return _salt.CloneByteArray(); + } + + set + { + if (value == null) + throw new ArgumentNullException(nameof(value)); + if (value.Length < MinimumSaltSize) + throw new ArgumentException(Cryptography_PasswordDerivedBytes_FewBytesSalt); + _salt = value.CloneByteArray(); + Initialize(); + } + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (_hmac != null) + { + _hmac.Dispose(); + _hmac = null; + } + + if (_buffer != null) + Array.Clear(_buffer, 0, _buffer.Length); + if (_password != null) + Array.Clear(_password, 0, _password.Length); + if (_salt != null) + Array.Clear(_salt, 0, _salt.Length); + } + base.Dispose(disposing); + } + + public override byte[] GetBytes(int cb) + { + Debug.Assert(_blockSize > 0); + + if (cb <= 0) + throw new ArgumentOutOfRangeException(nameof(cb), ArgumentOutOfRange_NeedPosNum); + byte[] password = new byte[cb]; + + int offset = 0; + int size = _endIndex - _startIndex; + if (size > 0) + { + if (cb >= size) + { + Buffer.BlockCopy(_buffer, _startIndex, password, 0, size); + _startIndex = _endIndex = 0; + offset += size; + } + else + { + Buffer.BlockCopy(_buffer, _startIndex, password, 0, cb); + _startIndex += cb; + return password; + } + } + + Debug.Assert(_startIndex == 0 && _endIndex == 0, "Invalid start or end index in the internal buffer."); + + while (offset < cb) + { + byte[] T_block = Func(); + int remainder = cb - offset; + if (remainder > _blockSize) + { + Buffer.BlockCopy(T_block, 0, password, offset, _blockSize); + offset += _blockSize; + } + else + { + Buffer.BlockCopy(T_block, 0, password, offset, remainder); + offset += remainder; + Buffer.BlockCopy(T_block, remainder, _buffer, _startIndex, _blockSize - remainder); + _endIndex += (_blockSize - remainder); + return password; + } + } + return password; + } + + public byte[] CryptDeriveKey(string algname, string alghashname, int keySize, byte[] rgbIV) + { + // If this were to be implemented here, CAPI would need to be used (not CNG) because of + // unfortunate differences between the two. Using CNG would break compatibility. Since this + // assembly currently doesn't use CAPI it would require non-trivial additions. + // In addition, if implemented here, only Windows would be supported as it is intended as + // a thin wrapper over the corresponding native API. + // Note that this method is implemented in PasswordDeriveBytes (in the Csp assembly) using CAPI. + throw new PlatformNotSupportedException(); + } + + public override void Reset() + { + Initialize(); + } + + private static string Format(string resourceFormat, params object[] args) + { + return args != null ? string.Format(resourceFormat, args) : resourceFormat; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA5350", Justification = "HMACSHA1 is needed for compat. (https://github.com/dotnet/corefx/issues/9438)")] + private HMAC OpenHmac() + { + Debug.Assert(_password != null); + + HashAlgorithmName hashAlgorithm = HashAlgorithm; + + if (string.IsNullOrEmpty(hashAlgorithm.Name)) + throw new CryptographicException(Cryptography_HashAlgorithmNameNullOrEmpty); + + if (hashAlgorithm == HashAlgorithmName.SHA1) + return new HMACSHA1(_password); + if (hashAlgorithm == HashAlgorithmName.SHA256) + return new HMACSHA256(_password); + if (hashAlgorithm == HashAlgorithmName.SHA384) + return new HMACSHA384(_password); + if (hashAlgorithm == HashAlgorithmName.SHA512) + return new HMACSHA512(_password); + + throw new CryptographicException(Format(Cryptography_UnknownHashAlgorithm, hashAlgorithm.Name)); + } + + private void Initialize() + { + if (_buffer != null) + Array.Clear(_buffer, 0, _buffer.Length); + _buffer = new byte[_blockSize]; + _block = 1; + _startIndex = _endIndex = 0; + } + + // This function is defined as follows: + // Func (S, i) = HMAC(S || i) | HMAC2(S || i) | ... | HMAC(iterations) (S || i) + // where i is the block number. + private byte[] Func() + { + byte[] temp = new byte[_salt.Length + sizeof(uint)]; + Buffer.BlockCopy(_salt, 0, temp, 0, _salt.Length); + CryptographyHelpers.WriteInt(_block, temp, _salt.Length); + + temp = _hmac.ComputeHash(temp); + + byte[] ret = temp; + for (int i = 2; i <= _iterations; i++) + { + temp = _hmac.ComputeHash(temp); + + for (int j = 0; j < _blockSize; j++) + { + ret[j] ^= temp[j]; + } + } + + // increment the block count. + _block++; + return ret; + } + } +} diff --git a/src/MongoDB.Driver/TargetFramework.cs b/src/MongoDB.Driver/TargetFramework.cs index 4b42204f12d..8ca10bd0536 100644 --- a/src/MongoDB.Driver/TargetFramework.cs +++ b/src/MongoDB.Driver/TargetFramework.cs @@ -22,6 +22,8 @@ internal static class TargetFramework "net472"; #elif NETSTANDARD2_1 "netstandard21"; +#elif NETSTANDARD2_0 + "netstandard20"; #elif NET6_0 "net60"; #endif diff --git a/tests/BuildProps/Tests.Build.props b/tests/BuildProps/Tests.Build.props index a14112ae334..feff7fa6ec9 100644 --- a/tests/BuildProps/Tests.Build.props +++ b/tests/BuildProps/Tests.Build.props @@ -46,6 +46,7 @@ +