Skip to content

Commit

Permalink
Re-write Android PBKDF2 one shot in Java
Browse files Browse the repository at this point in the history
  • Loading branch information
vcsjones authored Jun 7, 2024
1 parent d3222c9 commit 2ea80d6
Show file tree
Hide file tree
Showing 9 changed files with 265 additions and 1 deletion.
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);

0 comments on commit 2ea80d6

Please sign in to comment.