Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Re-write Android PBKDF2 one shot in Java #103016

Merged
merged 5 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<byte> pPassword,
int passwordLength,
ReadOnlySpan<byte> pSalt,
int saltLength,
int iterations,
Span<byte> pDestination,
int destinationLength);

internal static void Pbkdf2(
string algorithmName,
ReadOnlySpan<byte> password,
ReadOnlySpan<byte> salt,
int iterations,
Span<byte> 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<byte> destination)
{
// The partial needs to match the OpenSSL parameters.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1017,7 +1017,7 @@
<Compile Include="System\Security\Cryptography\OpenSslCipher.cs" />
<Compile Include="System\Security\Cryptography\OpenSslCipherLite.cs" />
<Compile Include="System\Security\Cryptography\PasswordDeriveBytes.NotSupported.cs" />
<Compile Include="System\Security\Cryptography\Pbkdf2Implementation.Managed.cs" />
<Compile Include="System\Security\Cryptography\Pbkdf2Implementation.Android.cs" />
<Compile Include="System\Security\Cryptography\PinAndClear.cs" />
<Compile Include="System\Security\Cryptography\RandomNumberGeneratorImplementation.OpenSsl.cs" />
<Compile Include="System\Security\Cryptography\RC2CryptoServiceProvider.Unix.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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<byte> password,
ReadOnlySpan<byte> salt,
int iterations,
HashAlgorithmName hashAlgorithmName,
Span<byte> destination)
{
Debug.Assert(!destination.IsEmpty);
Debug.Assert(hashAlgorithmName.Name is not null);
Interop.Crypto.Pbkdf2(hashAlgorithmName.Name, password, salt, iterations, destination);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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, "<init>", "(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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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);
Loading