Files
Common-VS2019/SacsStandardBase/Base/Helpers/SacsAuthenticatedEncryption.cs

110 lines
3.5 KiB
C#

using System;
using System.Security.Cryptography;
using System.Text;
namespace Sarand.Net.Standard.Base.Helpers
{
public static class SacsAuthenticatedEncryption
{
// --- Definitions ---
public const String EnvelopePrefix = "SACSENC2.";
public const Int32 NonceLength = 12;
public const Int32 TagLength = 16;
public const Int32 MaximumEnvelopeLength = 1024 * 1024;
// --- Encryption ---
public static String Encrypt(String plaintext, Byte[] key, String purpose)
{
ArgumentNullException.ThrowIfNull(plaintext);
ValidateKeyAndPurpose(key, purpose);
Byte[] plaintextBytes = Encoding.UTF8.GetBytes(plaintext);
Byte[] nonce = RandomNumberGenerator.GetBytes(NonceLength);
Byte[] ciphertext = new Byte[plaintextBytes.Length];
Byte[] tag = new Byte[TagLength];
Byte[] associatedData = Encoding.UTF8.GetBytes(purpose);
using AesGcm aes = new AesGcm(key, TagLength);
aes.Encrypt(nonce, plaintextBytes, ciphertext, tag, associatedData);
Byte[] envelope = new Byte[NonceLength + TagLength + ciphertext.Length];
nonce.CopyTo(envelope, 0);
tag.CopyTo(envelope, NonceLength);
ciphertext.CopyTo(envelope, NonceLength + TagLength);
return EnvelopePrefix + ToBase64Url(envelope);
}
public static String Decrypt(String encryptedValue, Byte[] key, String purpose)
{
ArgumentException.ThrowIfNullOrWhiteSpace(encryptedValue);
ValidateKeyAndPurpose(key, purpose);
if (!encryptedValue.StartsWith(EnvelopePrefix, StringComparison.Ordinal))
{
throw new CryptographicException("The encrypted value does not use the supported authenticated format.");
}
if (encryptedValue.Length > MaximumEnvelopeLength)
{
throw new CryptographicException("The encrypted value exceeds the supported size.");
}
Byte[] envelope;
try
{
envelope = FromBase64Url(encryptedValue.Substring(EnvelopePrefix.Length));
}
catch (FormatException exception)
{
throw new CryptographicException("The encrypted value is malformed.", exception);
}
if (envelope.Length < NonceLength + TagLength)
{
throw new CryptographicException("The encrypted value is incomplete.");
}
ReadOnlySpan<Byte> nonce = envelope.AsSpan(0, NonceLength);
ReadOnlySpan<Byte> tag = envelope.AsSpan(NonceLength, TagLength);
ReadOnlySpan<Byte> ciphertext = envelope.AsSpan(NonceLength + TagLength);
Byte[] plaintext = new Byte[ciphertext.Length];
Byte[] associatedData = Encoding.UTF8.GetBytes(purpose);
using AesGcm aes = new AesGcm(key, TagLength);
aes.Decrypt(nonce, ciphertext, tag, plaintext, associatedData);
return Encoding.UTF8.GetString(plaintext);
}
// --- Utilities ---
private static void ValidateKeyAndPurpose(Byte[] key, String purpose)
{
ArgumentNullException.ThrowIfNull(key);
ArgumentException.ThrowIfNullOrWhiteSpace(purpose);
if (key.Length != 16 && key.Length != 24 && key.Length != 32)
{
throw new ArgumentException("AES keys must contain 16, 24, or 32 bytes.", nameof(key));
}
if (!AesGcm.IsSupported)
{
throw new PlatformNotSupportedException("AES-GCM is not supported on this platform.");
}
}
private static String ToBase64Url(Byte[] value)
{
return Convert.ToBase64String(value).TrimEnd('=').Replace('+', '-').Replace('/', '_');
}
private static Byte[] FromBase64Url(String value)
{
String base64 = value.Replace('-', '+').Replace('_', '/');
base64 = base64.PadRight(base64.Length + ((4 - base64.Length % 4) % 4), '=');
return Convert.FromBase64String(base64);
}
// --- The End ---
}
}