diff --git a/src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.Pbkdf2.cs b/src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.Pbkdf2.cs new file mode 100644 index 00000000000000..4eb5bfd57edfae --- /dev/null +++ b/src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.Pbkdf2.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Security.Cryptography; + +internal static partial class Interop +{ + internal static partial class AppleCrypto + { + internal static unsafe void Pbkdf2( + PAL_HashAlgorithm prfAlgorithm, + ReadOnlySpan password, + ReadOnlySpan salt, + int iterations, + Span destination) + { + fixed (byte* pPassword = password) + fixed (byte* pSalt = salt) + fixed (byte* pDestination = destination) + { + int ret = AppleCryptoNative_Pbkdf2( + prfAlgorithm, + pPassword, + password.Length, + pSalt, + salt.Length, + iterations, + pDestination, + destination.Length, + out int ccStatus); + + if (ret == 0) + { + throw Interop.AppleCrypto.CreateExceptionForCCError( + ccStatus, + Interop.AppleCrypto.CCCryptorStatus); + } + + if (ret != 1) + { + Debug.Fail($"Pbkdf2 failed with invalid input {ret}"); + throw new CryptographicException(); + } + } + } + + [DllImport(Libraries.AppleCryptoNative)] + private static extern unsafe int AppleCryptoNative_Pbkdf2( + PAL_HashAlgorithm prfAlgorithm, + byte* password, + int passwordLen, + byte* salt, + int saltLen, + int iterations, + byte* derivedKey, + int derivedKeyLen, + out int errorCode); + } +} diff --git a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EVP.cs b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EVP.cs index ab184581dbcbf6..45e3cce6715d94 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EVP.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EVP.cs @@ -51,10 +51,43 @@ internal static int EvpDigestUpdate(SafeEvpMdCtxHandle ctx, ReadOnlySpan d [DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_EvpSha512")] internal static extern IntPtr EvpSha512(); - [DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_GetMaxMdSize")] private static extern int GetMaxMdSize(); + [DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_Pbkdf2")] + private static unsafe extern int Pbkdf2( + byte* pPassword, + int passwordLength, + byte* pSalt, + int saltLength, + int iterations, + IntPtr digestEvp, + byte* pDestination, + int destinationLength); + + internal static unsafe int Pbkdf2( + ReadOnlySpan password, + ReadOnlySpan salt, + int iterations, + IntPtr digestEvp, + Span destination) + { + fixed (byte* pPassword = password) + fixed (byte* pSalt = salt) + fixed (byte* pDestination = destination) + { + return Pbkdf2( + pPassword, + password.Length, + pSalt, + salt.Length, + iterations, + digestEvp, + pDestination, + destination.Length); + } + } + internal static readonly int EVP_MAX_MD_SIZE = GetMaxMdSize(); } } diff --git a/src/libraries/Common/src/Interop/Windows/BCrypt/Cng.cs b/src/libraries/Common/src/Interop/Windows/BCrypt/Cng.cs index 3163dceeda0d29..7642b9faeafa5a 100644 --- a/src/libraries/Common/src/Interop/Windows/BCrypt/Cng.cs +++ b/src/libraries/Common/src/Interop/Windows/BCrypt/Cng.cs @@ -35,6 +35,7 @@ internal static class AlgorithmName public const string Sha256 = "SHA256"; // BCRYPT_SHA256_ALGORITHM public const string Sha384 = "SHA384"; // BCRYPT_SHA384_ALGORITHM public const string Sha512 = "SHA512"; // BCRYPT_SHA512_ALGORITHM + public const string Pbkdf2 = "PBKDF2"; // BCRYPT_PBKDF2_ALGORITHM } internal static class KeyDerivationFunction diff --git a/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptAlgPseudoHandle.cs b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptAlgPseudoHandle.cs new file mode 100644 index 00000000000000..8ebae440952617 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptAlgPseudoHandle.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +internal partial class Interop +{ + internal partial class BCrypt + { + // Pseudo-handles, as defined in bcrypt.h + // TODO: This really should be backed by 'nuint' (see https://github.com/dotnet/roslyn/issues/44110) + public enum BCryptAlgPseudoHandle : uint + { + BCRYPT_MD5_ALG_HANDLE = 0x00000021, + BCRYPT_SHA1_ALG_HANDLE = 0x00000031, + BCRYPT_SHA256_ALG_HANDLE = 0x00000041, + BCRYPT_SHA384_ALG_HANDLE = 0x00000051, + BCRYPT_SHA512_ALG_HANDLE = 0x00000061, + BCRYPT_PBKDF2_ALG_HANDLE = 0x00000331, + } + } +} diff --git a/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptDeriveKeyPBKDF2.cs b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptDeriveKeyPBKDF2.cs new file mode 100644 index 00000000000000..d514f2b8e5471e --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptDeriveKeyPBKDF2.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +using Microsoft.Win32.SafeHandles; + +internal partial class Interop +{ + internal partial class BCrypt + { + [DllImport(Libraries.BCrypt, CharSet = CharSet.Unicode)] + internal static extern unsafe NTSTATUS BCryptDeriveKeyPBKDF2( + SafeBCryptAlgorithmHandle hPrf, + byte* pbPassword, + int cbPassword, + byte* pbSalt, + int cbSalt, + ulong cIterations, + byte* pbDerivedKey, + int cbDerivedKey, + uint dwFlags); + } +} diff --git a/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptGenerateSymmetricKey.cs b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptGenerateSymmetricKey.cs new file mode 100644 index 00000000000000..0f829c5d33ea95 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptGenerateSymmetricKey.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +internal partial class Interop +{ + internal partial class BCrypt + { + [DllImport(Libraries.BCrypt, CharSet = CharSet.Unicode)] + internal static unsafe extern NTSTATUS BCryptGenerateSymmetricKey( + SafeBCryptAlgorithmHandle hAlgorithm, + out SafeBCryptKeyHandle phKey, + IntPtr pbKeyObject, + int cbKeyObject, + byte* pbSecret, + int cbSecret, + uint dwFlags); + + [DllImport(Libraries.BCrypt, CharSet = CharSet.Unicode)] + internal static unsafe extern NTSTATUS BCryptGenerateSymmetricKey( + nuint hAlgorithm, + out SafeBCryptKeyHandle phKey, + IntPtr pbKeyObject, + int cbKeyObject, + byte* pbSecret, + int cbSecret, + uint dwFlags); + } +} diff --git a/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptHash.cs b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptHash.cs index e018062b9c7d1c..530e7b2c97dafc 100644 --- a/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptHash.cs +++ b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptHash.cs @@ -10,16 +10,5 @@ internal partial class BCrypt { [DllImport(Libraries.BCrypt, CharSet = CharSet.Unicode)] internal static unsafe extern NTSTATUS BCryptHash(nuint hAlgorithm, byte* pbSecret, int cbSecret, byte* pbInput, int cbInput, byte* pbOutput, int cbOutput); - - // Pseudo-handles, as defined in bcrypt.h - // TODO: This really should be backed by 'nuint' (see https://github.com/dotnet/roslyn/issues/44110) - public enum BCryptAlgPseudoHandle : uint - { - BCRYPT_MD5_ALG_HANDLE = 0x00000021, - BCRYPT_SHA1_ALG_HANDLE = 0x00000031, - BCRYPT_SHA256_ALG_HANDLE = 0x00000041, - BCRYPT_SHA384_ALG_HANDLE = 0x00000051, - BCRYPT_SHA512_ALG_HANDLE = 0x00000061, - } } } diff --git a/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptKeyDerivation.cs b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptKeyDerivation.cs new file mode 100644 index 00000000000000..dfe2c7d3f32f77 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.BCryptKeyDerivation.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +using Microsoft.Win32.SafeHandles; + +internal partial class Interop +{ + internal partial class BCrypt + { + [DllImport(Libraries.BCrypt, CharSet = CharSet.Unicode)] + internal static unsafe extern NTSTATUS BCryptKeyDerivation( + SafeBCryptKeyHandle hKey, + BCryptBufferDesc* pParameterList, + byte* pbDerivedKey, + int cbDerivedKey, + out uint pcbResult, + int dwFlags); + } +} diff --git a/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.Blobs.cs b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.Blobs.cs index 8caf36e8a17947..374a80eb7c67f8 100644 --- a/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.Blobs.cs +++ b/src/libraries/Common/src/Interop/Windows/BCrypt/Interop.Blobs.cs @@ -227,10 +227,27 @@ internal struct BCRYPT_ECCFULLKEY_BLOB } /// - /// NCrypt buffer descriptors + /// NCrypt or BCrypt buffer descriptors /// - internal enum NCryptBufferDescriptors : int + internal enum CngBufferDescriptors : int { + KDF_HASH_ALGORITHM = 0, + KDF_SECRET_PREPEND = 1, + KDF_SECRET_APPEND = 2, + KDF_HMAC_KEY = 3, + KDF_TLS_PRF_LABEL = 4, + KDF_TLS_PRF_SEED = 5, + KDF_SECRET_HANDLE = 6, + KDF_TLS_PRF_PROTOCOL = 7, + KDF_ALGORITHMID = 8, + KDF_PARTYUINFO = 9, + KDF_PARTYVINFO = 10, + KDF_SUPPPUBINFO = 11, + KDF_SUPPPRIVINFO = 12, + KDF_LABEL = 13, + KDF_CONTEXT = 14, + KDF_SALT = 15, + KDF_ITERATION_COUNT = 16, NCRYPTBUFFER_ECC_CURVE_NAME = 60, } @@ -241,7 +258,7 @@ internal enum NCryptBufferDescriptors : int internal struct BCryptBuffer { internal int cbBuffer; // Length of buffer, in bytes - internal NCryptBufferDescriptors BufferType; // Buffer type + internal CngBufferDescriptors BufferType; // Buffer type internal IntPtr pvBuffer; // Pointer to buffer } diff --git a/src/libraries/Common/src/System/Security/Cryptography/ECCng.ImportExport.cs b/src/libraries/Common/src/System/Security/Cryptography/ECCng.ImportExport.cs index 4064f60f5f4ff4..5e394540dad205 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/ECCng.ImportExport.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/ECCng.ImportExport.cs @@ -474,7 +474,7 @@ internal static SafeNCryptKeyHandle ImportKeyBlob( descPtr = Marshal.AllocHGlobal(Marshal.SizeOf(desc)); buffPtr = Marshal.AllocHGlobal(Marshal.SizeOf(buff)); buff.cbBuffer = (curveName.Length + 1) * 2; // Add 1 for null terminator - buff.BufferType = Interop.BCrypt.NCryptBufferDescriptors.NCRYPTBUFFER_ECC_CURVE_NAME; + buff.BufferType = Interop.BCrypt.CngBufferDescriptors.NCRYPTBUFFER_ECC_CURVE_NAME; buff.pvBuffer = safeCurveName.DangerousGetHandle(); Marshal.StructureToPtr(buff, buffPtr, false); diff --git a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Apple/CMakeLists.txt b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Apple/CMakeLists.txt index e1e4c47a6d861f..a22275331045cb 100644 --- a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Apple/CMakeLists.txt +++ b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Apple/CMakeLists.txt @@ -10,6 +10,7 @@ set(NATIVECRYPTO_SOURCES pal_ecc.c pal_hmac.c pal_keyagree.c + pal_keyderivation.c pal_keychain.c pal_random.c pal_rsa.c diff --git a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Apple/entrypoints.c b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Apple/entrypoints.c index 3c00bb540e2ce4..43f76ca4028236 100644 --- a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Apple/entrypoints.c +++ b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Apple/entrypoints.c @@ -20,6 +20,7 @@ #include "pal_trust.h" #include "pal_x509.h" #include "pal_x509chain.h" +#include "pal_keyderivation.h" static const Entry s_cryptoAppleNative[] = { @@ -103,6 +104,7 @@ static const Entry s_cryptoAppleNative[] = DllImportEntry(AppleCryptoNative_X509ChainGetStatusAtIndex) DllImportEntry(AppleCryptoNative_GetOSStatusForChainStatus) DllImportEntry(AppleCryptoNative_X509ChainSetTrustAnchorCertificates) + DllImportEntry(AppleCryptoNative_Pbkdf2) }; EXTERN_C const void* CryptoAppleResolveDllImport(const char* name); diff --git a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Apple/pal_keyderivation.c b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Apple/pal_keyderivation.c new file mode 100644 index 00000000000000..6358fe85085cfe --- /dev/null +++ b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Apple/pal_keyderivation.c @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include "pal_keyderivation.h" + +#if !defined(TARGET_IOS) && !defined(TARGET_TVOS) + +static int32_t PrfAlgorithmFromHashAlgorithm(PAL_HashAlgorithm hashAlgorithm, CCPseudoRandomAlgorithm* algorithm) +{ + if (algorithm == NULL) + return 0; + + switch (hashAlgorithm) + { + case PAL_SHA1: + *algorithm = kCCPRFHmacAlgSHA1; + return 1; + case PAL_SHA256: + *algorithm = kCCPRFHmacAlgSHA256; + return 1; + case PAL_SHA384: + *algorithm = kCCPRFHmacAlgSHA384; + return 1; + case PAL_SHA512: + *algorithm = kCCPRFHmacAlgSHA512; + return 1; + default: + *algorithm = 0; + return 0; + } +} + +int32_t AppleCryptoNative_Pbkdf2(PAL_HashAlgorithm prfAlgorithm, + const char* password, + int32_t passwordLen, + const uint8_t* salt, + int32_t saltLen, + int32_t iterations, + uint8_t* derivedKey, + uint32_t derivedKeyLen, + int32_t* errorCode) +{ + if (errorCode != NULL) + *errorCode = noErr; + + if (passwordLen < 0 || saltLen < 0 || iterations < 0 || derivedKey == NULL || + derivedKeyLen < 0 || errorCode == NULL) + { + return -1; + } + + if (salt == NULL && saltLen != 0) + { + return -1; + } + + const char* empty = ""; + + if (password == NULL) + { + if (passwordLen != 0) + { + return -1; + } + + // macOS will not accept a null password, but it will accept a zero-length + // password with a valid pointer. + password = empty; + } + + CCPseudoRandomAlgorithm prf; + + if (!PrfAlgorithmFromHashAlgorithm(prfAlgorithm, &prf)) + { + return -2; + } + + CCStatus result = CCKeyDerivationPBKDF(kCCPBKDF2, password, passwordLen, salt, + saltLen, prf, iterations, derivedKey, derivedKeyLen); + *errorCode = result; + return result == kCCSuccess ? 1 : 0; +} +#endif diff --git a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Apple/pal_keyderivation.h b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Apple/pal_keyderivation.h new file mode 100644 index 00000000000000..ee9cfae8dc3d1e --- /dev/null +++ b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Apple/pal_keyderivation.h @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma once + +#include "pal_digest.h" +#include + +#if !defined(TARGET_IOS) && !defined(TARGET_TVOS) +/* +Filled the derivedKey buffer with PBKDF2 derived data. + +Implemented by: +1) Validating input +2) Calling CCKeyDerivationPBKDF + +password and salt may be NULL if their respective length parameter +is zero. When password is NULL, it will be replaced with a pointer to an empty +location. + +Returns -1 on invalid input, or -2 if the prfAlgorithm is an unknown +or unsupported hash algorithm. On valid input, the return value +is 1 if successful, and 0 if unsuccessful. + +Returns the result of SecKeychainCreate. + +Output: +errorCode: Contains the CCStatus of the operation. This will contain the +error code when the call is unsuccessful with valid input. +*/ +PALEXPORT int32_t AppleCryptoNative_Pbkdf2(PAL_HashAlgorithm prfAlgorithm, + const char* password, + int32_t passwordLen, + const uint8_t* salt, + int32_t saltLen, + int32_t iterations, + uint8_t* derivedKey, + uint32_t derivedKeyLen, + int32_t* errorCode); +#endif diff --git a/src/libraries/Native/Unix/System.Security.Cryptography.Native/entrypoints.c b/src/libraries/Native/Unix/System.Security.Cryptography.Native/entrypoints.c index 9856a0b10f1f05..bd379df88242e1 100644 --- a/src/libraries/Native/Unix/System.Security.Cryptography.Native/entrypoints.c +++ b/src/libraries/Native/Unix/System.Security.Cryptography.Native/entrypoints.c @@ -209,6 +209,7 @@ static const Entry s_cryptoNative[] = DllImportEntry(CryptoNative_ObjTxt2Obj) DllImportEntry(CryptoNative_OcspRequestDestroy) DllImportEntry(CryptoNative_OcspResponseDestroy) + DllImportEntry(CryptoNative_Pbkdf2) DllImportEntry(CryptoNative_PemReadBioPkcs7) DllImportEntry(CryptoNative_PemReadBioX509Crl) DllImportEntry(CryptoNative_PemReadX509FromBio) diff --git a/src/libraries/Native/Unix/System.Security.Cryptography.Native/opensslshim.h b/src/libraries/Native/Unix/System.Security.Cryptography.Native/opensslshim.h index 9e6fa85a4949e3..5490d1a83666ce 100644 --- a/src/libraries/Native/Unix/System.Security.Cryptography.Native/opensslshim.h +++ b/src/libraries/Native/Unix/System.Security.Cryptography.Native/opensslshim.h @@ -437,6 +437,7 @@ void SSL_get0_alpn_selected(const SSL* ssl, const unsigned char** protocol, unsi REQUIRED_FUNCTION(PEM_read_bio_X509_AUX) \ REQUIRED_FUNCTION(PEM_read_bio_X509_CRL) \ REQUIRED_FUNCTION(PEM_write_bio_X509_CRL) \ + REQUIRED_FUNCTION(PKCS5_PBKDF2_HMAC) \ REQUIRED_FUNCTION(PKCS12_free) \ REQUIRED_FUNCTION(PKCS12_parse) \ REQUIRED_FUNCTION(PKCS7_sign) \ @@ -847,6 +848,7 @@ FOR_ALL_OPENSSL_FUNCTIONS #define PEM_read_bio_X509_AUX PEM_read_bio_X509_AUX_ptr #define PEM_read_bio_X509_CRL PEM_read_bio_X509_CRL_ptr #define PEM_write_bio_X509_CRL PEM_write_bio_X509_CRL_ptr +#define PKCS5_PBKDF2_HMAC PKCS5_PBKDF2_HMAC_ptr #define PKCS12_free PKCS12_free_ptr #define PKCS12_parse PKCS12_parse_ptr #define PKCS7_sign PKCS7_sign_ptr diff --git a/src/libraries/Native/Unix/System.Security.Cryptography.Native/pal_evp.c b/src/libraries/Native/Unix/System.Security.Cryptography.Native/pal_evp.c index 4e67c450e9e888..a119f8178f0017 100644 --- a/src/libraries/Native/Unix/System.Security.Cryptography.Native/pal_evp.c +++ b/src/libraries/Native/Unix/System.Security.Cryptography.Native/pal_evp.c @@ -151,3 +151,44 @@ int32_t CryptoNative_GetMaxMdSize() { return EVP_MAX_MD_SIZE; } + +int32_t CryptoNative_Pbkdf2(const char* password, + int32_t passwordLength, + const unsigned char* salt, + int32_t saltLength, + int32_t iterations, + const EVP_MD* digest, + unsigned char* destination, + int32_t destinationLength) +{ + if (passwordLength < 0 || saltLength < 0 || iterations <= 0 || digest == NULL || + destination == NULL || destinationLength < 0) + { + return -1; + } + + const char* empty = ""; + + if (salt == NULL) + { + if (saltLength != 0) + { + return -1; + } + + salt = (const unsigned char*)empty; + } + + if (password == NULL) + { + if (passwordLength != 0) + { + return -1; + } + + password = empty; + } + + return PKCS5_PBKDF2_HMAC( + password, passwordLength, salt, saltLength, iterations, digest, destinationLength, destination); +} diff --git a/src/libraries/Native/Unix/System.Security.Cryptography.Native/pal_evp.h b/src/libraries/Native/Unix/System.Security.Cryptography.Native/pal_evp.h index 3a546c9f92322c..11c8a167191815 100644 --- a/src/libraries/Native/Unix/System.Security.Cryptography.Native/pal_evp.h +++ b/src/libraries/Native/Unix/System.Security.Cryptography.Native/pal_evp.h @@ -117,6 +117,29 @@ PALEXPORT const EVP_MD* CryptoNative_EvpSha512(void); Function: GetMaxMdSize -Returns the maxium bytes for a message digest. +Returns the maximum bytes for a message digest. */ PALEXPORT int32_t CryptoNative_GetMaxMdSize(void); + +/* +Filled the destination buffer with PBKDF2 derived data. + +Implemented by: +1) Validating input +2) Calling PKCS5_PBKDF2_HMAC + +password and salt may be NULL if their respective length parameters +are zero. When null, it will be replaced with a pointer to an empty +location. + +Returns -1 on invalid input. On valid input, the return value +is the return value of PKCS5_PBKDF2_HMAC. +*/ +PALEXPORT int32_t CryptoNative_Pbkdf2(const char* password, + int32_t passwordLength, + const unsigned char* salt, + int32_t saltLength, + int32_t iterations, + const EVP_MD* digest, + unsigned char* destination, + int32_t destinationLength); diff --git a/src/libraries/System.Security.Cryptography.Algorithms/ref/System.Security.Cryptography.Algorithms.cs b/src/libraries/System.Security.Cryptography.Algorithms/ref/System.Security.Cryptography.Algorithms.cs index 8e9d12de19a4e8..e5a78f387eedf3 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/ref/System.Security.Cryptography.Algorithms.cs +++ b/src/libraries/System.Security.Cryptography.Algorithms/ref/System.Security.Cryptography.Algorithms.cs @@ -587,6 +587,12 @@ public Rfc2898DeriveBytes(string password, int saltSize, int iterations, System. public byte[] CryptDeriveKey(string algname, string alghashname, int keySize, byte[] rgbIV) { throw null; } protected override void Dispose(bool disposing) { } public override byte[] GetBytes(int cb) { throw null; } + public static byte[] Pbkdf2(byte[] password, byte[] salt, int iterations, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, int outputLength) { throw null; } + public static byte[] Pbkdf2(System.ReadOnlySpan password, System.ReadOnlySpan salt, int iterations, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, int outputLength) { throw null; } + public static void Pbkdf2(System.ReadOnlySpan password, System.ReadOnlySpan salt, System.Span destination, int iterations, System.Security.Cryptography.HashAlgorithmName hashAlgorithm) { } + public static byte[] Pbkdf2(System.ReadOnlySpan password, System.ReadOnlySpan salt, int iterations, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, int outputLength) { throw null; } + public static void Pbkdf2(System.ReadOnlySpan password, System.ReadOnlySpan salt, System.Span destination, int iterations, System.Security.Cryptography.HashAlgorithmName hashAlgorithm) { } + public static byte[] Pbkdf2(string password, byte[] salt, int iterations, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, int outputLength) { throw null; } public override void Reset() { } } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/HashProviderDispenser.Unix.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/HashProviderDispenser.Unix.cs index a67223334986f6..21647eca04f53c 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/HashProviderDispenser.Unix.cs +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/HashProviderDispenser.Unix.cs @@ -29,7 +29,7 @@ public static HashProvider CreateMacProvider(string hashAlgorithmId, ReadOnlySpa return new HmacHashProvider(evpType, key); } - private static IntPtr HashAlgorithmToEvp(string hashAlgorithmId) => hashAlgorithmId switch { + public static IntPtr HashAlgorithmToEvp(string hashAlgorithmId) => hashAlgorithmId switch { HashAlgorithmNames.SHA1 => s_evpSha1 == IntPtr.Zero ? (s_evpSha1 = Interop.Crypto.EvpSha1()) : s_evpSha1, HashAlgorithmNames.SHA256 => s_evpSha256 == IntPtr.Zero ? (s_evpSha256 = Interop.Crypto.EvpSha256()) : s_evpSha256, HashAlgorithmNames.SHA384 => s_evpSha384 == IntPtr.Zero ? (s_evpSha384 = Interop.Crypto.EvpSha384()) : s_evpSha384, diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/Pbkdf2Implementation.OSX.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/Pbkdf2Implementation.OSX.cs new file mode 100644 index 00000000000000..f588543899e286 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/Pbkdf2Implementation.OSX.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Security.Cryptography; +using PAL_HashAlgorithm = Interop.AppleCrypto.PAL_HashAlgorithm; + +namespace Internal.Cryptography +{ + internal partial class Pbkdf2Implementation + { + public static unsafe void Fill( + ReadOnlySpan password, + ReadOnlySpan salt, + int iterations, + HashAlgorithmName hashAlgorithmName, + Span destination) + { + Debug.Assert(!destination.IsEmpty); + + PAL_HashAlgorithm prfAlgorithm; + + switch (hashAlgorithmName.Name) + { + case HashAlgorithmNames.SHA1: + prfAlgorithm = PAL_HashAlgorithm.Sha1; + break; + case HashAlgorithmNames.SHA256: + prfAlgorithm = PAL_HashAlgorithm.Sha256; + break; + case HashAlgorithmNames.SHA384: + prfAlgorithm = PAL_HashAlgorithm.Sha384; + break; + case HashAlgorithmNames.SHA512: + prfAlgorithm = PAL_HashAlgorithm.Sha512; + break; + default: + Debug.Fail($"Unexpected hash algorithm '{hashAlgorithmName.Name}'"); + throw new CryptographicException(); + }; + + Interop.AppleCrypto.Pbkdf2(prfAlgorithm, password, salt, iterations, destination); + } + } +} diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/Pbkdf2Implementation.Unix.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/Pbkdf2Implementation.Unix.cs new file mode 100644 index 00000000000000..a4a04b1a6887de --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/Pbkdf2Implementation.Unix.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Security.Cryptography; + +namespace Internal.Cryptography +{ + internal partial class Pbkdf2Implementation + { + public static unsafe void Fill( + ReadOnlySpan password, + ReadOnlySpan salt, + int iterations, + HashAlgorithmName hashAlgorithmName, + Span destination) + { + Debug.Assert(!destination.IsEmpty); + Debug.Assert(hashAlgorithmName.Name is not null); + IntPtr evpHashType = HashProviderDispenser.HashAlgorithmToEvp(hashAlgorithmName.Name); + int result = Interop.Crypto.Pbkdf2(password, salt, iterations, evpHashType, destination); + const int Success = 1; + + if (result != Success) + { + Debug.Assert(result == 0, $"Unexpected result {result}"); + throw Interop.Crypto.CreateOpenSslCryptographicException(); + } + } + } +} diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/Pbkdf2Implementation.Windows.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/Pbkdf2Implementation.Windows.cs new file mode 100644 index 00000000000000..93c0bd55ebb894 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/Pbkdf2Implementation.Windows.cs @@ -0,0 +1,281 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Threading; +using System.Security.Cryptography; +using Microsoft.Win32.SafeHandles; +using BCryptAlgPseudoHandle = Interop.BCrypt.BCryptAlgPseudoHandle; +using BCryptBuffer = Interop.BCrypt.BCryptBuffer; +using BCryptOpenAlgorithmProviderFlags = Interop.BCrypt.BCryptOpenAlgorithmProviderFlags; +using CngBufferDescriptors = Interop.BCrypt.CngBufferDescriptors; +using NTSTATUS = Interop.BCrypt.NTSTATUS; + +namespace Internal.Cryptography +{ + internal partial class Pbkdf2Implementation + { + private static readonly bool s_usePseudoHandles = OperatingSystem.IsWindowsVersionAtLeast(10, 0, 0); + + // For Windows 7 we will use BCryptDeriveKeyPBKDF2. For Windows 8+ we will use BCryptKeyDerivation + // since it has better performance. + private static readonly bool s_useKeyDerivation = OperatingSystem.IsWindowsVersionAtLeast(8, 0, 0); + + // A cached instance of PBKDF2 for Windows 8, where pseudo handles are not supported. + private static SafeBCryptAlgorithmHandle? s_pbkdf2AlgorithmHandle; + + public static unsafe void Fill( + ReadOnlySpan password, + ReadOnlySpan salt, + int iterations, + HashAlgorithmName hashAlgorithmName, + Span destination) + { + Debug.Assert(!destination.IsEmpty); + Debug.Assert(iterations >= 0); + Debug.Assert(hashAlgorithmName.Name is not null); + + if (s_useKeyDerivation) + { + FillKeyDerivation(password, salt, iterations, hashAlgorithmName.Name, destination); + } + else + { + FillDeriveKeyPBKDF2(password, salt, iterations, hashAlgorithmName.Name, destination); + } + } + + private static unsafe void FillKeyDerivation( + ReadOnlySpan password, + ReadOnlySpan salt, + int iterations, + string hashAlgorithmName, + Span destination) + { + SafeBCryptKeyHandle keyHandle; + int hashBlockSizeBytes = GetHashBlockSize(hashAlgorithmName); + + // stackalloc 0 to let compiler know this cannot escape. + Span clearSpan = stackalloc byte[0]; + ReadOnlySpan symmetricKeyMaterial = stackalloc byte[0]; + int symmetricKeyMaterialLength; + + if (password.IsEmpty) + { + // CNG won't accept a null pointer for the password. + symmetricKeyMaterial = stackalloc byte[1]; + symmetricKeyMaterialLength = 0; + clearSpan = default; + } + else if (password.Length <= hashBlockSizeBytes) + { + // Password is small enough to use as-is. + symmetricKeyMaterial = password; + symmetricKeyMaterialLength = password.Length; + clearSpan = default; + } + else + { + // RFC 2104: "The key for HMAC can be of any length (keys longer than B bytes are + // first hashed using H). + // We denote by B the byte-length of such + // blocks (B=64 for all the above mentioned examples of hash functions) + // + // Windows' PBKDF2 will do this up to a point. To ensure we accept arbitrary inputs for + // PBKDF2, we do the hashing ourselves. + Span hashBuffer = stackalloc byte[512 / 8]; // 64 bytes is SHA512, the largest digest handled. + int hashBufferSize; + + switch (hashAlgorithmName) + { + case HashAlgorithmNames.SHA1: + hashBufferSize = SHA1.HashData(password, hashBuffer); + break; + case HashAlgorithmNames.SHA256: + hashBufferSize = SHA256.HashData(password, hashBuffer); + break; + case HashAlgorithmNames.SHA384: + hashBufferSize = SHA384.HashData(password, hashBuffer); + break; + case HashAlgorithmNames.SHA512: + hashBufferSize = SHA512.HashData(password, hashBuffer); + break; + default: + Debug.Fail($"Unexpected hash algorithm '{hashAlgorithmName}'"); + throw new CryptographicException(); + } + + clearSpan = hashBuffer.Slice(0, hashBufferSize); + symmetricKeyMaterial = clearSpan; + symmetricKeyMaterialLength = hashBufferSize; + } + + Debug.Assert(symmetricKeyMaterial.Length > 0); + + NTSTATUS generateKeyStatus; + + if (s_usePseudoHandles) + { + fixed (byte* pSymmetricKeyMaterial = symmetricKeyMaterial) + { + generateKeyStatus = Interop.BCrypt.BCryptGenerateSymmetricKey( + (nuint)BCryptAlgPseudoHandle.BCRYPT_PBKDF2_ALG_HANDLE, + out keyHandle, + pbKeyObject: IntPtr.Zero, + cbKeyObject: 0, + pSymmetricKeyMaterial, + symmetricKeyMaterialLength, + dwFlags: 0); + } + } + else + { + if (s_pbkdf2AlgorithmHandle is null) + { + NTSTATUS openStatus = Interop.BCrypt.BCryptOpenAlgorithmProvider( + out SafeBCryptAlgorithmHandle pbkdf2AlgorithmHandle, + Internal.NativeCrypto.BCryptNative.AlgorithmName.Pbkdf2, + null, + BCryptOpenAlgorithmProviderFlags.None); + + if (openStatus != NTSTATUS.STATUS_SUCCESS) + { + pbkdf2AlgorithmHandle.Dispose(); + CryptographicOperations.ZeroMemory(clearSpan); + throw Interop.BCrypt.CreateCryptographicException(openStatus); + } + + // This might race, and that's okay. Worst case the algorithm is opened + // more than once, and the ones that lost will get cleaned up during collection. + Interlocked.CompareExchange(ref s_pbkdf2AlgorithmHandle, pbkdf2AlgorithmHandle, null); + } + + fixed (byte* pSymmetricKeyMaterial = symmetricKeyMaterial) + { + generateKeyStatus = Interop.BCrypt.BCryptGenerateSymmetricKey( + s_pbkdf2AlgorithmHandle, + out keyHandle, + pbKeyObject: IntPtr.Zero, + cbKeyObject: 0, + pSymmetricKeyMaterial, + symmetricKeyMaterialLength, + dwFlags: 0); + } + } + + CryptographicOperations.ZeroMemory(clearSpan); + + if (generateKeyStatus != NTSTATUS.STATUS_SUCCESS) + { + keyHandle.Dispose(); + throw Interop.BCrypt.CreateCryptographicException(generateKeyStatus); + } + + Debug.Assert(!keyHandle.IsInvalid); + + ulong kdfIterations = (ulong)iterations; // Previously asserted to be positive. + + using (keyHandle) + fixed (char* pHashAlgorithmName = hashAlgorithmName) + fixed (byte* pSalt = salt) + fixed (byte* pDestination = destination) + { + Span buffers = stackalloc BCryptBuffer[3]; + buffers[0].BufferType = CngBufferDescriptors.KDF_ITERATION_COUNT; + buffers[0].pvBuffer = (IntPtr)(&kdfIterations); + buffers[0].cbBuffer = sizeof(ulong); + + buffers[1].BufferType = CngBufferDescriptors.KDF_SALT; + buffers[1].pvBuffer = (IntPtr)pSalt; + buffers[1].cbBuffer = salt.Length; + + buffers[2].BufferType = CngBufferDescriptors.KDF_HASH_ALGORITHM; + buffers[2].pvBuffer = (IntPtr)pHashAlgorithmName; + + // C# spec: "A char* value produced by fixing a string instance always points to a null-terminated string" + buffers[2].cbBuffer = checked((hashAlgorithmName.Length + 1) * sizeof(char)); // Add null terminator. + + fixed (BCryptBuffer* pBuffers = buffers) + { + Interop.BCrypt.BCryptBufferDesc bufferDesc; + bufferDesc.ulVersion = Interop.BCrypt.BCRYPTBUFFER_VERSION; + bufferDesc.cBuffers = buffers.Length; + bufferDesc.pBuffers = (IntPtr)pBuffers; + + NTSTATUS deriveStatus = Interop.BCrypt.BCryptKeyDerivation( + keyHandle, + &bufferDesc, + pDestination, + destination.Length, + out uint resultLength, + dwFlags: 0); + + if (deriveStatus != NTSTATUS.STATUS_SUCCESS) + { + throw Interop.BCrypt.CreateCryptographicException(deriveStatus); + } + + if (destination.Length != resultLength) + { + Debug.Fail("PBKDF2 resultLength != destination.Length"); + throw new CryptographicException(); + } + } + } + } + + private static unsafe void FillDeriveKeyPBKDF2( + ReadOnlySpan password, + ReadOnlySpan salt, + int iterations, + string hashAlgorithmName, + Span destination) + { + const BCryptOpenAlgorithmProviderFlags OpenAlgorithmFlags = BCryptOpenAlgorithmProviderFlags.BCRYPT_ALG_HANDLE_HMAC_FLAG; + + // This code path will only be taken on Windows 7, so we can assume pseudo handles are not supported. + // Do not dispose handle since it is shared and cached. + SafeBCryptAlgorithmHandle handle = + Interop.BCrypt.BCryptAlgorithmCache.GetCachedBCryptAlgorithmHandle(hashAlgorithmName, OpenAlgorithmFlags, out _); + + fixed (byte* pPassword = password) + fixed (byte* pSalt = salt) + fixed (byte* pDestination = destination) + { + NTSTATUS status = Interop.BCrypt.BCryptDeriveKeyPBKDF2( + handle, + pPassword, + password.Length, + pSalt, + salt.Length, + (ulong)iterations, + pDestination, + destination.Length, + dwFlags: 0); + + if (status != NTSTATUS.STATUS_SUCCESS) + { + throw Interop.BCrypt.CreateCryptographicException(status); + } + } + } + + private static int GetHashBlockSize(string hashAlgorithmName) + { + // Block sizes per NIST FIPS pub 180-4. + switch (hashAlgorithmName) + { + case HashAlgorithmNames.SHA1: + case HashAlgorithmNames.SHA256: + return 512 / 8; + case HashAlgorithmNames.SHA384: + case HashAlgorithmNames.SHA512: + return 1024 / 8; + default: + Debug.Fail($"Unexpected hash algorithm '{hashAlgorithmName}'"); + throw new CryptographicException(); + } + } + } +} diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/System.Security.Cryptography.Algorithms.csproj b/src/libraries/System.Security.Cryptography.Algorithms/src/System.Security.Cryptography.Algorithms.csproj index 4bc44931c4420c..4e6ab6f54dddce 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/src/System.Security.Cryptography.Algorithms.csproj +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/System.Security.Cryptography.Algorithms.csproj @@ -77,6 +77,7 @@ + @@ -292,6 +293,7 @@ + + + + + + + + @@ -539,6 +554,8 @@ Link="Common\Interop\OSX\System.Security.Cryptography.Native.Apple\Interop.SecKeyRef.Export.cs" /> + + diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/Rfc2898DeriveBytes.OneShot.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/Rfc2898DeriveBytes.OneShot.cs new file mode 100644 index 00000000000000..205bb91860c1fc --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/Rfc2898DeriveBytes.OneShot.cs @@ -0,0 +1,347 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Text; +using Internal.Cryptography; + +namespace System.Security.Cryptography +{ + public partial class Rfc2898DeriveBytes + { + // Throwing UTF8 on invalid input. + private static readonly Encoding s_throwingUtf8Encoding = new UTF8Encoding(false, true); + + /// + /// Creates a PBKDF2 derived key from password bytes. + /// + /// The password used to derive the key. + /// The key salt used to derive the key. + /// The number of iterations for the operation. + /// The hash algorithm to use to derive the key. + /// The size of key to derive. + /// + /// or is . + /// + /// + /// is not zero or a positive value. + /// -or- + /// is not a positive value. + /// + /// + /// has a + /// that is empty or . + /// + /// + /// is an unsupported hash algorithm. Supported algorithms + /// are , , + /// , and . + /// + public static byte[] Pbkdf2( + byte[] password, + byte[] salt, + int iterations, + HashAlgorithmName hashAlgorithm, + int outputLength) + { + if (password is null) + throw new ArgumentNullException(nameof(password)); + if (salt is null) + throw new ArgumentNullException(nameof(salt)); + + return Pbkdf2(new ReadOnlySpan(password), new ReadOnlySpan(salt), iterations, hashAlgorithm, outputLength); + } + + /// + /// Creates a PBKDF2 derived key from password bytes. + /// + /// The password used to derive the key. + /// The key salt used to derive the key. + /// The number of iterations for the operation. + /// The hash algorithm to use to derive the key. + /// The size of key to derive. + /// + /// is not zero or a positive value. + /// -or- + /// is not a positive value. + /// + /// + /// has a + /// that is empty or . + /// + /// + /// is an unsupported hash algorithm. Supported algorithms + /// are , , + /// , and . + /// + public static byte[] Pbkdf2( + ReadOnlySpan password, + ReadOnlySpan salt, + int iterations, + HashAlgorithmName hashAlgorithm, + int outputLength) + { + if (iterations <= 0) + throw new ArgumentOutOfRangeException(nameof(iterations), SR.ArgumentOutOfRange_NeedPosNum); + if (outputLength < 0) + throw new ArgumentOutOfRangeException(nameof(outputLength), SR.ArgumentOutOfRange_NeedNonNegNum); + + ValidateHashAlgorithm(hashAlgorithm); + + byte[] result = new byte[outputLength]; + Pbkdf2Core(password, salt, result, iterations, hashAlgorithm); + return result; + } + + /// + /// Fills a buffer with a PBKDF2 derived key. + /// + /// The password used to derive the key. + /// The key salt used to derive the key. + /// The number of iterations for the operation. + /// The hash algorithm to use to derive the key. + /// The buffer to fill with a derived key. + /// + /// is not a positive value. + /// + /// + /// has a + /// that is empty or . + /// + /// + /// is an unsupported hash algorithm. Supported algorithms + /// are , , + /// , and . + /// + public static void Pbkdf2( + ReadOnlySpan password, + ReadOnlySpan salt, + Span destination, + int iterations, + HashAlgorithmName hashAlgorithm) + { + if (iterations <= 0) + throw new ArgumentOutOfRangeException(nameof(iterations), SR.ArgumentOutOfRange_NeedPosNum); + + ValidateHashAlgorithm(hashAlgorithm); + + Pbkdf2Core(password, salt, destination, iterations, hashAlgorithm); + } + + /// + /// Creates a PBKDF2 derived key from a password. + /// + /// The password used to derive the key. + /// The key salt used to derive the key. + /// The number of iterations for the operation. + /// The hash algorithm to use to derive the key. + /// The size of key to derive. + /// + /// or is . + /// + /// + /// is not zero or a positive value. + /// -or- + /// is not a positive value. + /// + /// + /// has a + /// that is empty or . + /// + /// + /// is an unsupported hash algorithm. Supported algorithms + /// are , , + /// , and . + /// + /// + /// contains text that cannot be converted to UTF8. + /// + /// + /// The will be converted to bytes using the UTF8 encoding. For + /// other encodings, convert the password string to bytes using the appropriate + /// and use . + /// + public static byte[] Pbkdf2( + string password, + byte[] salt, + int iterations, + HashAlgorithmName hashAlgorithm, + int outputLength) + { + if (password is null) + throw new ArgumentNullException(nameof(password)); + if (salt is null) + throw new ArgumentNullException(nameof(salt)); + + return Pbkdf2(password.AsSpan(), new ReadOnlySpan(salt), iterations, hashAlgorithm, outputLength); + } + + /// + /// Creates a PBKDF2 derived key from a password. + /// + /// The password used to derive the key. + /// The key salt used to derive the key. + /// The number of iterations for the operation. + /// The hash algorithm to use to derive the key. + /// The size of key to derive. + /// + /// is not zero or a positive value. + /// -or- + /// is not a positive value. + /// + /// + /// has a + /// that is empty or . + /// + /// + /// is an unsupported hash algorithm. Supported algorithms + /// are , , + /// , and . + /// + /// + /// contains text that cannot be converted to UTF8. + /// + /// + /// The will be converted to bytes using the UTF8 encoding. For + /// other encodings, convert the password string to bytes using the appropriate + /// and use . + /// + public static byte[] Pbkdf2( + ReadOnlySpan password, + ReadOnlySpan salt, + int iterations, + HashAlgorithmName hashAlgorithm, + int outputLength) + { + if (outputLength < 0) + throw new ArgumentOutOfRangeException(nameof(outputLength), SR.ArgumentOutOfRange_NeedNonNegNum); + if (iterations <= 0) + throw new ArgumentOutOfRangeException(nameof(iterations), SR.ArgumentOutOfRange_NeedPosNum); + + ValidateHashAlgorithm(hashAlgorithm); + + byte[] result = new byte[outputLength]; + Pbkdf2Core(password, salt, result, iterations, hashAlgorithm); + return result; + } + + /// + /// Fills a buffer with a PBKDF2 derived key. + /// + /// The password used to derive the key. + /// The key salt used to derive the key. + /// The number of iterations for the operation. + /// The hash algorithm to use to derive the key. + /// The buffer to fill with a derived key. + /// + /// is not a positive value. + /// + /// + /// has a + /// that is empty or . + /// + /// + /// is an unsupported hash algorithm. Supported algorithms + /// are , , + /// , and . + /// + /// + /// contains text that cannot be converted to UTF8. + /// + /// + /// The will be converted to bytes using the UTF8 encoding. For + /// other encodings, convert the password string to bytes using the appropriate + /// and use . + /// + public static void Pbkdf2( + ReadOnlySpan password, + ReadOnlySpan salt, + Span destination, + int iterations, + HashAlgorithmName hashAlgorithm) + { + if (iterations <= 0) + throw new ArgumentOutOfRangeException(nameof(iterations), SR.ArgumentOutOfRange_NeedPosNum); + + ValidateHashAlgorithm(hashAlgorithm); + + Pbkdf2Core(password, salt, destination, iterations, hashAlgorithm); + } + + private static void Pbkdf2Core( + ReadOnlySpan password, + ReadOnlySpan salt, + Span destination, + int iterations, + HashAlgorithmName hashAlgorithm) + { + Debug.Assert(hashAlgorithm.Name is not null); + Debug.Assert(iterations > 0); + + if (destination.IsEmpty) + { + return; + } + + const int MaxPasswordStackSize = 256; + + byte[]? rentedPasswordBuffer = null; + int maxEncodedSize = s_throwingUtf8Encoding.GetMaxByteCount(password.Length); + + Span passwordBuffer = maxEncodedSize > MaxPasswordStackSize ? + (rentedPasswordBuffer = CryptoPool.Rent(maxEncodedSize)) : + stackalloc byte[MaxPasswordStackSize]; + int passwordBytesWritten = s_throwingUtf8Encoding.GetBytes(password, passwordBuffer); + Span passwordBytes = passwordBuffer.Slice(0, passwordBytesWritten); + + try + { + Pbkdf2Implementation.Fill(passwordBytes, salt, iterations, hashAlgorithm, destination); + } + finally + { + CryptographicOperations.ZeroMemory(passwordBytes); + } + + if (rentedPasswordBuffer is not null) + { + CryptoPool.Return(rentedPasswordBuffer, clearSize: 0); // manually cleared above. + } + } + + private static void Pbkdf2Core( + ReadOnlySpan password, + ReadOnlySpan salt, + Span destination, + int iterations, + HashAlgorithmName hashAlgorithm) + { + Debug.Assert(hashAlgorithm.Name is not null); + Debug.Assert(iterations > 0); + + if (destination.IsEmpty) + { + return; + } + + Pbkdf2Implementation.Fill(password, salt, iterations, hashAlgorithm, destination); + } + + private static void ValidateHashAlgorithm(HashAlgorithmName hashAlgorithm) + { + if (string.IsNullOrEmpty(hashAlgorithm.Name)) + throw new ArgumentException(SR.Cryptography_HashAlgorithmNameNullOrEmpty, nameof(hashAlgorithm)); + + string hashAlgorithmName = hashAlgorithm.Name; + + // MD5 intentionally left out. + if (hashAlgorithmName != HashAlgorithmName.SHA1.Name && + hashAlgorithmName != HashAlgorithmName.SHA256.Name && + hashAlgorithmName != HashAlgorithmName.SHA384.Name && + hashAlgorithmName != HashAlgorithmName.SHA512.Name) + { + throw new CryptographicException(SR.Format(SR.Cryptography_UnknownHashAlgorithm, hashAlgorithmName)); + } + } + } +} diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/Rfc2898DeriveBytes.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/Rfc2898DeriveBytes.cs index bf833303ecc90b..7c150159dc37a6 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/Rfc2898DeriveBytes.cs +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/Rfc2898DeriveBytes.cs @@ -12,7 +12,7 @@ namespace System.Security.Cryptography { [UnsupportedOSPlatform("browser")] - public class Rfc2898DeriveBytes : DeriveBytes + public partial class Rfc2898DeriveBytes : DeriveBytes { private const int MinimumSaltSize = 8; diff --git a/src/libraries/System.Security.Cryptography.Algorithms/tests/Rfc2898OneShotTests.cs b/src/libraries/System.Security.Cryptography.Algorithms/tests/Rfc2898OneShotTests.cs new file mode 100644 index 00000000000000..12f2fd03d5fff2 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Algorithms/tests/Rfc2898OneShotTests.cs @@ -0,0 +1,355 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Text; +using Xunit; +using Test.Cryptography; + +namespace System.Security.Cryptography.DeriveBytesTests +{ + [SkipOnMono("Not supported on Browser", TestPlatforms.Browser)] + public static class Rfc2898OneShotTests + { + private const string Password = "tired"; + + private static readonly byte[] s_passwordBytes = Encoding.UTF8.GetBytes(Password); + private static readonly byte[] s_salt = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; + private static readonly int s_extractLength = 14; + + [Fact] + public static void Pbkdf2_PasswordBytes_NullPassword() + { + AssertExtensions.Throws("password", () => + Rfc2898DeriveBytes.Pbkdf2( + password: (byte[])null, s_salt, iterations: 1, HashAlgorithmName.SHA256, s_extractLength) + ); + } + + [Fact] + public static void Pbkdf2_PasswordBytes_NullSalt() + { + AssertExtensions.Throws("salt", () => + Rfc2898DeriveBytes.Pbkdf2( + s_passwordBytes, salt: (byte[])null, iterations: 1, HashAlgorithmName.SHA256, s_extractLength) + ); + } + + [Fact] + public static void Pbkdf2_PasswordBytes_SaltBytes_SaltEmpty() + { + byte[] expectedKey = "1E437A1C79D75BE61E91141DAE20".HexToByteArray(); + byte[] key = Rfc2898DeriveBytes.Pbkdf2( + new byte[0], salt: new byte[0], iterations: 1, HashAlgorithmName.SHA1, s_extractLength); + Assert.Equal(expectedKey, key); + } + + [Fact] + public static void Pbkdf2_PasswordBytes_SaltBytes_IterationsNegative() + { + AssertExtensions.Throws("iterations", () => + Rfc2898DeriveBytes.Pbkdf2( + s_passwordBytes, s_salt, iterations: -1, HashAlgorithmName.SHA256, s_extractLength) + ); + } + + [Fact] + public static void Pbkdf2_PasswordBytes_SaltBytes_OutputLengthNegative() + { + AssertExtensions.Throws("outputLength", () => + Rfc2898DeriveBytes.Pbkdf2( + s_passwordBytes, s_salt, iterations: 1, HashAlgorithmName.SHA256, -1) + ); + } + + [Fact] + public static void Pbkdf2_PasswordBytes_BogusHash() + { + Assert.Throws(() => + Rfc2898DeriveBytes.Pbkdf2( + s_passwordBytes, s_salt, iterations: 1, new HashAlgorithmName("BLAH"), s_extractLength) + ); + } + + [Fact] + public static void Pbkdf2_PasswordBytes_NullHashName() + { + AssertExtensions.Throws("hashAlgorithm", () => + Rfc2898DeriveBytes.Pbkdf2( + s_passwordBytes, s_salt, iterations: 1, default(HashAlgorithmName), s_extractLength) + ); + } + + [Fact] + public static void Pbkdf2_PasswordBytes_EmptyHashName() + { + AssertExtensions.Throws("hashAlgorithm", () => + Rfc2898DeriveBytes.Pbkdf2( + s_passwordBytes, s_salt, iterations: 1, new HashAlgorithmName(""), s_extractLength) + ); + } + + [Fact] + public static void Pbkdf2_PasswordString_NullPassword() + { + AssertExtensions.Throws("password", () => + Rfc2898DeriveBytes.Pbkdf2( + password: (string)null, s_salt, iterations: 1, HashAlgorithmName.SHA256, s_extractLength) + ); + } + + [Fact] + public static void Pbkdf2_PasswordString_NullSalt() + { + AssertExtensions.Throws("salt", () => + Rfc2898DeriveBytes.Pbkdf2( + Password, salt: null, iterations: 1, HashAlgorithmName.SHA256, s_extractLength) + ); + } + + [Fact] + public static void Pbkdf2_PasswordString_SaltBytes_SaltEmpty() + { + byte[] expectedKey = "1E437A1C79D75BE61E91141DAE20".HexToByteArray(); + byte[] key = Rfc2898DeriveBytes.Pbkdf2( + password: "", salt: new byte[0], iterations: 1, HashAlgorithmName.SHA1, s_extractLength); + Assert.Equal(expectedKey, key); + } + + [Fact] + public static void Pbkdf2_PasswordString_SaltBytes_IterationsNegative() + { + AssertExtensions.Throws("iterations", () => + Rfc2898DeriveBytes.Pbkdf2( + Password, s_salt, iterations: -1, HashAlgorithmName.SHA256, s_extractLength) + ); + } + + [Fact] + public static void Pbkdf2_PasswordString_SaltBytes_OutputLengthNegative() + { + AssertExtensions.Throws("outputLength", () => + Rfc2898DeriveBytes.Pbkdf2( + Password, s_salt, iterations: 1, HashAlgorithmName.SHA256, -1) + ); + } + + [Fact] + public static void Pbkdf2_PasswordString_BogusHash() + { + Assert.Throws(() => + Rfc2898DeriveBytes.Pbkdf2( + Password, s_salt, iterations: 1, new HashAlgorithmName("BLAH"), s_extractLength) + ); + } + + [Fact] + public static void Pbkdf2_PasswordString_NullHashName() + { + AssertExtensions.Throws("hashAlgorithm", () => + Rfc2898DeriveBytes.Pbkdf2( + Password, s_salt, iterations: 1, default(HashAlgorithmName), s_extractLength) + ); + } + + [Fact] + public static void Pbkdf2_PasswordString_EmptyHashName() + { + AssertExtensions.Throws("hashAlgorithm", () => + Rfc2898DeriveBytes.Pbkdf2( + Password, s_salt, iterations: 1, new HashAlgorithmName(""), s_extractLength) + ); + } + + [Fact] + public static void Pbkdf2_PasswordString_InvalidUtf8() + { + Assert.Throws(() => + Rfc2898DeriveBytes.Pbkdf2( + "\uD800", s_salt, iterations: 1, HashAlgorithmName.SHA256, s_extractLength)); + } + + [Fact] + public static void Pbkdf2_Password_Salt_Overlapping_Completely() + { + Span buffer = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; + byte[] expected = { 0xBE, 0xA4, 0xEE, 0x0E, 0xC3, 0x98, 0xBF, 0x32 }; + Rfc2898DeriveBytes.Pbkdf2(buffer, buffer, buffer, iterations: 1, HashAlgorithmName.SHA256); + Assert.Equal(expected, buffer.ToArray()); + } + + [Fact] + public static void Pbkdf2_Password_Salt_Overlapping_Forward() + { + Span buffer = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 0xFF }; + Span output = buffer[1..]; + Span inputs = buffer[..^1]; + byte[] expected = { 0xBE, 0xA4, 0xEE, 0x0E, 0xC3, 0x98, 0xBF, 0x32 }; + Rfc2898DeriveBytes.Pbkdf2(inputs, inputs, output, iterations: 1, HashAlgorithmName.SHA256); + Assert.Equal(expected, output.ToArray()); + } + + [Fact] + public static void Pbkdf2_Password_Salt_Overlapping_Backward() + { + Span buffer = new byte[] { 0xFF, 1, 2, 3, 4, 5, 6, 7, 8 }; + Span output = buffer[..^1]; + Span inputs = buffer[1..]; + byte[] expected = { 0xBE, 0xA4, 0xEE, 0x0E, 0xC3, 0x98, 0xBF, 0x32 }; + Rfc2898DeriveBytes.Pbkdf2(inputs, inputs, output, iterations: 1, HashAlgorithmName.SHA256); + Assert.Equal(expected, output.ToArray()); + } + + [Theory] + [MemberData(nameof(Pbkdf2_PasswordBytes_Compare_Data))] + public static void Pbkdf2_PasswordBytes_Compare( + string hashAlgorithm, + int length, + int iterations, + string passwordHex, + string saltHex) + { + byte[] password = Convert.FromHexString(passwordHex); + byte[] salt = Convert.FromHexString(saltHex); + HashAlgorithmName hashAlgorithmName = new HashAlgorithmName(hashAlgorithm); + byte[] key1; + + using (Rfc2898DeriveBytes instanceKdf = new Rfc2898DeriveBytes(password, salt, iterations, hashAlgorithmName)) + { + key1 = instanceKdf.GetBytes(length); + } + + // byte array allocating + byte[] key2 = Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, hashAlgorithmName, length); + Assert.Equal(key1, key2); + + Span destinationBuffer = new byte[length + 2]; + Span destination = destinationBuffer.Slice(1, length); + Rfc2898DeriveBytes.Pbkdf2(password, salt, destination, iterations, hashAlgorithmName); + + Assert.True(key1.AsSpan().SequenceEqual(destination), "key1 == destination"); + Assert.Equal(0, destinationBuffer[^1]); // Make sure we didn't write past the destination + Assert.Equal(0, destinationBuffer[0]); // Make sure we didn't write before the destination + } + + [Theory] + [MemberData(nameof(Pbkdf2_PasswordString_Compare_Data))] + public static void Pbkdf2_PasswordString_Compare( + string hashAlgorithm, + int length, + int iterations, + string password, + string saltHex) + { + byte[] salt = Convert.FromHexString(saltHex); + HashAlgorithmName hashAlgorithmName = new HashAlgorithmName(hashAlgorithm); + byte[] key1; + + using (Rfc2898DeriveBytes instanceKdf = new Rfc2898DeriveBytes(password, salt, iterations, hashAlgorithmName)) + { + key1 = instanceKdf.GetBytes(length); + } + + // byte array allocating + byte[] key2 = Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, hashAlgorithmName, length); + Assert.Equal(key1, key2); + + Span destinationBuffer = new byte[length + 2]; + Span destination = destinationBuffer.Slice(1, length); + Rfc2898DeriveBytes.Pbkdf2(password, salt, destination, iterations, hashAlgorithmName); + + Assert.True(key1.AsSpan().SequenceEqual(destination), "key1 == destination"); + Assert.Equal(0, destinationBuffer[^1]); // Make sure we didn't write past the destination + Assert.Equal(0, destinationBuffer[0]); // Make sure we didn't write before the destination + } + + [Theory] + [MemberData(nameof(Pbkdf2_Rfc6070_Vectors))] + public static void Pbkdf2_Rfc6070(string password, string salt, int iterations, string expectedHex) + { + byte[] expected = expectedHex.HexToByteArray(); + byte[] saltBytes = Encoding.UTF8.GetBytes(salt); + byte[] actual = Rfc2898DeriveBytes.Pbkdf2(password, saltBytes, iterations, HashAlgorithmName.SHA1, expected.Length); + Assert.Equal(expected, actual); + } + + [Fact] + [OuterLoop("Uses a high number of iterations that can take over 20 seconds on some machines")] + public static void Pbkdf2_Rfc6070_HighIterations() + { + string password = "password"; + int iterations = 16777216; + byte[] expected = "eefe3d61cd4da4e4e9945b3d6ba2158c2634e984".HexToByteArray(); + byte[] salt = Encoding.UTF8.GetBytes("salt"); + byte[] actual = Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, HashAlgorithmName.SHA1, expected.Length); + Assert.Equal(expected, actual); + } + + public static IEnumerable Pbkdf2_PasswordBytes_Compare_Data() + { + string largeInputHex = new string('A', 8192); // 8192 hex characters = 4096 bytes. + + foreach (HashAlgorithmName hashAlgorithm in SupportedHashAlgorithms) + { + // hashAlgorithm, length, iterations, passwordHex, saltHex + yield return new object[] { hashAlgorithm.Name, 1, 1, s_passwordBytes.ByteArrayToHex(), s_salt.ByteArrayToHex() }; + yield return new object[] { hashAlgorithm.Name, 1, 1, "", s_salt.ByteArrayToHex() }; + yield return new object[] { hashAlgorithm.Name, 257, 257, "", s_salt.ByteArrayToHex() }; + yield return new object[] { hashAlgorithm.Name, 257, 257, "D8D8D8D8D8D8D8D8", "D8D8D8D8D8D8D8D8" }; + yield return new object[] { hashAlgorithm.Name, 257, 257, "0000000000000000", "0000000000000000" }; + yield return new object[] { hashAlgorithm.Name, 257, 257, largeInputHex, largeInputHex }; + } + + // Test around HMAC SHA1 and SHA256 block boundaries + for (int blockBoundary = 63; blockBoundary <= 65; blockBoundary++) + { + byte[] password = new byte[blockBoundary]; + yield return new object[] { HashAlgorithmName.SHA1.Name, 257, 257, password.ByteArrayToHex(), "0000000000000000" }; + yield return new object[] { HashAlgorithmName.SHA256.Name, 257, 257, password.ByteArrayToHex(), "0000000000000000" }; + } + + // Test around HMAC SHA384 and SHA512 block boundaries + for (int blockBoundary = 127; blockBoundary <= 129; blockBoundary++) + { + byte[] password = new byte[blockBoundary]; + yield return new object[] { HashAlgorithmName.SHA384.Name, 257, 257, password.ByteArrayToHex(), "0000000000000000" }; + yield return new object[] { HashAlgorithmName.SHA512.Name, 257, 257, password.ByteArrayToHex(), "0000000000000000" }; + } + } + + public static IEnumerable Pbkdf2_PasswordString_Compare_Data() + { + string largePassword = new string('y', 1024); + + foreach (HashAlgorithmName hashAlgorithm in SupportedHashAlgorithms) + { + // hashAlgorithm, length, iterations, password, saltHex + yield return new object[] { hashAlgorithm.Name, 1, 1, Password, s_salt.ByteArrayToHex() }; + yield return new object[] { hashAlgorithm.Name, 1, 1, "", s_salt.ByteArrayToHex() }; + yield return new object[] { hashAlgorithm.Name, 257, 257, "", s_salt.ByteArrayToHex() }; + yield return new object[] { hashAlgorithm.Name, 257, 257, Password, "D8D8D8D8D8D8D8D8" }; + yield return new object[] { hashAlgorithm.Name, 257, 257, Password, "0000000000000000" }; + + // case for password exceeding the stack buffer limit. + yield return new object[] { hashAlgorithm.Name, 257, 257, largePassword, "0000000000000000" }; + } + } + + public static IEnumerable Pbkdf2_Rfc6070_Vectors() + { + // password (P), salt (S), iterations (c), expected (DK) + yield return new object[] { "password", "salt", 1, "0c60c80f961f0e71f3a9b524af6012062fe037a6" }; + yield return new object[] { "password", "salt", 2, "ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957" }; + yield return new object[] { "passwordPASSWORDpassword", "saltSALTsaltSALTsaltSALTsaltSALTsalt", 4096, "3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038" }; + yield return new object[] { "pass\0word", "sa\0lt", 4096, "56fa6aa75548099dcc37d7f03425e0c3" }; + } + + private static HashAlgorithmName[] SupportedHashAlgorithms => new [] + { + HashAlgorithmName.SHA1, + HashAlgorithmName.SHA256, + HashAlgorithmName.SHA384, + HashAlgorithmName.SHA512 + }; + } +} diff --git a/src/libraries/System.Security.Cryptography.Algorithms/tests/System.Security.Cryptography.Algorithms.Tests.csproj b/src/libraries/System.Security.Cryptography.Algorithms/tests/System.Security.Cryptography.Algorithms.Tests.csproj index 3c3183281c985a..f48d669b029416 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/tests/System.Security.Cryptography.Algorithms.Tests.csproj +++ b/src/libraries/System.Security.Cryptography.Algorithms/tests/System.Security.Cryptography.Algorithms.Tests.csproj @@ -85,6 +85,7 @@ +