Skip to content

Commit

Permalink
Implement AES-GCM with CryptoKit on macOS
Browse files Browse the repository at this point in the history
  • Loading branch information
vcsjones committed Oct 1, 2022
1 parent fe33559 commit 7b4a0be
Show file tree
Hide file tree
Showing 10 changed files with 525 additions and 104 deletions.
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(
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" />
<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;
private const int CryptoKitSupportedTagSizeInBytes = 16;

private byte[]? _key;

// CryptoKit added ChaCha20Poly1305 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);
}
}
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);
}
}
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

0 comments on commit 7b4a0be

Please sign in to comment.