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 1 commit
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,101 @@
// 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.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, byte[] salt, int iterations, byte[] destination)
vcsjones marked this conversation as resolved.
Show resolved Hide resolved
throws ShortBufferException, InvalidKeyException {

// 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.

assert algorithmName != null;
assert password != null;
assert salt != null;
assert iterations > 0;
assert destination != null;
bartonjs marked this conversation as resolved.
Show resolved Hide resolved

// The .NET side already validates the hash algorithm name inputs.
String javaAlgorithmName = "Hmac" + algorithmName;
Mac mac;

try {
mac = Mac.getInstance(javaAlgorithmName);
}
catch (NoSuchAlgorithmException nsae) {
assert false : "The algorithm was checked before, so it should be available.";
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 };
}

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.length) {
assert blockCounter > 0; // Given the comment above, this should never overflow.

writeBigEndianInt(blockCounter, blockCounterBuffer);

mac.update(salt);
vcsjones marked this conversation as resolved.
Show resolved Hide resolved
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.reset();
mac.update(u);
mac.doFinal(u, 0);

for (int j = 0; j < u.length; j++) {
block[j] ^= u[j];
}
}

System.arraycopy(block, 0, destination, destinationOffset, Math.min(block.length, destination.length - 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;[B[BI[B)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,52 @@
// 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,
const uint8_t* salt,
int32_t saltLength,
int32_t iterations,
uint8_t* destination,
int32_t destinationLength)
{
JNIEnv* env = GetJNIEnv();

jstring javaAlgorithmName = make_java_string(env, algorithmName);
jbyteArray passwordBytes = make_java_byte_array(env, passwordLength);
jbyteArray saltBytes = make_java_byte_array(env, saltLength);
jbyteArray destinationBytes = make_java_byte_array(env, destinationLength);

if (password && passwordLength > 0)
{
(*env)->SetByteArrayRegion(env, passwordBytes, 0, passwordLength, (const jbyte*)password);
}

if (salt && saltLength > 0)
{
(*env)->SetByteArrayRegion(env, saltBytes, 0, saltLength, (const jbyte*)salt);
}

jint ret = (*env)->CallStaticIntMethod(env, g_PalPbkdf2, g_PalPbkdf2Pbkdf2OneShot,
javaAlgorithmName, passwordBytes, saltBytes, iterations, destinationBytes);

if (CheckJNIExceptions(env))
{
ret = FAIL;
}
else if (ret == SUCCESS)
{
(*env)->GetByteArrayRegion(env, destinationBytes, 0, destinationLength, (jbyte*)destination);
}

(*env)->DeleteLocalRef(env, javaAlgorithmName);
(*env)->DeleteLocalRef(env, passwordBytes);
(*env)->DeleteLocalRef(env, saltBytes);
(*env)->DeleteLocalRef(env, destinationBytes);

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,
const uint8_t* salt,
int32_t saltLength,
int32_t iterations,
uint8_t* destination,
int32_t destinationLength);
Loading