110 lines
3.5 KiB
C#
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 ---
|
|
}
|
|
}
|