Support ChaChaPoly1305 on Android if available
authorKevin Jones <kevin@vcsjones.com>
Wed, 19 May 2021 16:55:29 +0000 (12:55 -0400)
committerGitHub <noreply@github.com>
Wed, 19 May 2021 16:55:29 +0000 (09:55 -0700)
In addition to making ChaCha/Poly work, this change refactors the "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.

src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.Cipher.cs
src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_cipher.c
src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_cipher.h
src/libraries/System.Security.Cryptography.Algorithms/src/System.Security.Cryptography.Algorithms.csproj
src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/ChaCha20Poly1305.Android.cs [new file with mode: 0644]
src/libraries/System.Security.Cryptography.Algorithms/tests/ChaCha20Poly1305Tests.cs

index bed79c75f775a3a78c3f0eaf4f99ff4817366043..c1abefa490d75f98cfc7a265d7a11fb221fb4592 100644 (file)
@@ -135,6 +135,10 @@ internal static partial class Interop
             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 partial class Interop
         [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,
index 0544e8f9b85af25946ba25928f54dabc72a465ad..c907f83a524e75ab5393742282a6f5a21fbbfd82 100644 (file)
@@ -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);
         }
index b9b9e1e45e29ba678d6123440cce986ecd34d95c..fda90fcc6284f318675a01a7419a6a9aa2b81380 100644 (file)
@@ -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);
index 833822b03c7fdc985c9000bb51e28944424dab57..b829552cc56cc20b69b4852fd108fee0ad1c51d0 100644 (file)
     <Compile Include="Internal\Cryptography\RC2Implementation.Android.cs" />
     <Compile Include="System\Security\Cryptography\AesCcm.Android.cs" />
     <Compile Include="System\Security\Cryptography\AesGcm.Android.cs" />
-    <Compile Include="System\Security\Cryptography\ChaCha20Poly1305.NotSupported.cs" />
+    <Compile Include="System\Security\Cryptography\ChaCha20Poly1305.Android.cs" />
     <Compile Include="System\Security\Cryptography\ECDiffieHellman.Create.Android.cs" />
     <Compile Include="System\Security\Cryptography\ECDsa.Create.Android.cs" />
     <Compile Include="System\Security\Cryptography\RSA.Create.Android.cs" />
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 (file)
index 0000000..a365992
--- /dev/null
@@ -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<byte> 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<byte>.Empty,
+                Interop.Crypto.EvpCipherDirection.NoChange);
+
+            Interop.Crypto.CipherSetNonceLength(_ctxHandle, NonceSizeInBytes);
+        }
+
+        private void EncryptCore(
+            ReadOnlySpan<byte> nonce,
+            ReadOnlySpan<byte> plaintext,
+            Span<byte> ciphertext,
+            Span<byte> tag,
+            ReadOnlySpan<byte> associatedData = default)
+        {
+
+            Interop.Crypto.EvpCipherSetKeyAndIV(
+                _ctxHandle,
+                Span<byte>.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<byte> 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<byte> nonce,
+            ReadOnlySpan<byte> ciphertext,
+            ReadOnlySpan<byte> tag,
+            Span<byte> plaintext,
+            ReadOnlySpan<byte> associatedData)
+        {
+            Interop.Crypto.EvpCipherSetKeyAndIV(
+                _ctxHandle,
+                ReadOnlySpan<byte>.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();
+        }
+    }
+}
index 5a96943f19ddea7bbbff32a9e1cac3d5d8c3f340..64d40f0370589448e6f21d6f544284ec2d3f72d3 100644 (file)
@@ -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 @@ namespace System.Security.Cryptography.Algorithms.Tests
                 // 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 @@ namespace System.Security.Cryptography.Algorithms.Tests
 
             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;
+        }
     }
 }