Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement AES-GCM with CryptoKit on macOS #76490

Merged
merged 10 commits into from
Nov 8, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,81 @@ internal static unsafe void ChaCha20Poly1305Decrypt(
}
}
}
internal static unsafe void AesGcmEncrypt(
vcsjones marked this conversation as resolved.
Show resolved Hide resolved
ReadOnlySpan<byte> key,
ReadOnlySpan<byte> nonce,
ReadOnlySpan<byte> plaintext,
Span<byte> ciphertext,
Span<byte> tag,
ReadOnlySpan<byte> aad)
{
fixed (byte* keyPtr = key)
fixed (byte* noncePtr = nonce)
fixed (byte* plaintextPtr = plaintext)
fixed (byte* ciphertextPtr = ciphertext)
fixed (byte* tagPtr = tag)
fixed (byte* aadPtr = aad)
{
const int Success = 1;
int result = AppleCryptoNative_AesGcmEncrypt(
keyPtr, key.Length,
noncePtr, nonce.Length,
plaintextPtr, plaintext.Length,
ciphertextPtr, ciphertext.Length,
tagPtr, tag.Length,
aadPtr, aad.Length);

if (result != Success)
{
Debug.Assert(result == 0);
CryptographicOperations.ZeroMemory(ciphertext);
CryptographicOperations.ZeroMemory(tag);
throw new CryptographicException();
}
}
}

internal static unsafe void AesGcmDecrypt(
ReadOnlySpan<byte> key,
ReadOnlySpan<byte> nonce,
ReadOnlySpan<byte> ciphertext,
ReadOnlySpan<byte> tag,
Span<byte> plaintext,
ReadOnlySpan<byte> aad)
{
fixed (byte* keyPtr = key)
fixed (byte* noncePtr = nonce)
fixed (byte* ciphertextPtr = ciphertext)
fixed (byte* tagPtr = tag)
fixed (byte* plaintextPtr = plaintext)
fixed (byte* aadPtr = aad)
{
const int Success = 1;
const int AuthTagMismatch = -1;
int result = AppleCryptoNative_AesGcmDecrypt(
keyPtr, key.Length,
noncePtr, nonce.Length,
ciphertextPtr, ciphertext.Length,
tagPtr, tag.Length,
plaintextPtr, plaintext.Length,
aadPtr, aad.Length);

if (result != Success)
{
CryptographicOperations.ZeroMemory(plaintext);

if (result == AuthTagMismatch)
{
throw new AuthenticationTagMismatchException();
}
else
{
Debug.Assert(result == 0);
throw new CryptographicException();
}
}
}
}

