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;
+ }
}
}