From ce085308876cfbd79a6554fbd6adcd89821c10b2 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Thu, 4 Jun 2026 13:03:17 -0400 Subject: [PATCH 1/4] Temporary diagnostics for ChaCha20 on Android --- .../tests/ChaCha20Poly1305Tests.cs | 103 +++++++++++++++++- .../pal_cipher.c | 26 ++++- 2 files changed, 124 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Security.Cryptography/tests/ChaCha20Poly1305Tests.cs b/src/libraries/System.Security.Cryptography/tests/ChaCha20Poly1305Tests.cs index ff618943d0546f..b99d8ee9258e36 100644 --- a/src/libraries/System.Security.Cryptography/tests/ChaCha20Poly1305Tests.cs +++ b/src/libraries/System.Security.Cryptography/tests/ChaCha20Poly1305Tests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; using Test.Cryptography; using Xunit; @@ -35,11 +36,109 @@ public static void EncryptTamperAADDecrypt(int dataLength, int additionalDataLen additionalData[0] ^= 1; byte[] decrypted = new byte[dataLength]; - Assert.Throws( - () => chaChaPoly.Decrypt(nonce, ciphertext, tag, decrypted, additionalData)); + try + { + Assert.Throws( + () => chaChaPoly.Decrypt(nonce, ciphertext, tag, decrypted, additionalData)); + } + catch (Exception ex) when (ShouldLogAndroidAeadDiagnostics(dataLength, additionalDataLength)) + { + LogAndroidAeadDiagnostics( + nameof(EncryptTamperAADDecrypt), + nameof(ChaCha20Poly1305), + dataLength, + additionalDataLength, + ex); + throw; + } } } + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsAndroid))] + public static void AndroidDiagnostic_TamperedAadDecryptMatrix() + { + LogAndroidAeadEnvironment(nameof(ChaCha20Poly1305)); + + int failures = 0; + failures += RunAndroidDiagnosticCase(dataLength: 0, additionalDataLength: 0, tamperAAD: false, expectAuthenticationFailure: false); + failures += RunAndroidDiagnosticCase(dataLength: 0, additionalDataLength: 1, tamperAAD: true, expectAuthenticationFailure: true); + failures += RunAndroidDiagnosticCase(dataLength: 0, additionalDataLength: 30, tamperAAD: true, expectAuthenticationFailure: true); + failures += RunAndroidDiagnosticCase(dataLength: 1, additionalDataLength: 1, tamperAAD: true, expectAuthenticationFailure: true); + + Assert.Equal(0, failures); + } + + private static int RunAndroidDiagnosticCase( + int dataLength, + int additionalDataLength, + bool tamperAAD, + bool expectAuthenticationFailure) + { + byte[] additionalData = new byte[additionalDataLength]; + RandomNumberGenerator.Fill(additionalData); + + byte[] plaintext = Enumerable.Range(1, dataLength).Select((x) => (byte)x).ToArray(); + byte[] ciphertext = new byte[dataLength]; + byte[] key = RandomNumberGenerator.GetBytes(KeySizeInBytes); + byte[] nonce = RandomNumberGenerator.GetBytes(NonceSizeInBytes); + byte[] tag = new byte[TagSizeInBytes]; + + using var chaChaPoly = new ChaCha20Poly1305(key); + chaChaPoly.Encrypt(nonce, plaintext, ciphertext, tag, additionalData); + + if (tamperAAD) + { + additionalData[0] ^= 1; + } + + byte[] decrypted = new byte[dataLength]; + Exception ex = Record.Exception(() => chaChaPoly.Decrypt(nonce, ciphertext, tag, decrypted, additionalData)); + + LogAndroidAeadDiagnostics( + nameof(AndroidDiagnostic_TamperedAadDecryptMatrix), + nameof(ChaCha20Poly1305), + dataLength, + additionalDataLength, + ex); + + if (expectAuthenticationFailure) + { + return ex is AuthenticationTagMismatchException ? 0 : 1; + } + + return ex is null && plaintext.SequenceEqual(decrypted) ? 0 : 1; + } + + private static bool ShouldLogAndroidAeadDiagnostics(int dataLength, int additionalDataLength) + { + return PlatformDetection.IsAndroid && dataLength == 0 && additionalDataLength == 1; + } + + private static void LogAndroidAeadEnvironment(string algorithm) + { + Console.WriteLine( + $"Android AEAD diagnostics for {algorithm}: " + + $"RuntimeIdentifier={RuntimeInformation.RuntimeIdentifier}, " + + $"ProcessArchitecture={RuntimeInformation.ProcessArchitecture}, " + + $"OSArchitecture={RuntimeInformation.OSArchitecture}, " + + $"IntPtr.Size={IntPtr.Size}, " + + $"Framework={RuntimeInformation.FrameworkDescription}"); + } + + private static void LogAndroidAeadDiagnostics( + string testName, + string algorithm, + int dataLength, + int additionalDataLength, + Exception ex) + { + Console.WriteLine( + $"Android AEAD diagnostics: test={testName}, algorithm={algorithm}, " + + $"dataLength={dataLength}, additionalDataLength={additionalDataLength}, " + + $"exceptionType={ex?.GetType().FullName ?? ""}, " + + $"exceptionMessage={ex?.Message ?? ""}"); + } + [Theory] [InlineData(0)] [InlineData(1)] diff --git a/src/native/libs/System.Security.Cryptography.Native.Android/pal_cipher.c b/src/native/libs/System.Security.Cryptography.Native.Android/pal_cipher.c index a775b0f7552888..348612dc2c618c 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Android/pal_cipher.c +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_cipher.c @@ -242,7 +242,14 @@ int32_t AndroidCryptoNative_CipherUpdateAAD(CipherCtx* ctx, uint8_t* in, int32_t (*env)->SetByteArrayRegion(env, inDataBytes, 0, inl, (jbyte*)in); (*env)->CallVoidMethod(env, ctx->cipher, g_cipherUpdateAADMethod, inDataBytes); (*env)->DeleteLocalRef(env, inDataBytes); - return CheckJNIExceptions(env) ? FAIL : SUCCESS; + + if ((*env)->ExceptionCheck(env)) + { + LOG_ERROR("Cipher.updateAAD failed for %s with input length %d", ctx->type->name, inl); + return CheckJNIExceptions(env) ? FAIL : SUCCESS; + } + + return SUCCESS; } int32_t AndroidCryptoNative_CipherUpdate(CipherCtx* ctx, uint8_t* outm, int32_t* outl, uint8_t* in, int32_t inl) @@ -273,7 +280,14 @@ int32_t AndroidCryptoNative_CipherUpdate(CipherCtx* ctx, uint8_t* outm, int32_t* } (*env)->DeleteLocalRef(env, inDataBytes); - return CheckJNIExceptions(env) ? FAIL : SUCCESS; + + if ((*env)->ExceptionCheck(env)) + { + LOG_ERROR("Cipher.update failed for %s with input length %d", ctx->type->name, inl); + return CheckJNIExceptions(env) ? FAIL : SUCCESS; + } + + return SUCCESS; } int32_t AndroidCryptoNative_CipherFinalEx(CipherCtx* ctx, uint8_t* outm, int32_t* outl) @@ -317,10 +331,11 @@ int32_t AndroidCryptoNative_AeadCipherFinalEx(CipherCtx* ctx, uint8_t* outm, int jbyteArray outBytes = (jbyteArray)(*env)->CallObjectMethod(env, ctx->cipher, g_cipherDoFinalMethod); jthrowable ex = NULL; - if (TryGetJNIException(env, &ex, false)) + if (TryGetJNIException(env, &ex, true)) { if (ex == NULL) { + LOG_ERROR("Cipher.doFinal failed for %s without an exception object", ctx->type->name); return FAIL; } @@ -329,6 +344,11 @@ int32_t AndroidCryptoNative_AeadCipherFinalEx(CipherCtx* ctx, uint8_t* outm, int *authTagMismatch = 1; } + LOG_ERROR( + "Cipher.doFinal failed for %s; is AEADBadTagException=%d", + ctx->type->name, + *authTagMismatch); + (*env)->DeleteLocalRef(env, ex); return FAIL; } From 8c4872780e5a43e631d558b7fed9632f013c57f9 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Thu, 4 Jun 2026 16:58:52 -0400 Subject: [PATCH 2/4] Try to get android to tell me what is happening --- .../tests/ChaCha20Poly1305Tests.cs | 89 ++++++++++++++----- 1 file changed, 66 insertions(+), 23 deletions(-) diff --git a/src/libraries/System.Security.Cryptography/tests/ChaCha20Poly1305Tests.cs b/src/libraries/System.Security.Cryptography/tests/ChaCha20Poly1305Tests.cs index b99d8ee9258e36..83e421d058d58a 100644 --- a/src/libraries/System.Security.Cryptography/tests/ChaCha20Poly1305Tests.cs +++ b/src/libraries/System.Security.Cryptography/tests/ChaCha20Poly1305Tests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; +using System.Text; using Test.Cryptography; using Xunit; @@ -43,12 +44,16 @@ public static void EncryptTamperAADDecrypt(int dataLength, int additionalDataLen } catch (Exception ex) when (ShouldLogAndroidAeadDiagnostics(dataLength, additionalDataLength)) { - LogAndroidAeadDiagnostics( - nameof(EncryptTamperAADDecrypt), - nameof(ChaCha20Poly1305), - dataLength, - additionalDataLength, - ex); + Assert.Fail( + CreateAndroidAeadDiagnostics( + nameof(EncryptTamperAADDecrypt), + nameof(ChaCha20Poly1305), + dataLength, + additionalDataLength, + tamperAAD: true, + expectAuthenticationFailure: true, + decryptedMatchesPlaintext: false, + ex)); throw; } } @@ -57,18 +62,20 @@ public static void EncryptTamperAADDecrypt(int dataLength, int additionalDataLen [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsAndroid))] public static void AndroidDiagnostic_TamperedAadDecryptMatrix() { - LogAndroidAeadEnvironment(nameof(ChaCha20Poly1305)); + StringBuilder diagnostics = new(); + AppendAndroidAeadEnvironment(diagnostics, nameof(ChaCha20Poly1305)); int failures = 0; - failures += RunAndroidDiagnosticCase(dataLength: 0, additionalDataLength: 0, tamperAAD: false, expectAuthenticationFailure: false); - failures += RunAndroidDiagnosticCase(dataLength: 0, additionalDataLength: 1, tamperAAD: true, expectAuthenticationFailure: true); - failures += RunAndroidDiagnosticCase(dataLength: 0, additionalDataLength: 30, tamperAAD: true, expectAuthenticationFailure: true); - failures += RunAndroidDiagnosticCase(dataLength: 1, additionalDataLength: 1, tamperAAD: true, expectAuthenticationFailure: true); + failures += RunAndroidDiagnosticCase(diagnostics, dataLength: 0, additionalDataLength: 0, tamperAAD: false, expectAuthenticationFailure: false); + failures += RunAndroidDiagnosticCase(diagnostics, dataLength: 0, additionalDataLength: 1, tamperAAD: true, expectAuthenticationFailure: true); + failures += RunAndroidDiagnosticCase(diagnostics, dataLength: 0, additionalDataLength: 30, tamperAAD: true, expectAuthenticationFailure: true); + failures += RunAndroidDiagnosticCase(diagnostics, dataLength: 1, additionalDataLength: 1, tamperAAD: true, expectAuthenticationFailure: true); - Assert.Equal(0, failures); + Assert.True(failures == 0, diagnostics.ToString()); } private static int RunAndroidDiagnosticCase( + StringBuilder diagnostics, int dataLength, int additionalDataLength, bool tamperAAD, @@ -93,12 +100,17 @@ private static int RunAndroidDiagnosticCase( byte[] decrypted = new byte[dataLength]; Exception ex = Record.Exception(() => chaChaPoly.Decrypt(nonce, ciphertext, tag, decrypted, additionalData)); + bool decryptedMatchesPlaintext = plaintext.SequenceEqual(decrypted); - LogAndroidAeadDiagnostics( + AppendAndroidAeadDiagnostics( + diagnostics, nameof(AndroidDiagnostic_TamperedAadDecryptMatrix), nameof(ChaCha20Poly1305), dataLength, additionalDataLength, + tamperAAD, + expectAuthenticationFailure, + decryptedMatchesPlaintext, ex); if (expectAuthenticationFailure) @@ -114,27 +126,58 @@ private static bool ShouldLogAndroidAeadDiagnostics(int dataLength, int addition return PlatformDetection.IsAndroid && dataLength == 0 && additionalDataLength == 1; } - private static void LogAndroidAeadEnvironment(string algorithm) + private static string CreateAndroidAeadDiagnostics( + string testName, + string algorithm, + int dataLength, + int additionalDataLength, + bool tamperAAD, + bool expectAuthenticationFailure, + bool decryptedMatchesPlaintext, + Exception ex) + { + StringBuilder diagnostics = new(); + AppendAndroidAeadEnvironment(diagnostics, algorithm); + AppendAndroidAeadDiagnostics( + diagnostics, + testName, + algorithm, + dataLength, + additionalDataLength, + tamperAAD, + expectAuthenticationFailure, + decryptedMatchesPlaintext, + ex); + + return diagnostics.ToString(); + } + + private static void AppendAndroidAeadEnvironment(StringBuilder diagnostics, string algorithm) { - Console.WriteLine( - $"Android AEAD diagnostics for {algorithm}: " + - $"RuntimeIdentifier={RuntimeInformation.RuntimeIdentifier}, " + - $"ProcessArchitecture={RuntimeInformation.ProcessArchitecture}, " + - $"OSArchitecture={RuntimeInformation.OSArchitecture}, " + - $"IntPtr.Size={IntPtr.Size}, " + - $"Framework={RuntimeInformation.FrameworkDescription}"); + diagnostics.AppendLine($"Android AEAD diagnostics for {algorithm}:"); + diagnostics.AppendLine($"RuntimeIdentifier={RuntimeInformation.RuntimeIdentifier}"); + diagnostics.AppendLine($"ProcessArchitecture={RuntimeInformation.ProcessArchitecture}"); + diagnostics.AppendLine($"OSArchitecture={RuntimeInformation.OSArchitecture}"); + diagnostics.AppendLine($"IntPtr.Size={IntPtr.Size}"); + diagnostics.AppendLine($"Framework={RuntimeInformation.FrameworkDescription}"); } - private static void LogAndroidAeadDiagnostics( + private static void AppendAndroidAeadDiagnostics( + StringBuilder diagnostics, string testName, string algorithm, int dataLength, int additionalDataLength, + bool tamperAAD, + bool expectAuthenticationFailure, + bool decryptedMatchesPlaintext, Exception ex) { - Console.WriteLine( + diagnostics.AppendLine( $"Android AEAD diagnostics: test={testName}, algorithm={algorithm}, " + $"dataLength={dataLength}, additionalDataLength={additionalDataLength}, " + + $"tamperAAD={tamperAAD}, expectAuthenticationFailure={expectAuthenticationFailure}, " + + $"decryptedMatchesPlaintext={decryptedMatchesPlaintext}, " + $"exceptionType={ex?.GetType().FullName ?? ""}, " + $"exceptionMessage={ex?.Message ?? ""}"); } From 54309b53875cbaa89b94ec87413f0b0889487f14 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Thu, 4 Jun 2026 18:54:19 -0400 Subject: [PATCH 3/4] More diagnostics --- .../tests/ChaCha20Poly1305Tests.cs | 83 +++++++- .../pal_cipher.c | 182 ++++++++++++++++++ .../pal_cipher.h | 1 + 3 files changed, 256 insertions(+), 10 deletions(-) diff --git a/src/libraries/System.Security.Cryptography/tests/ChaCha20Poly1305Tests.cs b/src/libraries/System.Security.Cryptography/tests/ChaCha20Poly1305Tests.cs index 83e421d058d58a..40421c9b42f328 100644 --- a/src/libraries/System.Security.Cryptography/tests/ChaCha20Poly1305Tests.cs +++ b/src/libraries/System.Security.Cryptography/tests/ChaCha20Poly1305Tests.cs @@ -66,16 +66,26 @@ public static void AndroidDiagnostic_TamperedAadDecryptMatrix() AppendAndroidAeadEnvironment(diagnostics, nameof(ChaCha20Poly1305)); int failures = 0; - failures += RunAndroidDiagnosticCase(diagnostics, dataLength: 0, additionalDataLength: 0, tamperAAD: false, expectAuthenticationFailure: false); - failures += RunAndroidDiagnosticCase(diagnostics, dataLength: 0, additionalDataLength: 1, tamperAAD: true, expectAuthenticationFailure: true); - failures += RunAndroidDiagnosticCase(diagnostics, dataLength: 0, additionalDataLength: 30, tamperAAD: true, expectAuthenticationFailure: true); - failures += RunAndroidDiagnosticCase(diagnostics, dataLength: 1, additionalDataLength: 1, tamperAAD: true, expectAuthenticationFailure: true); + failures += RunAndroidDiagnosticCase(diagnostics, "baseline-valid-empty", dataLength: 0, additionalDataLength: 0, tamperAAD: false, expectAuthenticationFailure: false); - Assert.True(failures == 0, diagnostics.ToString()); + foreach (int additionalDataLength in new[] { 1, 2, 15, 16, 17, 30, 31, 32, 33 }) + { + failures += RunAndroidDiagnosticCase(diagnostics, $"empty-ciphertext-aad-{additionalDataLength}", dataLength: 0, additionalDataLength: additionalDataLength, tamperAAD: true, expectAuthenticationFailure: true); + } + + failures += RunAndroidDiagnosticCase(diagnostics, "nonempty-ciphertext-aad-1", dataLength: 1, additionalDataLength: 1, tamperAAD: true, expectAuthenticationFailure: true); + failures += RunAndroidDiagnosticPrimedCase(diagnostics, "target-after-aad30-auth-failure", primerDataLength: 0, primerAdditionalDataLength: 30); + failures += RunAndroidDiagnosticPrimedCase(diagnostics, "target-after-nonempty-auth-failure", primerDataLength: 1, primerAdditionalDataLength: 1); + + if (failures != 0) + { + Assert.Fail(diagnostics.ToString()); + } } private static int RunAndroidDiagnosticCase( StringBuilder diagnostics, + string caseName, int dataLength, int additionalDataLength, bool tamperAAD, @@ -100,18 +110,21 @@ private static int RunAndroidDiagnosticCase( byte[] decrypted = new byte[dataLength]; Exception ex = Record.Exception(() => chaChaPoly.Decrypt(nonce, ciphertext, tag, decrypted, additionalData)); + string nativeDiagnostic = GetLastAndroidCipherNativeDiagnostic(); bool decryptedMatchesPlaintext = plaintext.SequenceEqual(decrypted); AppendAndroidAeadDiagnostics( diagnostics, nameof(AndroidDiagnostic_TamperedAadDecryptMatrix), + caseName, nameof(ChaCha20Poly1305), dataLength, additionalDataLength, tamperAAD, expectAuthenticationFailure, decryptedMatchesPlaintext, - ex); + ex, + nativeDiagnostic); if (expectAuthenticationFailure) { @@ -121,6 +134,32 @@ private static int RunAndroidDiagnosticCase( return ex is null && plaintext.SequenceEqual(decrypted) ? 0 : 1; } + private static int RunAndroidDiagnosticPrimedCase( + StringBuilder diagnostics, + string caseName, + int primerDataLength, + int primerAdditionalDataLength) + { + int failures = 0; + failures += RunAndroidDiagnosticCase( + diagnostics, + $"{caseName}-primer", + primerDataLength, + primerAdditionalDataLength, + tamperAAD: true, + expectAuthenticationFailure: true); + + failures += RunAndroidDiagnosticCase( + diagnostics, + $"{caseName}-target", + dataLength: 0, + additionalDataLength: 1, + tamperAAD: true, + expectAuthenticationFailure: true); + + return failures; + } + private static bool ShouldLogAndroidAeadDiagnostics(int dataLength, int additionalDataLength) { return PlatformDetection.IsAndroid && dataLength == 0 && additionalDataLength == 1; @@ -141,13 +180,15 @@ private static string CreateAndroidAeadDiagnostics( AppendAndroidAeadDiagnostics( diagnostics, testName, + caseName: "theory", algorithm, dataLength, additionalDataLength, tamperAAD, expectAuthenticationFailure, decryptedMatchesPlaintext, - ex); + ex, + GetLastAndroidCipherNativeDiagnostic()); return diagnostics.ToString(); } @@ -165,23 +206,45 @@ private static void AppendAndroidAeadEnvironment(StringBuilder diagnostics, stri private static void AppendAndroidAeadDiagnostics( StringBuilder diagnostics, string testName, + string caseName, string algorithm, int dataLength, int additionalDataLength, bool tamperAAD, bool expectAuthenticationFailure, bool decryptedMatchesPlaintext, - Exception ex) + Exception ex, + string nativeDiagnostic) { diagnostics.AppendLine( - $"Android AEAD diagnostics: test={testName}, algorithm={algorithm}, " + + $"Android AEAD diagnostics: test={testName}, case={caseName}, algorithm={algorithm}, " + $"dataLength={dataLength}, additionalDataLength={additionalDataLength}, " + $"tamperAAD={tamperAAD}, expectAuthenticationFailure={expectAuthenticationFailure}, " + $"decryptedMatchesPlaintext={decryptedMatchesPlaintext}, " + $"exceptionType={ex?.GetType().FullName ?? ""}, " + - $"exceptionMessage={ex?.Message ?? ""}"); + $"exceptionMessage={ex?.Message ?? ""}, " + + $"nativeDiagnostic={nativeDiagnostic}"); + } + + private static string GetLastAndroidCipherNativeDiagnostic() + { + if (!PlatformDetection.IsAndroid) + { + return ""; + } + + byte[] buffer = new byte[2048]; + int length = AndroidCryptoNative_CipherGetLastDiagnostic(buffer, buffer.Length); + int terminator = System.Array.IndexOf(buffer, (byte)0); + int bytesToDecode = terminator >= 0 ? terminator : buffer.Length; + string value = Encoding.UTF8.GetString(buffer, 0, bytesToDecode); + + return value.Length == 0 ? $"" : value; } + [DllImport("libSystem.Security.Cryptography.Native.Android", EntryPoint = "AndroidCryptoNative_CipherGetLastDiagnostic")] + private static extern int AndroidCryptoNative_CipherGetLastDiagnostic(byte[] buffer, int bufferLength); + [Theory] [InlineData(0)] [InlineData(1)] diff --git a/src/native/libs/System.Security.Cryptography.Native.Android/pal_cipher.c b/src/native/libs/System.Security.Cryptography.Native.Android/pal_cipher.c index 348612dc2c618c..7707d1ba9e2b78 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Android/pal_cipher.c +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_cipher.c @@ -3,6 +3,8 @@ #include "pal_cipher.h" #include "pal_utilities.h" +#include +#include enum { @@ -26,6 +28,143 @@ CipherInfo* AndroidCryptoNative_ ## cipherId(void) \ return &info; \ } +static __thread char t_lastCipherDiagnostic[1024]; + +static void ClearLastCipherDiagnostic(void) +{ + t_lastCipherDiagnostic[0] = '\0'; +} + +static void CopyJavaString(JNIEnv* env, jstring value, char* destination, size_t destinationLength) +{ + if (destinationLength == 0) + return; + + destination[0] = '\0'; + + if (value == NULL) + return; + + const char* utf8 = (*env)->GetStringUTFChars(env, value, NULL); + if (utf8 == NULL) + { + (void)TryClearJNIExceptions(env); + return; + } + + snprintf(destination, destinationLength, "%s", utf8); + (*env)->ReleaseStringUTFChars(env, value, utf8); +} + +static void GetThrowableClassName(JNIEnv* env, jthrowable ex, char* destination, size_t destinationLength) +{ + if (destinationLength == 0) + return; + + destination[0] = '\0'; + + jclass classClass = (*env)->FindClass(env, "java/lang/Class"); + if (classClass == NULL) + { + (void)TryClearJNIExceptions(env); + return; + } + + jmethodID getName = (*env)->GetMethodID(env, classClass, "getName", "()Ljava/lang/String;"); + if (getName == NULL) + { + (*env)->DeleteLocalRef(env, classClass); + (void)TryClearJNIExceptions(env); + return; + } + + jclass exClass = (*env)->GetObjectClass(env, ex); + if (exClass == NULL) + { + (*env)->DeleteLocalRef(env, classClass); + (void)TryClearJNIExceptions(env); + return; + } + + jstring name = (jstring)(*env)->CallObjectMethod(env, exClass, getName); + if (name != NULL) + { + CopyJavaString(env, name, destination, destinationLength); + (*env)->DeleteLocalRef(env, name); + } + else + { + (void)TryClearJNIExceptions(env); + } + + (*env)->DeleteLocalRef(env, exClass); + (*env)->DeleteLocalRef(env, classClass); +} + +static void RecordCipherExceptionDiagnostic(JNIEnv* env, CipherCtx* ctx, const char* phase, int32_t inputLength, jthrowable ex) +{ + char className[256]; + char message[512]; + char causeClassName[256]; + char causeMessage[512]; + + className[0] = '\0'; + message[0] = '\0'; + causeClassName[0] = '\0'; + causeMessage[0] = '\0'; + + if (ex != NULL) + { + GetThrowableClassName(env, ex, className, sizeof(className)); + + jstring javaMessage = (jstring)(*env)->CallObjectMethod(env, ex, g_ThrowableGetMessage); + if (javaMessage != NULL) + { + CopyJavaString(env, javaMessage, message, sizeof(message)); + (*env)->DeleteLocalRef(env, javaMessage); + } + else + { + (void)TryClearJNIExceptions(env); + } + + jthrowable cause = (jthrowable)(*env)->CallObjectMethod(env, ex, g_ThrowableGetCause); + if (cause != NULL) + { + GetThrowableClassName(env, cause, causeClassName, sizeof(causeClassName)); + + jstring javaCauseMessage = (jstring)(*env)->CallObjectMethod(env, cause, g_ThrowableGetMessage); + if (javaCauseMessage != NULL) + { + CopyJavaString(env, javaCauseMessage, causeMessage, sizeof(causeMessage)); + (*env)->DeleteLocalRef(env, javaCauseMessage); + } + else + { + (void)TryClearJNIExceptions(env); + } + + (*env)->DeleteLocalRef(env, cause); + } + else + { + (void)TryClearJNIExceptions(env); + } + } + + snprintf( + t_lastCipherDiagnostic, + sizeof(t_lastCipherDiagnostic), + "nativePhase=%s; cipher=%s; inputLength=%d; javaExceptionClass=%s; javaExceptionMessage=%s; javaCauseClass=%s; javaCauseMessage=%s", + phase, + ctx != NULL && ctx->type != NULL ? ctx->type->name : "", + inputLength, + className[0] != '\0' ? className : "", + message[0] != '\0' ? message : "", + causeClassName[0] != '\0' ? causeClassName : "", + causeMessage[0] != '\0' ? causeMessage : ""); +} + DEFINE_CIPHER(Aes128Ecb, 128, "AES/ECB/NoPadding", CIPHER_NONE) DEFINE_CIPHER(Aes128Cbc, 128, "AES/CBC/NoPadding", CIPHER_REQUIRES_IV) DEFINE_CIPHER(Aes128Cfb8, 128, "AES/CFB8/NoPadding", CIPHER_REQUIRES_IV) @@ -53,6 +192,28 @@ DEFINE_CIPHER(Des3Cfb8, 128, "DESede/CFB8/NoPadding", CIPHER_REQUIRES_IV DEFINE_CIPHER(Des3Cfb64, 128, "DESede/CFB/NoPadding", CIPHER_REQUIRES_IV) DEFINE_CIPHER(ChaCha20Poly1305, 256, "ChaCha20/Poly1305/NoPadding", CIPHER_REQUIRES_IV) +int32_t AndroidCryptoNative_CipherGetLastDiagnostic(uint8_t* buffer, int32_t bufferLength) +{ + if (bufferLength < 0) + return FAIL; + + size_t diagnosticLength = strlen(t_lastCipherDiagnostic); + + if (buffer != NULL && bufferLength > 0) + { + size_t bytesToCopy = diagnosticLength; + if (bytesToCopy >= (size_t)bufferLength) + { + bytesToCopy = (size_t)bufferLength - 1; + } + + memcpy(buffer, t_lastCipherDiagnostic, bytesToCopy); + buffer[bytesToCopy] = '\0'; + } + + return (int32_t)diagnosticLength; +} + // // We don't have to check whether `CipherInfo` arguments are valid pointers, as these functions will be called after the // context is created and the type stored in `CipherInfo` is asserted to be not NULL on creation time. Managed code @@ -184,6 +345,8 @@ int32_t AndroidCryptoNative_CipherSetKeyAndIV(CipherCtx* ctx, uint8_t* key, uint if (!ctx) return FAIL; + ClearLastCipherDiagnostic(); + // input: 0 for Decrypt, 1 for Encrypt, -1 leave untouched // Cipher: 2 for Decrypt, 1 for Encrypt, N/A if (enc != -1) @@ -245,6 +408,14 @@ int32_t AndroidCryptoNative_CipherUpdateAAD(CipherCtx* ctx, uint8_t* in, int32_t if ((*env)->ExceptionCheck(env)) { + jthrowable ex = NULL; + (void)TryGetJNIException(env, &ex, false); + RecordCipherExceptionDiagnostic(env, ctx, "updateAAD", inl, ex); + if (ex != NULL) + { + (*env)->Throw(env, ex); + (*env)->DeleteLocalRef(env, ex); + } LOG_ERROR("Cipher.updateAAD failed for %s with input length %d", ctx->type->name, inl); return CheckJNIExceptions(env) ? FAIL : SUCCESS; } @@ -283,6 +454,14 @@ int32_t AndroidCryptoNative_CipherUpdate(CipherCtx* ctx, uint8_t* outm, int32_t* if ((*env)->ExceptionCheck(env)) { + jthrowable ex = NULL; + (void)TryGetJNIException(env, &ex, false); + RecordCipherExceptionDiagnostic(env, ctx, "update", inl, ex); + if (ex != NULL) + { + (*env)->Throw(env, ex); + (*env)->DeleteLocalRef(env, ex); + } LOG_ERROR("Cipher.update failed for %s with input length %d", ctx->type->name, inl); return CheckJNIExceptions(env) ? FAIL : SUCCESS; } @@ -335,10 +514,13 @@ int32_t AndroidCryptoNative_AeadCipherFinalEx(CipherCtx* ctx, uint8_t* outm, int { if (ex == NULL) { + RecordCipherExceptionDiagnostic(env, ctx, "doFinal", 0, NULL); LOG_ERROR("Cipher.doFinal failed for %s without an exception object", ctx->type->name); return FAIL; } + RecordCipherExceptionDiagnostic(env, ctx, "doFinal", 0, ex); + if ((*env)->IsInstanceOf(env, ex, g_AEADBadTagExceptionClass)) { *authTagMismatch = 1; diff --git a/src/native/libs/System.Security.Cryptography.Native.Android/pal_cipher.h b/src/native/libs/System.Security.Cryptography.Native.Android/pal_cipher.h index dc1bbe2211df53..0c573d6ec99fa0 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Android/pal_cipher.h +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_cipher.h @@ -24,6 +24,7 @@ typedef struct CipherCtx uint8_t* iv; } CipherCtx; +PALEXPORT int32_t AndroidCryptoNative_CipherGetLastDiagnostic(uint8_t* buffer, int32_t bufferLength); PALEXPORT int32_t AndroidCryptoNative_CipherIsSupported(CipherInfo* type); PALEXPORT CipherCtx* AndroidCryptoNative_CipherCreate(CipherInfo* type, uint8_t* key, int32_t keySizeInBits, uint8_t* iv, int32_t enc); PALEXPORT CipherCtx* AndroidCryptoNative_CipherCreatePartial(CipherInfo* type); From 0f7cd619ac178ae7edc98502146f25241f3b38e5 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Thu, 4 Jun 2026 21:17:12 -0400 Subject: [PATCH 4/4] What is even happening here? --- .../RSA/EncryptDecrypt.cs | 2 +- .../RSA/ImportExport.cs | 7 +++- .../RSA/SignVerify.cs | 4 +- .../tests/ChaCha20Poly1305Tests.cs | 39 ++++++++++++++++++- 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/RSA/EncryptDecrypt.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/RSA/EncryptDecrypt.cs index 09a7e5bb790299..882f61873624b4 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/RSA/EncryptDecrypt.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/RSA/EncryptDecrypt.cs @@ -644,7 +644,7 @@ public void RsaDecryptAfterExport() Assert.Equal(TestData.HelloBytes, output); } - [Fact] + [ConditionalFact(typeof(ImportExport), nameof(ImportExport.Supports16384))] public void LargeKeyCryptRoundtrip() { byte[] output; diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/RSA/ImportExport.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/RSA/ImportExport.cs index 909c455a8134b2..044b1fd442419b 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/RSA/ImportExport.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/RSA/ImportExport.cs @@ -69,7 +69,7 @@ public static void PaddedExport() RSATestHelpers.AssertKeyEquals(diminishedDPParameters, exported); } - [Fact] + [ConditionalFact(typeof(ImportExport), nameof(ImportExport.Supports16384))] public static void LargeKeyImportExport() { RSAParameters imported = TestData.RSA16384Params; @@ -367,6 +367,11 @@ internal static RSAParameters MakePublic(in RSAParameters rsaParams) private static bool TestRsa16384() { + if (PlatformDetection.IsAndroid) + { + return false; + } + try { using (RSA rsa = RSAFactory.Create()) diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/RSA/SignVerify.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/RSA/SignVerify.cs index c4e33527cdc277..a409e18b677986 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/RSA/SignVerify.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/RSA/SignVerify.cs @@ -1022,7 +1022,7 @@ public void VerifyExpectedSignature_PssSha256_RSA2048() modulus2048Signature); } - [Fact] + [ConditionalFact(typeof(ImportExport), nameof(ImportExport.Supports16384))] public void VerifyExpectedSignature_PssSha256_RSA16384() { byte[] modulus2048Signature = ( @@ -1098,7 +1098,7 @@ public void VerifyExpectedSignature_PssSha256_RSA16384() modulus2048Signature); } - [Fact] + [ConditionalFact(typeof(ImportExport), nameof(ImportExport.Supports16384))] public void VerifyExpectedSignature_PssSha384() { byte[] bigModulusSignature = ( diff --git a/src/libraries/System.Security.Cryptography/tests/ChaCha20Poly1305Tests.cs b/src/libraries/System.Security.Cryptography/tests/ChaCha20Poly1305Tests.cs index 40421c9b42f328..92efb88b8835c6 100644 --- a/src/libraries/System.Security.Cryptography/tests/ChaCha20Poly1305Tests.cs +++ b/src/libraries/System.Security.Cryptography/tests/ChaCha20Poly1305Tests.cs @@ -68,7 +68,7 @@ public static void AndroidDiagnostic_TamperedAadDecryptMatrix() int failures = 0; failures += RunAndroidDiagnosticCase(diagnostics, "baseline-valid-empty", dataLength: 0, additionalDataLength: 0, tamperAAD: false, expectAuthenticationFailure: false); - foreach (int additionalDataLength in new[] { 1, 2, 15, 16, 17, 30, 31, 32, 33 }) + foreach (int additionalDataLength in Enumerable.Range(1, 33)) { failures += RunAndroidDiagnosticCase(diagnostics, $"empty-ciphertext-aad-{additionalDataLength}", dataLength: 0, additionalDataLength: additionalDataLength, tamperAAD: true, expectAuthenticationFailure: true); } @@ -76,6 +76,10 @@ public static void AndroidDiagnostic_TamperedAadDecryptMatrix() failures += RunAndroidDiagnosticCase(diagnostics, "nonempty-ciphertext-aad-1", dataLength: 1, additionalDataLength: 1, tamperAAD: true, expectAuthenticationFailure: true); failures += RunAndroidDiagnosticPrimedCase(diagnostics, "target-after-aad30-auth-failure", primerDataLength: 0, primerAdditionalDataLength: 30); failures += RunAndroidDiagnosticPrimedCase(diagnostics, "target-after-nonempty-auth-failure", primerDataLength: 1, primerAdditionalDataLength: 1); + failures += RunAndroidDiagnosticOversizedRsaPrimedCase(diagnostics, "target-aad1-after-oversized-rsa-import", targetAdditionalDataLength: 1); + failures += RunAndroidDiagnosticOversizedRsaPrimedCase(diagnostics, "target-aad2-after-oversized-rsa-import", targetAdditionalDataLength: 2); + failures += RunAndroidDiagnosticOversizedRsaPrimedCase(diagnostics, "target-aad15-after-oversized-rsa-import", targetAdditionalDataLength: 15); + failures += RunAndroidDiagnosticOversizedRsaPrimedCase(diagnostics, "target-aad30-after-oversized-rsa-import", targetAdditionalDataLength: 30); if (failures != 0) { @@ -160,6 +164,39 @@ private static int RunAndroidDiagnosticPrimedCase( return failures; } + private static int RunAndroidDiagnosticOversizedRsaPrimedCase( + StringBuilder diagnostics, + string caseName, + int targetAdditionalDataLength) + { + Exception rsaException = Record.Exception(() => + { + using RSA rsa = RSA.Create(); + byte[] modulus = new byte[2048]; + modulus[0] = 0x80; + modulus[^1] = 0x01; + + rsa.ImportParameters(new RSAParameters + { + Modulus = modulus, + Exponent = new byte[] { 0x01, 0x00, 0x01 }, + }); + }); + + diagnostics.AppendLine( + $"Android AEAD primer diagnostics: case={caseName}, operation=RSA.ImportParameters, " + + $"modulusBytes=2048, exceptionType={rsaException?.GetType().FullName ?? ""}, " + + $"exceptionMessage={rsaException?.Message ?? ""}"); + + return RunAndroidDiagnosticCase( + diagnostics, + $"{caseName}-target", + dataLength: 0, + additionalDataLength: targetAdditionalDataLength, + tamperAAD: true, + expectAuthenticationFailure: true); + } + private static bool ShouldLogAndroidAeadDiagnostics(int dataLength, int additionalDataLength) { return PlatformDetection.IsAndroid && dataLength == 0 && additionalDataLength == 1;