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 ff618943d0546f..92efb88b8835c6 100644 --- a/src/libraries/System.Security.Cryptography/tests/ChaCha20Poly1305Tests.cs +++ b/src/libraries/System.Security.Cryptography/tests/ChaCha20Poly1305Tests.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; +using System.Text; using Test.Cryptography; using Xunit; @@ -35,11 +37,251 @@ 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)) + { + Assert.Fail( + CreateAndroidAeadDiagnostics( + nameof(EncryptTamperAADDecrypt), + nameof(ChaCha20Poly1305), + dataLength, + additionalDataLength, + tamperAAD: true, + expectAuthenticationFailure: true, + decryptedMatchesPlaintext: false, + ex)); + throw; + } } } + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsAndroid))] + public static void AndroidDiagnostic_TamperedAadDecryptMatrix() + { + StringBuilder diagnostics = new(); + AppendAndroidAeadEnvironment(diagnostics, nameof(ChaCha20Poly1305)); + + int failures = 0; + failures += RunAndroidDiagnosticCase(diagnostics, "baseline-valid-empty", dataLength: 0, additionalDataLength: 0, tamperAAD: false, expectAuthenticationFailure: false); + + foreach (int additionalDataLength in Enumerable.Range(1, 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); + 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) + { + Assert.Fail(diagnostics.ToString()); + } + } + + private static int RunAndroidDiagnosticCase( + StringBuilder diagnostics, + string caseName, + 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)); + string nativeDiagnostic = GetLastAndroidCipherNativeDiagnostic(); + bool decryptedMatchesPlaintext = plaintext.SequenceEqual(decrypted); + + AppendAndroidAeadDiagnostics( + diagnostics, + nameof(AndroidDiagnostic_TamperedAadDecryptMatrix), + caseName, + nameof(ChaCha20Poly1305), + dataLength, + additionalDataLength, + tamperAAD, + expectAuthenticationFailure, + decryptedMatchesPlaintext, + ex, + nativeDiagnostic); + + if (expectAuthenticationFailure) + { + return ex is AuthenticationTagMismatchException ? 0 : 1; + } + + 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 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; + } + + 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, + caseName: "theory", + algorithm, + dataLength, + additionalDataLength, + tamperAAD, + expectAuthenticationFailure, + decryptedMatchesPlaintext, + ex, + GetLastAndroidCipherNativeDiagnostic()); + + return diagnostics.ToString(); + } + + private static void AppendAndroidAeadEnvironment(StringBuilder diagnostics, string algorithm) + { + 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 AppendAndroidAeadDiagnostics( + StringBuilder diagnostics, + string testName, + string caseName, + string algorithm, + int dataLength, + int additionalDataLength, + bool tamperAAD, + bool expectAuthenticationFailure, + bool decryptedMatchesPlaintext, + Exception ex, + string nativeDiagnostic) + { + diagnostics.AppendLine( + $"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 ?? ""}, " + + $"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 a775b0f7552888..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) @@ -242,7 +405,22 @@ 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)) + { + 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; + } + + return SUCCESS; } int32_t AndroidCryptoNative_CipherUpdate(CipherCtx* ctx, uint8_t* outm, int32_t* outl, uint8_t* in, int32_t inl) @@ -273,7 +451,22 @@ int32_t AndroidCryptoNative_CipherUpdate(CipherCtx* ctx, uint8_t* outm, int32_t* } (*env)->DeleteLocalRef(env, inDataBytes); - return CheckJNIExceptions(env) ? FAIL : SUCCESS; + + 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; + } + + return SUCCESS; } int32_t AndroidCryptoNative_CipherFinalEx(CipherCtx* ctx, uint8_t* outm, int32_t* outl) @@ -317,18 +510,27 @@ 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) { + 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; } + LOG_ERROR( + "Cipher.doFinal failed for %s; is AEADBadTagException=%d", + ctx->type->name, + *authTagMismatch); + (*env)->DeleteLocalRef(env, ex); return FAIL; } 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);