diff --git a/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.Evp.cs b/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.Evp.cs index 59e93080c562e8..0c476ec04b3563 100644 --- a/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.Evp.cs +++ b/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.Evp.cs @@ -38,6 +38,52 @@ internal static partial class Crypto [LibraryImport(Libraries.AndroidCryptoNative, EntryPoint = "CryptoNative_GetMaxMdSize")] private static partial int GetMaxMdSize(); + [LibraryImport(Libraries.AndroidCryptoNative, EntryPoint = "AndroidCryptoNative_Pbkdf2", StringMarshalling = StringMarshalling.Utf8)] + private static partial int Pbkdf2( + string algorithmName, + ReadOnlySpan pPassword, + int passwordLength, + ReadOnlySpan pSalt, + int saltLength, + int iterations, + Span pDestination, + int destinationLength); + + internal static void Pbkdf2( + string algorithmName, + ReadOnlySpan password, + ReadOnlySpan salt, + int iterations, + Span destination) + { + const int Success = 1; + const int UnsupportedAlgorithm = -1; + const int Failed = 0; + + int result = Pbkdf2( + algorithmName, + password, + password.Length, + salt, + salt.Length, + iterations, + destination, + destination.Length); + + switch (result) + { + case Success: + return; + case UnsupportedAlgorithm: + throw new CryptographicException(SR.Format(SR.Cryptography_UnknownHashAlgorithm, algorithmName)); + case Failed: + throw new CryptographicException(); + default: + Debug.Fail($"Unexpected result {result}"); + throw new CryptographicException(); + } + } + internal static unsafe int EvpDigestFinalXOF(SafeEvpMdCtxHandle ctx, Span destination) { // The partial needs to match the OpenSSL parameters. diff --git a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj index 4a7bd04e14eecf..7d0fd8f152118e 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -1017,7 +1017,7 @@ - + diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/Pbkdf2Implementation.Android.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/Pbkdf2Implementation.Android.cs new file mode 100644 index 00000000000000..bca299f689c7a2 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/Pbkdf2Implementation.Android.cs @@ -0,0 +1,22 @@ +// 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; + +namespace System.Security.Cryptography +{ + internal static 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); + Interop.Crypto.Pbkdf2(hashAlgorithmName.Name, password, salt, iterations, destination); + } + } +} diff --git a/src/native/libs/System.Security.Cryptography.Native.Android/CMakeLists.txt b/src/native/libs/System.Security.Cryptography.Native.Android/CMakeLists.txt index 76c93faf093f0e..a277d5df3e5bd3 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Android/CMakeLists.txt +++ b/src/native/libs/System.Security.Cryptography.Native.Android/CMakeLists.txt @@ -20,6 +20,7 @@ set(NATIVECRYPTO_SOURCES pal_lifetime.c pal_memory.c pal_misc.c + pal_pbkdf2.c pal_rsa.c pal_signature.c pal_ssl.c diff --git a/src/native/libs/System.Security.Cryptography.Native.Android/net/dot/android/crypto/PalPbkdf2.java b/src/native/libs/System.Security.Cryptography.Native.Android/net/dot/android/crypto/PalPbkdf2.java new file mode 100644 index 00000000000000..8b9d176b3f69aa --- /dev/null +++ b/src/native/libs/System.Security.Cryptography.Native.Android/net/dot/android/crypto/PalPbkdf2.java @@ -0,0 +1,107 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +package net.dot.android.crypto; + +import java.nio.ByteBuffer; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import javax.crypto.Mac; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.SecretKeySpec; + +public final class PalPbkdf2 { + private static final int ERROR_UNSUPPORTED_ALGORITHM = -1; + private static final int SUCCESS = 1; + + public static int pbkdf2OneShot(String algorithmName, byte[] password, ByteBuffer salt, int iterations, ByteBuffer destination) + throws ShortBufferException, InvalidKeyException, IllegalArgumentException { + // salt and destination are DirectByteBuffers that point to memory created by .NET. + // These must not be touched after this method returns. + + // We do not ever expect a ShortBufferException to ever get thrown since the buffer destination length is always + // checked. Let it go through the checked exception and JNI will handle it as a generic failure. + // InvalidKeyException should not throw except the the case of an empty key, which we already handle. Let JNI + // handle it as a generic failure. + + // We use a custom implementation of PBKDF2 instead of the one provided by the Android platform for two reasons: + // The first is that Android only added support for PBKDF2 + SHA-2 family of agorithms in API level 26, and we + // need to support SHA-2 prior to that. + // The second is that PBEKeySpec only supports char-based passwords, whereas .NET supports arbitrary byte keys. + + if (algorithmName == null || password == null || destination == null) { + // These are essentially asserts since the .NET side should have already validated these. + throw new IllegalArgumentException("algorithmName, password, and destination must not be null."); + } + // The .NET side already validates the hash algorithm name inputs. + String javaAlgorithmName = "Hmac" + algorithmName; + Mac mac; + + try { + mac = Mac.getInstance(javaAlgorithmName); + } + catch (NoSuchAlgorithmException nsae) { + return ERROR_UNSUPPORTED_ALGORITHM; + } + + if (password.length == 0) { + // SecretKeySpec does not permit empty keys. Since HMAC just zero extends the key, a single zero byte key is + // the same as an empty key. + password = new byte[] { 0 }; + } + + // Since the salt needs to be read for each block, mark its current position before entering the loop. + if (salt != null) { + salt.mark(); + } + + SecretKeySpec key = new SecretKeySpec(password, javaAlgorithmName); + mac.init(key); + + // Since this is a one-shot, it should not be possible to exceed the extract limit since the .NET side is + // limited to the length of a span (2^31 - 1 bytes). It would only take ~128 million SHA-1 blocks to fill an entire + // span, and 128 million fits in a signed 32-bit integer. + int blockCounter = 1; + int destinationOffset = 0; + byte[] blockCounterBuffer = new byte[4]; // Big-endian 32-bit integer + byte[] block = new byte[mac.getMacLength()]; + byte[] u = new byte[block.length]; + + while (destinationOffset < destination.capacity()) { + writeBigEndianInt(blockCounter, blockCounterBuffer); + + if (salt != null) { + mac.update(salt); + salt.reset(); // Resets it back to the previous mark. It does not consume the mark, so we don't need to mark again. + } + + mac.update(blockCounterBuffer); + mac.doFinal(u, 0); + + System.arraycopy(u, 0, block, 0, block.length); + + // Start at 2 since we did the first iteration above. + for (int i = 2; i <= iterations; i++) { + mac.update(u); + mac.doFinal(u, 0); + + for (int j = 0; j < u.length; j++) { + block[j] ^= u[j]; + } + } + + destination.put(block, 0, Math.min(block.length, destination.capacity() - destinationOffset)); + destinationOffset += block.length; + blockCounter++; + } + + return SUCCESS; + } + + private static void writeBigEndianInt(int value, byte[] destination) { + destination[0] = (byte)((value >> 24) & 0xFF); + destination[1] = (byte)((value >> 16) & 0xFF); + destination[2] = (byte)((value >> 8) & 0xFF); + destination[3] = (byte)(value & 0xFF); + } +} diff --git a/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.c b/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.c index d562451ee54a04..e4109c26e2c3d8 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.c +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.c @@ -489,6 +489,10 @@ jclass g_TrustManager; jclass g_DotnetProxyTrustManager; jmethodID g_DotnetProxyTrustManagerCtor; +// net/dot/android/crypto/PalPbkdf2 +jclass g_PalPbkdf2; +jmethodID g_PalPbkdf2Pbkdf2OneShot; + jobject ToGRef(JNIEnv *env, jobject lref) { if (lref) @@ -1096,5 +1100,8 @@ JNI_OnLoad(JavaVM *vm, void *reserved) g_DotnetProxyTrustManager = GetClassGRef(env, "net/dot/android/crypto/DotnetProxyTrustManager"); g_DotnetProxyTrustManagerCtor = GetMethod(env, false, g_DotnetProxyTrustManager, "", "(J)V"); + g_PalPbkdf2 = GetClassGRef(env, "net/dot/android/crypto/PalPbkdf2"); + g_PalPbkdf2Pbkdf2OneShot = GetMethod(env, true, g_PalPbkdf2, "pbkdf2OneShot", "(Ljava/lang/String;[BLjava/nio/ByteBuffer;ILjava/nio/ByteBuffer;)I"); + return JNI_VERSION_1_6; } diff --git a/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.h b/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.h index d1dc577bdf078a..fa716968cbc6d4 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.h +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.h @@ -503,6 +503,10 @@ extern jclass g_TrustManager; extern jclass g_DotnetProxyTrustManager; extern jmethodID g_DotnetProxyTrustManagerCtor; +// net/dot/android/crypto/PalPbkdf2 +extern jclass g_PalPbkdf2; +extern jmethodID g_PalPbkdf2Pbkdf2OneShot; + // Compatibility macros #if !defined (__mallocfunc) #if defined (__clang__) || defined (__GNUC__) diff --git a/src/native/libs/System.Security.Cryptography.Native.Android/pal_pbkdf2.c b/src/native/libs/System.Security.Cryptography.Native.Android/pal_pbkdf2.c new file mode 100644 index 00000000000000..793b70a74d952c --- /dev/null +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_pbkdf2.c @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include "pal_pbkdf2.h" +#include "pal_utilities.h" + +int32_t AndroidCryptoNative_Pbkdf2(const char* algorithmName, + const uint8_t* password, + int32_t passwordLength, + uint8_t* salt, + int32_t saltLength, + int32_t iterations, + uint8_t* destination, + int32_t destinationLength) +{ + JNIEnv* env = GetJNIEnv(); + jint ret = FAIL; + + jstring javaAlgorithmName = make_java_string(env, algorithmName); + jbyteArray passwordBytes = make_java_byte_array(env, passwordLength); + jobject destinationBuffer = (*env)->NewDirectByteBuffer(env, destination, destinationLength); + jobject saltByteBuffer = NULL; + + if (javaAlgorithmName == NULL || passwordBytes == NULL || destinationBuffer == NULL) + { + goto cleanup; + } + + if (password && passwordLength > 0) + { + (*env)->SetByteArrayRegion(env, passwordBytes, 0, passwordLength, (const jbyte*)password); + } + + if (salt && saltLength > 0) + { + saltByteBuffer = (*env)->NewDirectByteBuffer(env, salt, saltLength); + + if (saltByteBuffer == NULL) + { + goto cleanup; + } + } + + ret = (*env)->CallStaticIntMethod(env, g_PalPbkdf2, g_PalPbkdf2Pbkdf2OneShot, + javaAlgorithmName, passwordBytes, saltByteBuffer, iterations, destinationBuffer); + + if (CheckJNIExceptions(env)) + { + ret = FAIL; + } + +cleanup: + (*env)->DeleteLocalRef(env, javaAlgorithmName); + (*env)->DeleteLocalRef(env, passwordBytes); + (*env)->DeleteLocalRef(env, saltByteBuffer); + (*env)->DeleteLocalRef(env, destinationBuffer); + + return ret; +} diff --git a/src/native/libs/System.Security.Cryptography.Native.Android/pal_pbkdf2.h b/src/native/libs/System.Security.Cryptography.Native.Android/pal_pbkdf2.h new file mode 100644 index 00000000000000..a430de558cac78 --- /dev/null +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_pbkdf2.h @@ -0,0 +1,18 @@ +// 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_jni.h" +#include "pal_compiler.h" +#include "pal_types.h" + + +PALEXPORT int32_t AndroidCryptoNative_Pbkdf2(const char* algorithmName, + const uint8_t* password, + int32_t passwordLength, + uint8_t* salt, + int32_t saltLength, + int32_t iterations, + uint8_t* destination, + int32_t destinationLength);