[LibraryImport(Libraries.AppleCryptoNative)]
private static unsafe partial int AppleCryptoNative_ChaCha20Poly1305Encrypt(
Expand Down Expand Up @@ -116,5 +191,35 @@ private static unsafe partial int AppleCryptoNative_ChaCha20Poly1305Decrypt(
int plaintextLength,
byte* aadPtr,
int aadLength);

[LibraryImport(Libraries.AppleCryptoNative)]
private static unsafe partial int AppleCryptoNative_AesGcmEncrypt(
byte* keyPtr,
int keyLength,
byte* noncePtr,
int nonceLength,
byte* plaintextPtr,
int plaintextLength,
byte* ciphertextPtr,
int ciphertextLength,
byte* tagPtr,
int tagLength,
byte* aadPtr,
int aadLength);

[LibraryImport(Libraries.AppleCryptoNative)]
private static unsafe partial int AppleCryptoNative_AesGcmDecrypt(
byte* keyPtr,
int keyLength,
byte* noncePtr,
int nonceLength,
byte* ciphertextPtr,
int ciphertextLength,
byte* tagPtr,
int tagLength,
byte* plaintextPtr,
int plaintextLength,
byte* aadPtr,
int aadLength);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,9 @@
<data name="PersistedFiles_NoHomeDirectory" xml:space="preserve">
<value>The home directory of the current user could not be determined.</value>
</data>
<data name="PlatformNotSupported_AesGcmTagSize" xml:space="preserve">
<value>The current platform only supports 128-bit AES-GCM tags.</value>
</data>
<data name="PlatformNotSupported_CryptographyCng" xml:space="preserve">
<value>Windows Cryptography Next Generation (CNG) is not supported on this platform.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,8 @@
Link="Common\System\Text\UrlBase64Encoding.cs" />
<Compile Include="$(CommonPath)System\Text\ValueUtf8Converter.cs"
Link="Common\System\Text\ValueUtf8Converter.cs" />
<Compile Include="System\Security\Cryptography\AesGcm.OpenSsl.cs" />
<Compile Include="System\Security\Cryptography\AesGcmOpenSslCommon.cs" />
<Compile Include="System\Security\Cryptography\AesImplementation.OpenSsl.cs" />
<Compile Include="System\Security\Cryptography\AsnFormatter.OpenSsl.cs" />
<Compile Include="System\Security\Cryptography\CapiHelper.DSA.Shared.cs" />
Expand Down Expand Up @@ -870,7 +872,6 @@
<Compile Include="$(CommonPath)Microsoft\Win32\SafeHandles\SafeEvpCipherCtxHandle.Unix.cs"
Link="Common\Microsoft\Win32\SafeHandles\SafeEvpCipherCtxHandle.Unix.cs" />
<Compile Include="System\Security\Cryptography\AesCcm.OpenSsl.cs" />
<Compile Include="System\Security\Cryptography\AesGcm.OpenSsl.cs" />
</ItemGroup>
<ItemGroup Condition="'$(UseAndroidCrypto)' == 'true'">
<Compile Include="$(CommonPath)Interop\Android\Interop.JObjectLifetime.cs"
Expand Down Expand Up @@ -1291,6 +1292,8 @@
Link="Common\System\Security\Cryptography\RSASecurityTransforms.macOS.cs" />
<Compile Include="$(CommonPath)System\Security\Cryptography\RSAOpenSsl.cs"
Link="Common\System\Security\Cryptography\RSAOpenSsl.cs" />
<Compile Include="System\Security\Cryptography\AesGcm.macOS.cs" />
vcsjones marked this conversation as resolved.
Show resolved Hide resolved
<Compile Include="System\Security\Cryptography\AesGcmOpenSslCommon.cs" />
<Compile Include="System\Security\Cryptography\ChaCha20Poly1305.macOS.cs" />
<Compile Include="System\Security\Cryptography\DSA.Create.SecurityTransforms.cs" />
<Compile Include="System\Security\Cryptography\DSACryptoServiceProvider.Unix.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public sealed partial class AesGcm
[MemberNotNull(nameof(_ctxHandle))]
private void ImportKey(ReadOnlySpan<byte> key)
{
_ctxHandle = Interop.Crypto.EvpCipherCreatePartial(GetCipher(key.Length * 8));
_ctxHandle = Interop.Crypto.EvpCipherCreatePartial(AesGcmOpenSslCommon.GetCipher(key.Length * 8));

Interop.Crypto.CheckValidOpenSslHandle(_ctxHandle);
Interop.Crypto.EvpCipherSetKeyAndIV(
Expand All @@ -32,44 +32,9 @@ private void EncryptCore(
ReadOnlySpan<byte> plaintext,
Span<byte> ciphertext,
Span<byte> tag,
ReadOnlySpan<byte> associatedData = default)
ReadOnlySpan<byte> associatedData)
{
Interop.Crypto.EvpCipherSetKeyAndIV(
_ctxHandle,
Span<byte>.Empty,
nonce,
Interop.Crypto.EvpCipherDirection.Encrypt);

if (associatedData.Length != 0)
{
if (!Interop.Crypto.EvpCipherUpdate(_ctxHandle, Span<byte>.Empty, out _, associatedData))
{
throw Interop.Crypto.CreateOpenSslCryptographicException();
}
}

if (!Interop.Crypto.EvpCipherUpdate(_ctxHandle, ciphertext, out int ciphertextBytesWritten, plaintext))
{
throw Interop.Crypto.CreateOpenSslCryptographicException();
}

if (!Interop.Crypto.EvpCipherFinalEx(
_ctxHandle,
ciphertext.Slice(ciphertextBytesWritten),
out int bytesWritten))
{
throw Interop.Crypto.CreateOpenSslCryptographicException();
}

ciphertextBytesWritten += bytesWritten;

if (ciphertextBytesWritten != ciphertext.Length)
{
Debug.Fail($"GCM encrypt wrote {ciphertextBytesWritten} of {ciphertext.Length} bytes.");
throw new CryptographicException();
}

Interop.Crypto.EvpCipherGetGcmTag(_ctxHandle, tag);
AesGcmOpenSslCommon.Encrypt(_ctxHandle, nonce, plaintext, ciphertext, tag, associatedData);
}

private void DecryptCore(
Expand All @@ -79,56 +44,7 @@ private void DecryptCore(
Span<byte> plaintext,
ReadOnlySpan<byte> associatedData)
{
Interop.Crypto.EvpCipherSetKeyAndIV(
_ctxHandle,
ReadOnlySpan<byte>.Empty,
nonce,
Interop.Crypto.EvpCipherDirection.Decrypt);

if (associatedData.Length != 0)
{
if (!Interop.Crypto.EvpCipherUpdate(_ctxHandle, Span<byte>.Empty, out _, associatedData))
{
throw Interop.Crypto.CreateOpenSslCryptographicException();
}
}

if (!Interop.Crypto.EvpCipherUpdate(_ctxHandle, plaintext, out int plaintextBytesWritten, ciphertext))
{
throw Interop.Crypto.CreateOpenSslCryptographicException();
}

Interop.Crypto.EvpCipherSetGcmTag(_ctxHandle, tag);

if (!Interop.Crypto.EvpCipherFinalEx(
_ctxHandle,
plaintext.Slice(plaintextBytesWritten),
out int bytesWritten))
{
CryptographicOperations.ZeroMemory(plaintext);
throw new AuthenticationTagMismatchException();
}

plaintextBytesWritten += bytesWritten;

if (plaintextBytesWritten != plaintext.Length)
{
Debug.Fail($"GCM decrypt wrote {plaintextBytesWritten} of {plaintext.Length} bytes.");
throw new CryptographicException();
}
}

private static IntPtr GetCipher(int keySizeInBits)
{
switch (keySizeInBits)
{
case 128: return Interop.Crypto.EvpAes128Gcm();
case 192: return Interop.Crypto.EvpAes192Gcm();
case 256: return Interop.Crypto.EvpAes256Gcm();
default:
Debug.Fail("Key size should already be validated");
return IntPtr.Zero;
}
AesGcmOpenSslCommon.Decrypt(_ctxHandle, nonce, ciphertext, tag, plaintext, associatedData);
}

public void Dispose()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// 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 AesGcm
{
// Apple CryptoKit does not support short authentication tags. Since .NET originally supported AES-GCM via
// OpenSSL, which does support short tags, we need to continue to support them. If a caller supplies a short
// tag we will continue to use OpenSSL if it is available. Otherwise, use CryptoKit.
private static readonly bool s_openSslAvailable = Interop.OpenSslNoInit.OpenSslIsAvailable;
vcsjones marked this conversation as resolved.
Show resolved Hide resolved
private const int CryptoKitSupportedTagSizeInBytes = 16;

private byte[]? _key;

// CryptoKit added AES.GCM in macOS 10.15, which is our minimum target for macOS. We still may end
// up throwing a platform not supported if a caller uses a short authentication tag and OpenSSL is not
// available. But recommended use of AES-GCM with a 16-byte tag is supported.
public static bool IsSupported => true;

[MemberNotNull(nameof(_key))]
private void ImportKey(ReadOnlySpan<byte> key)
{
// We should only be calling this in the constructor, so there shouldn't be a previous key.
Debug.Assert(_key is null);

// Pin the array on the POH so that the GC doesn't move it around to allow zeroing to be more effective.
_key = GC.AllocateArray<byte>(key.Length, pinned: true);
key.CopyTo(_key);
}

private void EncryptCore(
ReadOnlySpan<byte> nonce,
ReadOnlySpan<byte> plaintext,
Span<byte> ciphertext,
Span<byte> tag,
ReadOnlySpan<byte> associatedData)
{
CheckDisposed();

if (tag.Length != CryptoKitSupportedTagSizeInBytes)
{
using (SafeEvpCipherCtxHandle ctxHandle = CreateOpenSslHandle())
{
AesGcmOpenSslCommon.Encrypt(ctxHandle, nonce, plaintext, ciphertext, tag, associatedData);
}
}
vcsjones marked this conversation as resolved.
Show resolved Hide resolved
else
{
Interop.AppleCrypto.AesGcmEncrypt(
_key,
nonce,
plaintext,
ciphertext,
tag,
associatedData);
}
}

private void DecryptCore(
ReadOnlySpan<byte> nonce,
ReadOnlySpan<byte> ciphertext,
ReadOnlySpan<byte> tag,
Span<byte> plaintext,
ReadOnlySpan<byte> associatedData)
{
CheckDisposed();

if (tag.Length != CryptoKitSupportedTagSizeInBytes)
{
using (SafeEvpCipherCtxHandle ctxHandle = CreateOpenSslHandle())
{
AesGcmOpenSslCommon.Decrypt(ctxHandle, nonce, ciphertext, tag, plaintext, associatedData);
}
}
vcsjones marked this conversation as resolved.
Show resolved Hide resolved
else
{
Interop.AppleCrypto.AesGcmDecrypt(
_key,
nonce,
ciphertext,
tag,
plaintext,
associatedData);
}
}

public void Dispose()
{
CryptographicOperations.ZeroMemory(_key);
_key = null;
}

[MemberNotNull(nameof(_key))]
private void CheckDisposed()
{
ObjectDisposedException.ThrowIf(_key is null, this);
}

private SafeEvpCipherCtxHandle CreateOpenSslHandle()
{
Debug.Assert(_key is not null);

// We should only get here if the tag size is not 128-bit. If that happens, and OpenSSL is not available,
// then we can't proceed.
if (!s_openSslAvailable)
{
throw new PlatformNotSupportedException(SR.PlatformNotSupported_AesGcmTagSize);
}

IntPtr cipherHandle = AesGcmOpenSslCommon.GetCipher(_key.Length * 8);
SafeEvpCipherCtxHandle ctxHandle = Interop.Crypto.EvpCipherCreatePartial(cipherHandle);

Interop.Crypto.CheckValidOpenSslHandle(ctxHandle);
Interop.Crypto.EvpCipherSetKeyAndIV(
ctxHandle,
_key,
Span<byte>.Empty,
Interop.Crypto.EvpCipherDirection.NoChange);
Interop.Crypto.EvpCipherSetGcmNonceLength(ctxHandle, NonceSize);
return ctxHandle;
}
}
}
Loading