From cf96d67c45d5572f8e896b7ab7517484ccecfb30 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Sat, 15 May 2021 13:13:38 -0400 Subject: [PATCH 1/5] Initial support for ChaChaPoly1305 on Android --- .../Interop.Cipher.cs | 3 + .../pal_cipher.c | 61 ++++--- .../pal_cipher.h | 1 + ...em.Security.Cryptography.Algorithms.csproj | 2 +- .../Cryptography/ChaCha20Poly1305.Android.cs | 160 ++++++++++++++++++ .../tests/ChaCha20Poly1305Tests.cs | 4 + 6 files changed, 204 insertions(+), 27 deletions(-) create mode 100644 src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/ChaCha20Poly1305.Android.cs diff --git a/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.Cipher.cs b/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.Cipher.cs index bed79c75f775a3..d7f5687f9f27a9 100644 --- a/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.Cipher.cs +++ b/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.Cipher.cs @@ -216,6 +216,9 @@ internal static extern bool CipherSetTagLength( [DllImport(Libraries.AndroidCryptoNative, EntryPoint = "AndroidCryptoNative_RC2Ecb")] internal static extern IntPtr EvpRC2Ecb(); + [DllImport(Libraries.AndroidCryptoNative, EntryPoint = "AndroidCryptoNative_ChaCha20Poly1305")] + internal static extern IntPtr EvpChaCha20Poly1305(); + internal enum EvpCipherDirection : int { NoChange = -1, diff --git a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_cipher.c b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_cipher.c index 0544e8f9b85af2..f2a92c2e45e652 100644 --- a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_cipher.c +++ b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_cipher.c @@ -26,31 +26,32 @@ CipherInfo* AndroidCryptoNative_ ## cipherId() \ return &info; \ } -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) -DEFINE_CIPHER(Aes128Cfb128, 128, "AES/CFB128/NoPadding", CIPHER_REQUIRES_IV) -DEFINE_CIPHER(Aes128Gcm, 128, "AES/GCM/NoPadding", CIPHER_HAS_TAG | CIPHER_REQUIRES_IV) -DEFINE_CIPHER(Aes128Ccm, 128, "AES/CCM/NoPadding", CIPHER_HAS_TAG | CIPHER_REQUIRES_IV) -DEFINE_CIPHER(Aes192Ecb, 192, "AES/ECB/NoPadding", CIPHER_NONE) -DEFINE_CIPHER(Aes192Cbc, 192, "AES/CBC/NoPadding", CIPHER_REQUIRES_IV) -DEFINE_CIPHER(Aes192Cfb8, 192, "AES/CFB8/NoPadding", CIPHER_REQUIRES_IV) -DEFINE_CIPHER(Aes192Cfb128, 192, "AES/CFB128/NoPadding", CIPHER_REQUIRES_IV) -DEFINE_CIPHER(Aes192Gcm, 192, "AES/GCM/NoPadding", CIPHER_HAS_TAG | CIPHER_REQUIRES_IV) -DEFINE_CIPHER(Aes192Ccm, 192, "AES/CCM/NoPadding", CIPHER_HAS_TAG | CIPHER_REQUIRES_IV) -DEFINE_CIPHER(Aes256Ecb, 256, "AES/ECB/NoPadding", CIPHER_NONE) -DEFINE_CIPHER(Aes256Cbc, 256, "AES/CBC/NoPadding", CIPHER_REQUIRES_IV) -DEFINE_CIPHER(Aes256Cfb8, 256, "AES/CFB8/NoPadding", CIPHER_REQUIRES_IV) -DEFINE_CIPHER(Aes256Cfb128, 256, "AES/CFB128/NoPadding", CIPHER_REQUIRES_IV) -DEFINE_CIPHER(Aes256Gcm, 256, "AES/GCM/NoPadding", CIPHER_HAS_TAG | CIPHER_REQUIRES_IV) -DEFINE_CIPHER(Aes256Ccm, 256, "AES/CCM/NoPadding", CIPHER_HAS_TAG | CIPHER_REQUIRES_IV) -DEFINE_CIPHER(DesEcb, 64, "DES/ECB/NoPadding", CIPHER_NONE) -DEFINE_CIPHER(DesCbc, 64, "DES/CBC/NoPadding", CIPHER_REQUIRES_IV) -DEFINE_CIPHER(DesCfb8, 64, "DES/CFB8/NoPadding", CIPHER_REQUIRES_IV) -DEFINE_CIPHER(Des3Ecb, 128, "DESede/ECB/NoPadding", CIPHER_NONE) -DEFINE_CIPHER(Des3Cbc, 128, "DESede/CBC/NoPadding", CIPHER_REQUIRES_IV) -DEFINE_CIPHER(Des3Cfb8, 128, "DESede/CFB8/NoPadding", CIPHER_REQUIRES_IV) -DEFINE_CIPHER(Des3Cfb64, 128, "DESede/CFB/NoPadding", CIPHER_REQUIRES_IV) +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) +DEFINE_CIPHER(Aes128Cfb128, 128, "AES/CFB128/NoPadding", CIPHER_REQUIRES_IV) +DEFINE_CIPHER(Aes128Gcm, 128, "AES/GCM/NoPadding", CIPHER_HAS_TAG | CIPHER_REQUIRES_IV) +DEFINE_CIPHER(Aes128Ccm, 128, "AES/CCM/NoPadding", CIPHER_HAS_TAG | CIPHER_REQUIRES_IV) +DEFINE_CIPHER(Aes192Ecb, 192, "AES/ECB/NoPadding", CIPHER_NONE) +DEFINE_CIPHER(Aes192Cbc, 192, "AES/CBC/NoPadding", CIPHER_REQUIRES_IV) +DEFINE_CIPHER(Aes192Cfb8, 192, "AES/CFB8/NoPadding", CIPHER_REQUIRES_IV) +DEFINE_CIPHER(Aes192Cfb128, 192, "AES/CFB128/NoPadding", CIPHER_REQUIRES_IV) +DEFINE_CIPHER(Aes192Gcm, 192, "AES/GCM/NoPadding", CIPHER_HAS_TAG | CIPHER_REQUIRES_IV) +DEFINE_CIPHER(Aes192Ccm, 192, "AES/CCM/NoPadding", CIPHER_HAS_TAG | CIPHER_REQUIRES_IV) +DEFINE_CIPHER(Aes256Ecb, 256, "AES/ECB/NoPadding", CIPHER_NONE) +DEFINE_CIPHER(Aes256Cbc, 256, "AES/CBC/NoPadding", CIPHER_REQUIRES_IV) +DEFINE_CIPHER(Aes256Cfb8, 256, "AES/CFB8/NoPadding", CIPHER_REQUIRES_IV) +DEFINE_CIPHER(Aes256Cfb128, 256, "AES/CFB128/NoPadding", CIPHER_REQUIRES_IV) +DEFINE_CIPHER(Aes256Gcm, 256, "AES/GCM/NoPadding", CIPHER_HAS_TAG | CIPHER_REQUIRES_IV) +DEFINE_CIPHER(Aes256Ccm, 256, "AES/CCM/NoPadding", CIPHER_HAS_TAG | CIPHER_REQUIRES_IV) +DEFINE_CIPHER(DesEcb, 64, "DES/ECB/NoPadding", CIPHER_NONE) +DEFINE_CIPHER(DesCbc, 64, "DES/CBC/NoPadding", CIPHER_REQUIRES_IV) +DEFINE_CIPHER(DesCfb8, 64, "DES/CFB8/NoPadding", CIPHER_REQUIRES_IV) +DEFINE_CIPHER(Des3Ecb, 128, "DESede/ECB/NoPadding", CIPHER_NONE) +DEFINE_CIPHER(Des3Cbc, 128, "DESede/CBC/NoPadding", CIPHER_REQUIRES_IV) +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_HAS_TAG | CIPHER_REQUIRES_IV) // // We don't have to check whether `CipherInfo` arguments are valid pointers, as these functions will be called after the @@ -138,7 +139,15 @@ ARGS_NON_NULL_ALL static int32_t ReinitializeCipher(CipherCtx* ctx) { jbyteArray ivBytes = make_java_byte_array(env, ctx->ivLength); (*env)->SetByteArrayRegion(env, ivBytes, 0, ctx->ivLength, (jbyte*)ctx->iv); - if (HasTag(ctx->type)) + + // ChaChaPoly1305 does not allow configuration of the tag length and + // we don't permit user-defined counters, so we can use IvParameterSpec + // for ChaChaPoly1305. + if (strcmp("ChaCha20/Poly1305/NoPadding", ctx->type->name) == 0) + { + ivPsObj = (*env)->NewObject(env, g_ivPsClass, g_ivPsCtor, ivBytes); + } + else if (HasTag(ctx->type)) { ivPsObj = (*env)->NewObject(env, g_GCMParameterSpecClass, g_GCMParameterSpecCtor, ctx->tagLength * 8, ivBytes); } diff --git a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_cipher.h b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_cipher.h index b9b9e1e45e29ba..732eca25d1db69 100644 --- a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_cipher.h +++ b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_cipher.h @@ -60,3 +60,4 @@ PALEXPORT CipherInfo* AndroidCryptoNative_Des3Cfb64(void); PALEXPORT CipherInfo* AndroidCryptoNative_DesEcb(void); PALEXPORT CipherInfo* AndroidCryptoNative_DesCfb8(void); PALEXPORT CipherInfo* AndroidCryptoNative_DesCbc(void); +PALEXPORT CipherInfo* AndroidCryptoNative_ChaCha20Poly1305(void); diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/System.Security.Cryptography.Algorithms.csproj b/src/libraries/System.Security.Cryptography.Algorithms/src/System.Security.Cryptography.Algorithms.csproj index 833822b03c7fdc..b829552cc56cc2 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/src/System.Security.Cryptography.Algorithms.csproj +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/System.Security.Cryptography.Algorithms.csproj @@ -697,7 +697,7 @@ - + diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/ChaCha20Poly1305.Android.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/ChaCha20Poly1305.Android.cs new file mode 100644 index 00000000000000..d8c53f398b7431 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/ChaCha20Poly1305.Android.cs @@ -0,0 +1,160 @@ +// 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; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Win32.SafeHandles; + +namespace System.Security.Cryptography +{ + public sealed partial class ChaCha20Poly1305 + { + private SafeEvpCipherCtxHandle _ctxHandle; + + public static bool IsSupported => true; + + [MemberNotNull(nameof(_ctxHandle))] + private void ImportKey(ReadOnlySpan key) + { + // Constructors should check key size before calling ImportKey. + Debug.Assert(key.Length == 256 / 8); + _ctxHandle = Interop.Crypto.EvpCipherCreatePartial(Interop.Crypto.EvpChaCha20Poly1305()); + + Interop.Crypto.CheckValidOpenSslHandle(_ctxHandle); + Interop.Crypto.EvpCipherSetKeyAndIV( + _ctxHandle, + key, + Span.Empty, + Interop.Crypto.EvpCipherDirection.NoChange); + + const int ChaChaNonceSize = 96 / 8; + Interop.Crypto.CipherSetNonceLength(_ctxHandle, ChaChaNonceSize); + } + + private void EncryptCore( + ReadOnlySpan nonce, + ReadOnlySpan plaintext, + Span ciphertext, + Span tag, + ReadOnlySpan associatedData = default) + { + + Interop.Crypto.EvpCipherSetKeyAndIV( + _ctxHandle, + Span.Empty, + nonce, + Interop.Crypto.EvpCipherDirection.Encrypt); + + if (!associatedData.IsEmpty) + { + Interop.Crypto.CipherUpdateAAD(_ctxHandle, associatedData); + } + + byte[]? rented = null; + int ciphertextAndTagLength = checked(ciphertext.Length + tag.Length); + + try + { + // Arbitrary limit. + const int StackAllocMax = 128; + Span ciphertextAndTag = stackalloc byte[StackAllocMax]; + + if (ciphertextAndTagLength > StackAllocMax) + { + rented = CryptoPool.Rent(ciphertextAndTagLength); + ciphertextAndTag = rented; + } + + ciphertextAndTag = ciphertextAndTag.Slice(0, ciphertextAndTagLength); + + if (!Interop.Crypto.EvpCipherUpdate(_ctxHandle, ciphertextAndTag, out int ciphertextBytesWritten, plaintext)) + { + throw new CryptographicException(); + } + + if (!Interop.Crypto.EvpCipherFinalEx( + _ctxHandle, + ciphertextAndTag.Slice(ciphertextBytesWritten), + out int bytesWritten)) + { + throw new CryptographicException(); + } + + ciphertextBytesWritten += bytesWritten; + + // NOTE: Android appends tag to the end of the ciphertext in case of ChaCha20Poly1305 and "encryption" mode + + if (ciphertextBytesWritten != ciphertextAndTagLength) + { + Debug.Fail($"ChaCha20Poly1305 encrypt wrote {ciphertextBytesWritten} of {ciphertextAndTagLength} bytes."); + throw new CryptographicException(); + } + + ciphertextAndTag.Slice(0, ciphertext.Length).CopyTo(ciphertext); + ciphertextAndTag.Slice(ciphertext.Length).CopyTo(tag); + } + finally + { + if (rented is not null) + { + CryptoPool.Return(rented, clearSize: ciphertextAndTagLength); + } + } + } + + private void DecryptCore( + ReadOnlySpan nonce, + ReadOnlySpan ciphertext, + ReadOnlySpan tag, + Span plaintext, + ReadOnlySpan associatedData) + { + Interop.Crypto.EvpCipherSetKeyAndIV( + _ctxHandle, + ReadOnlySpan.Empty, + nonce, + Interop.Crypto.EvpCipherDirection.Decrypt); + + if (!associatedData.IsEmpty) + { + Interop.Crypto.CipherUpdateAAD(_ctxHandle, associatedData); + } + + if (!Interop.Crypto.EvpCipherUpdate(_ctxHandle, plaintext, out int plaintextBytesWritten, ciphertext)) + { + CryptographicOperations.ZeroMemory(plaintext); + throw new CryptographicException(); + } + + if (!Interop.Crypto.EvpCipherUpdate(_ctxHandle, plaintext.Slice(plaintextBytesWritten), out int bytesWritten, tag)) + { + CryptographicOperations.ZeroMemory(plaintext); + throw new CryptographicException(); + } + + plaintextBytesWritten += bytesWritten; + + if (!Interop.Crypto.EvpCipherFinalEx( + _ctxHandle, + plaintext.Slice(plaintextBytesWritten), + out bytesWritten)) + { + CryptographicOperations.ZeroMemory(plaintext); + throw new CryptographicException(SR.Cryptography_AuthTagMismatch); + } + + plaintextBytesWritten += bytesWritten; + + if (plaintextBytesWritten != plaintext.Length) + { + Debug.Fail($"ChaCha20Poly1305 decrypt wrote {plaintextBytesWritten} of {plaintext.Length} bytes."); + throw new CryptographicException(); + } + } + + public void Dispose() + { + _ctxHandle.Dispose(); + } + } +} diff --git a/src/libraries/System.Security.Cryptography.Algorithms/tests/ChaCha20Poly1305Tests.cs b/src/libraries/System.Security.Cryptography.Algorithms/tests/ChaCha20Poly1305Tests.cs index 5a96943f19ddea..445450c6116320 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/tests/ChaCha20Poly1305Tests.cs +++ b/src/libraries/System.Security.Cryptography.Algorithms/tests/ChaCha20Poly1305Tests.cs @@ -453,6 +453,10 @@ public static void CheckIsSupported() const int OpenSslChaChaMinimumVersion = 0x1_01_00_00_F; //major_minor_fix_patch_status expectedIsSupported = SafeEvpPKeyHandle.OpenSslVersion >= OpenSslChaChaMinimumVersion; } + else if (PlatformDetection.IsAndroid) + { + expectedIsSupported = true; + } Assert.Equal(expectedIsSupported, ChaCha20Poly1305.IsSupported); } From 3826605913f6714bec5ffee84405335bd5f1ce1c Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Sat, 15 May 2021 14:10:35 -0400 Subject: [PATCH 2/5] Implement IsSupported correctly for ChaCha20Poly1305 on Android --- .../Interop.Cipher.cs | 4 ++++ .../pal_cipher.c | 18 ++++++++++++++++++ .../pal_cipher.h | 1 + .../Cryptography/ChaCha20Poly1305.Android.cs | 7 +++---- .../tests/ChaCha20Poly1305Tests.cs | 8 ++++---- 5 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.Cipher.cs b/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.Cipher.cs index d7f5687f9f27a9..c1abefa490d75f 100644 --- a/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.Cipher.cs +++ b/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.Cipher.cs @@ -135,6 +135,10 @@ internal static extern bool CipherSetTagLength( SafeEvpCipherCtxHandle ctx, int tagLength); + [DllImport(Libraries.AndroidCryptoNative, EntryPoint = "AndroidCryptoNative_CipherIsSupported")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool CipherIsSupported(IntPtr cipher); + [DllImport(Libraries.AndroidCryptoNative, EntryPoint = "AndroidCryptoNative_Aes128Ecb")] internal static extern IntPtr EvpAes128Ecb(); diff --git a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_cipher.c b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_cipher.c index f2a92c2e45e652..fdf4d154e7e3be 100644 --- a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_cipher.c +++ b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_cipher.c @@ -76,6 +76,24 @@ ARGS_NON_NULL_ALL static jobject GetAlgorithmName(JNIEnv* env, CipherInfo* type) return make_java_string(env, type->name); } +int32_t AndroidCryptoNative_CipherIsSupported(CipherInfo* type) +{ + abort_if_invalid_pointer_argument (type); + + JNIEnv* env = GetJNIEnv(); + jobject algName = GetAlgorithmName(env, type); + if (!algName) + return FAIL; + + jobject cipher = (*env)->CallStaticObjectMethod(env, g_cipherClass, g_cipherGetInstanceMethod, algName); + (*env)->DeleteLocalRef(env, algName); + (*env)->DeleteLocalRef(env, cipher); + + // If we were able to call Cipher.getInstance without an exception, like NoSuchAlgorithmException, + // then the algorithm is supported. + return CheckJNIExceptions(env) ? FAIL : SUCCESS; +} + CipherCtx* AndroidCryptoNative_CipherCreatePartial(CipherInfo* type) { abort_if_invalid_pointer_argument (type); diff --git a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_cipher.h b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_cipher.h index 732eca25d1db69..fda90fcc6284f3 100644 --- a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_cipher.h +++ b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_cipher.h @@ -24,6 +24,7 @@ typedef struct CipherCtx uint8_t* iv; } CipherCtx; +PALEXPORT int32_t AndroidCryptoNative_CipherIsSupported(CipherInfo* type); PALEXPORT CipherCtx* AndroidCryptoNative_CipherCreate(CipherInfo* type, uint8_t* key, int32_t keySizeInBits, int32_t effectiveKeyLength, uint8_t* iv, int32_t enc); PALEXPORT CipherCtx* AndroidCryptoNative_CipherCreatePartial(CipherInfo* type); PALEXPORT int32_t AndroidCryptoNative_CipherSetTagLength(CipherCtx* ctx, int32_t tagLength); diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/ChaCha20Poly1305.Android.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/ChaCha20Poly1305.Android.cs index d8c53f398b7431..a36599202e3d1d 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/ChaCha20Poly1305.Android.cs +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/ChaCha20Poly1305.Android.cs @@ -11,13 +11,13 @@ public sealed partial class ChaCha20Poly1305 { private SafeEvpCipherCtxHandle _ctxHandle; - public static bool IsSupported => true; + public static bool IsSupported { get; } = Interop.Crypto.CipherIsSupported(Interop.Crypto.EvpChaCha20Poly1305()); [MemberNotNull(nameof(_ctxHandle))] private void ImportKey(ReadOnlySpan key) { // Constructors should check key size before calling ImportKey. - Debug.Assert(key.Length == 256 / 8); + Debug.Assert(key.Length == KeySizeInBytes); _ctxHandle = Interop.Crypto.EvpCipherCreatePartial(Interop.Crypto.EvpChaCha20Poly1305()); Interop.Crypto.CheckValidOpenSslHandle(_ctxHandle); @@ -27,8 +27,7 @@ private void ImportKey(ReadOnlySpan key) Span.Empty, Interop.Crypto.EvpCipherDirection.NoChange); - const int ChaChaNonceSize = 96 / 8; - Interop.Crypto.CipherSetNonceLength(_ctxHandle, ChaChaNonceSize); + Interop.Crypto.CipherSetNonceLength(_ctxHandle, NonceSizeInBytes); } private void EncryptCore( diff --git a/src/libraries/System.Security.Cryptography.Algorithms/tests/ChaCha20Poly1305Tests.cs b/src/libraries/System.Security.Cryptography.Algorithms/tests/ChaCha20Poly1305Tests.cs index 445450c6116320..4a00b43bff92ee 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/tests/ChaCha20Poly1305Tests.cs +++ b/src/libraries/System.Security.Cryptography.Algorithms/tests/ChaCha20Poly1305Tests.cs @@ -447,16 +447,16 @@ public static void CheckIsSupported() // The test queries the OS directly to ensure our version check is correct. expectedIsSupported = CngUtility.IsAlgorithmSupported("CHACHA20_POLY1305"); } + else if (PlatformDetection.IsAndroid) + { + expectedIsSupported = true; + } else if (PlatformDetection.OpenSslPresentOnSystem && (PlatformDetection.IsOSX || PlatformDetection.IsOpenSslSupported)) { const int OpenSslChaChaMinimumVersion = 0x1_01_00_00_F; //major_minor_fix_patch_status expectedIsSupported = SafeEvpPKeyHandle.OpenSslVersion >= OpenSslChaChaMinimumVersion; } - else if (PlatformDetection.IsAndroid) - { - expectedIsSupported = true; - } Assert.Equal(expectedIsSupported, ChaCha20Poly1305.IsSupported); } From 5db9b3872b4671f1c4595c63c685bfb865eb39dc Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Sat, 15 May 2021 15:07:49 -0400 Subject: [PATCH 3/5] Don't describe the exception in an expected-exception path. --- .../System.Security.Cryptography.Native.Android/pal_cipher.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_cipher.c b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_cipher.c index fdf4d154e7e3be..dd34df4497b883 100644 --- a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_cipher.c +++ b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_cipher.c @@ -91,7 +91,7 @@ int32_t AndroidCryptoNative_CipherIsSupported(CipherInfo* type) // If we were able to call Cipher.getInstance without an exception, like NoSuchAlgorithmException, // then the algorithm is supported. - return CheckJNIExceptions(env) ? FAIL : SUCCESS; + return TryClearJNIExceptions(env) ? FAIL : SUCCESS; } CipherCtx* AndroidCryptoNative_CipherCreatePartial(CipherInfo* type) From b4dda5240cb098ee7602cb1014aad7c172a08331 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Sat, 15 May 2021 16:35:22 -0400 Subject: [PATCH 4/5] Detect Android SDK version for ChaChaPoly1305 IsSupported. --- .../tests/ChaCha20Poly1305Tests.cs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Security.Cryptography.Algorithms/tests/ChaCha20Poly1305Tests.cs b/src/libraries/System.Security.Cryptography.Algorithms/tests/ChaCha20Poly1305Tests.cs index 4a00b43bff92ee..64d40f03705894 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/tests/ChaCha20Poly1305Tests.cs +++ b/src/libraries/System.Security.Cryptography.Algorithms/tests/ChaCha20Poly1305Tests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using Test.Cryptography; using Xunit; @@ -449,7 +450,8 @@ public static void CheckIsSupported() } else if (PlatformDetection.IsAndroid) { - expectedIsSupported = true; + // Android with API Level 28 is the minimum API Level support for ChaChaPoly1305. + expectedIsSupported = GetAndroidSdkVersion() >= 28; } else if (PlatformDetection.OpenSslPresentOnSystem && (PlatformDetection.IsOSX || PlatformDetection.IsOpenSslSupported)) @@ -460,5 +462,22 @@ public static void CheckIsSupported() Assert.Equal(expectedIsSupported, ChaCha20Poly1305.IsSupported); } + + private static int GetAndroidSdkVersion() + { + using Process proc = new Process(); + proc.StartInfo.FileName = "getprop"; + proc.StartInfo.Arguments = " ro.build.version.sdk"; + proc.StartInfo.UseShellExecute = false; + proc.StartInfo.RedirectStandardOutput = true; + proc.Start(); + string stdout = proc.StandardOutput.ReadToEnd(); + + // This should never take more than a second. + int sdkVersion = -1; + bool success = proc.WaitForExit(5_000) && int.TryParse(stdout, out sdkVersion); + Assert.True(success, "Could not determine Android SDK version for current device."); + return sdkVersion; + } } } From 504ada062545fc47d4b27e92ccdc427f888165cc Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Sat, 15 May 2021 16:51:49 -0400 Subject: [PATCH 5/5] Refactor "HasTag" implementation. The CIPHER_HAS_TAG was used to determine if the GCMParameterSpec configuration is needed for variable length tags. While ChaCha20Poly1305 has authentication tags, it does not permit a tag length other than 16 bytes, so there is nothing to configure. This renames the CIPHER_HAS_TAG to be more specific that the cipher supports more than one tag length, and removes it from ChaCha20Poly1305. This simplifies the IV initialization a bit. --- .../pal_cipher.c | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_cipher.c b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_cipher.c index dd34df4497b883..c907f83a524e75 100644 --- a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_cipher.c +++ b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_cipher.c @@ -7,7 +7,7 @@ enum { CIPHER_NONE = 0, - CIPHER_HAS_TAG = 1, + CIPHER_HAS_VARIABLE_TAG = 1, CIPHER_REQUIRES_IV = 2, }; typedef uint32_t CipherFlags; @@ -30,20 +30,20 @@ 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) DEFINE_CIPHER(Aes128Cfb128, 128, "AES/CFB128/NoPadding", CIPHER_REQUIRES_IV) -DEFINE_CIPHER(Aes128Gcm, 128, "AES/GCM/NoPadding", CIPHER_HAS_TAG | CIPHER_REQUIRES_IV) -DEFINE_CIPHER(Aes128Ccm, 128, "AES/CCM/NoPadding", CIPHER_HAS_TAG | CIPHER_REQUIRES_IV) +DEFINE_CIPHER(Aes128Gcm, 128, "AES/GCM/NoPadding", CIPHER_HAS_VARIABLE_TAG | CIPHER_REQUIRES_IV) +DEFINE_CIPHER(Aes128Ccm, 128, "AES/CCM/NoPadding", CIPHER_HAS_VARIABLE_TAG | CIPHER_REQUIRES_IV) DEFINE_CIPHER(Aes192Ecb, 192, "AES/ECB/NoPadding", CIPHER_NONE) DEFINE_CIPHER(Aes192Cbc, 192, "AES/CBC/NoPadding", CIPHER_REQUIRES_IV) DEFINE_CIPHER(Aes192Cfb8, 192, "AES/CFB8/NoPadding", CIPHER_REQUIRES_IV) DEFINE_CIPHER(Aes192Cfb128, 192, "AES/CFB128/NoPadding", CIPHER_REQUIRES_IV) -DEFINE_CIPHER(Aes192Gcm, 192, "AES/GCM/NoPadding", CIPHER_HAS_TAG | CIPHER_REQUIRES_IV) -DEFINE_CIPHER(Aes192Ccm, 192, "AES/CCM/NoPadding", CIPHER_HAS_TAG | CIPHER_REQUIRES_IV) +DEFINE_CIPHER(Aes192Gcm, 192, "AES/GCM/NoPadding", CIPHER_HAS_VARIABLE_TAG | CIPHER_REQUIRES_IV) +DEFINE_CIPHER(Aes192Ccm, 192, "AES/CCM/NoPadding", CIPHER_HAS_VARIABLE_TAG | CIPHER_REQUIRES_IV) DEFINE_CIPHER(Aes256Ecb, 256, "AES/ECB/NoPadding", CIPHER_NONE) DEFINE_CIPHER(Aes256Cbc, 256, "AES/CBC/NoPadding", CIPHER_REQUIRES_IV) DEFINE_CIPHER(Aes256Cfb8, 256, "AES/CFB8/NoPadding", CIPHER_REQUIRES_IV) DEFINE_CIPHER(Aes256Cfb128, 256, "AES/CFB128/NoPadding", CIPHER_REQUIRES_IV) -DEFINE_CIPHER(Aes256Gcm, 256, "AES/GCM/NoPadding", CIPHER_HAS_TAG | CIPHER_REQUIRES_IV) -DEFINE_CIPHER(Aes256Ccm, 256, "AES/CCM/NoPadding", CIPHER_HAS_TAG | CIPHER_REQUIRES_IV) +DEFINE_CIPHER(Aes256Gcm, 256, "AES/GCM/NoPadding", CIPHER_HAS_VARIABLE_TAG | CIPHER_REQUIRES_IV) +DEFINE_CIPHER(Aes256Ccm, 256, "AES/CCM/NoPadding", CIPHER_HAS_VARIABLE_TAG | CIPHER_REQUIRES_IV) DEFINE_CIPHER(DesEcb, 64, "DES/ECB/NoPadding", CIPHER_NONE) DEFINE_CIPHER(DesCbc, 64, "DES/CBC/NoPadding", CIPHER_REQUIRES_IV) DEFINE_CIPHER(DesCfb8, 64, "DES/CFB8/NoPadding", CIPHER_REQUIRES_IV) @@ -51,7 +51,7 @@ DEFINE_CIPHER(Des3Ecb, 128, "DESede/ECB/NoPadding", CIPHER_NONE) DEFINE_CIPHER(Des3Cbc, 128, "DESede/CBC/NoPadding", CIPHER_REQUIRES_IV) 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_HAS_TAG | CIPHER_REQUIRES_IV) +DEFINE_CIPHER(ChaCha20Poly1305, 256, "ChaCha20/Poly1305/NoPadding", CIPHER_REQUIRES_IV) // // We don't have to check whether `CipherInfo` arguments are valid pointers, as these functions will be called after the @@ -61,9 +61,9 @@ DEFINE_CIPHER(ChaCha20Poly1305, 256, "ChaCha20/Poly1305/NoPadding", CIPHER_HAS_T // The entry functions (those that can be called by external code) take care to validate that the context passed to them // is a valid pointer and so we can assume the assertion from the preceding paragraph. // -ARGS_NON_NULL_ALL static bool HasTag(CipherInfo* type) +ARGS_NON_NULL_ALL static bool HasVariableTag(CipherInfo* type) { - return (type->flags & CIPHER_HAS_TAG) == CIPHER_HAS_TAG; + return (type->flags & CIPHER_HAS_VARIABLE_TAG) == CIPHER_HAS_VARIABLE_TAG; } ARGS_NON_NULL_ALL static bool RequiresIV(CipherInfo* type) @@ -158,14 +158,7 @@ ARGS_NON_NULL_ALL static int32_t ReinitializeCipher(CipherCtx* ctx) jbyteArray ivBytes = make_java_byte_array(env, ctx->ivLength); (*env)->SetByteArrayRegion(env, ivBytes, 0, ctx->ivLength, (jbyte*)ctx->iv); - // ChaChaPoly1305 does not allow configuration of the tag length and - // we don't permit user-defined counters, so we can use IvParameterSpec - // for ChaChaPoly1305. - if (strcmp("ChaCha20/Poly1305/NoPadding", ctx->type->name) == 0) - { - ivPsObj = (*env)->NewObject(env, g_ivPsClass, g_ivPsCtor, ivBytes); - } - else if (HasTag(ctx->type)) + if (HasVariableTag(ctx->type)) { ivPsObj = (*env)->NewObject(env, g_GCMParameterSpecClass, g_GCMParameterSpecCtor, ctx->tagLength * 8, ivBytes); }