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..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(); @@ -216,6 +220,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..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; @@ -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_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_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_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) +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_REQUIRES_IV) // // We don't have to check whether `CipherInfo` arguments are valid pointers, as these functions will be called after the @@ -60,9 +61,9 @@ DEFINE_CIPHER(Des3Cfb64, 128, "DESede/CFB/NoPadding", CIPHER_REQUIRES_IV) // 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) @@ -75,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 TryClearJNIExceptions(env) ? FAIL : SUCCESS; +} + CipherCtx* AndroidCryptoNative_CipherCreatePartial(CipherInfo* type) { abort_if_invalid_pointer_argument (type); @@ -138,7 +157,8 @@ 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)) + + if (HasVariableTag(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..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); @@ -60,3 +61,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..a36599202e3d1d --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/ChaCha20Poly1305.Android.cs @@ -0,0 +1,159 @@ +// 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 { 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 == KeySizeInBytes); + _ctxHandle = Interop.Crypto.EvpCipherCreatePartial(Interop.Crypto.EvpChaCha20Poly1305()); + + Interop.Crypto.CheckValidOpenSslHandle(_ctxHandle); + Interop.Crypto.EvpCipherSetKeyAndIV( + _ctxHandle, + key, + Span.Empty, + Interop.Crypto.EvpCipherDirection.NoChange); + + Interop.Crypto.CipherSetNonceLength(_ctxHandle, NonceSizeInBytes); + } + + 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..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; @@ -447,6 +448,11 @@ 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) + { + // Android with API Level 28 is the minimum API Level support for ChaChaPoly1305. + expectedIsSupported = GetAndroidSdkVersion() >= 28; + } else if (PlatformDetection.OpenSslPresentOnSystem && (PlatformDetection.IsOSX || PlatformDetection.IsOpenSslSupported)) { @@ -456,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; + } } }