Replace legacy encryption with AES-GCM

This commit is contained in:
Andy Howard
2026-09-16 18:40:32 +01:00
parent 6989c5f9b6
commit a0a5ef970c
13 changed files with 456 additions and 278 deletions
@@ -0,0 +1,32 @@
using Sarand.Net.Standard.Base.Helpers;
using System;
using System.Security.Cryptography;
using System.Text;
namespace Sarand.Net.Standard.Base.Data
{
public static class SacsAesGcmCrypter
{
// --- Key Derivation ---
private static Byte[] CreateKey(String keyValue)
{
ArgumentException.ThrowIfNullOrWhiteSpace(keyValue);
return SHA256.HashData(Encoding.UTF8.GetBytes(keyValue));
}
// --- Authenticated Encryption ---
public static String Encrypt(this String clearValue, String encryptionKey, String purpose)
{
return SacsAuthenticatedEncryption.Encrypt(clearValue, CreateKey(encryptionKey), purpose);
}
public static String Decrypt(this String encryptedValue, String encryptionKey, String purpose)
{
return SacsAuthenticatedEncryption.Decrypt(encryptedValue, CreateKey(encryptionKey), purpose);
}
// --- The End ---
}
}
@@ -1,113 +0,0 @@
using Sarand.Net.Standard.Base.App;
using System;
using System.IO;
using System.Security.Cryptography;
namespace Sarand.Net.Standard.Base.Data
{
//=======================================================================
public static class SacsAesIvCrypter
{
//-------------------------------------------------------------------
// Properties.
public static String AesIvDelimiter { get; } = ";";
public static String AesIvPrefix { get; } = "AESIV:";
//-------------------------------------------------------------------
// Ref. https://xamarinhelp.com/cryptography-in-xamarin-forms/
// For reversible encryption, AES is generally the recommended standard.
// AES 256 requires a 32 byte key to encrypt.
// Either generate a 32 byte key or use any string and convert it into a key with CreateKey.
// NB The salt will remain the same here for each key generated - it does not need to be randomized as with hashing.
//-------------------------------------------------------------------
private static byte[] CreateKey(string password, int keyBytes = 32)
{
byte[] salt = new byte[] { 80, 70, 60, 50, 40, 30, 20, 10 };
int iterations = 300;
return Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, HashAlgorithmName.SHA256, keyBytes);
}
//-------------------------------------------------------------------
// Encrypt String.
// Return the encrypted text with the IV appended to the end.
// In AES, the IV is a randomizer, making sure that no two identical messages ever give the same encrypted output.
// The IV can be public and does not need to be hidden.
//-------------------------------------------------------------------
public static string Encrypt(this string clearValue, string encryptionKey)
{
using (Aes aes = Aes.Create())
{
aes.Key = CreateKey(encryptionKey);
byte[] aesIV = aes.IV;
byte[] encrypted = Encrypt(clearValue, aes.Key, aesIV);
return Convert.ToBase64String(encrypted) + AesIvDelimiter + Convert.ToBase64String(aesIV);
}
}
//-------------------------------------------------------------------
public static byte[] Encrypt(string plainText, byte[] key, byte[] iv)
{
if (plainText == null || plainText.Length <= 0) throw new ArgumentNullException($"{nameof(plainText)}");
if (key == null || key.Length <= 0) throw new ArgumentNullException($"{nameof(key)}");
if (iv == null || iv.Length <= 0) throw new ArgumentNullException($"{nameof(iv)}");
byte[] encrypted;
using (Aes aes = Aes.Create())
{
aes.Key = key;
aes.IV = iv;
using (MemoryStream memoryStream = new MemoryStream())
{
using (ICryptoTransform encryptor = aes.CreateEncryptor())
using (CryptoStream cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
using (StreamWriter streamWriter = new StreamWriter(cryptoStream))
{
streamWriter.Write(plainText);
}
encrypted = memoryStream.ToArray();
}
}
return encrypted;
}
//-------------------------------------------------------------------
// Decrypt String.
// Decryption requires the encrypted value and the secret key.
// This function is directly related to the above encryption process and will
// assume that the IV is at the end of the string passed in.
// To decrypt a message encrypted by another function, the IV will have to be passed explicitly.
//-------------------------------------------------------------------
public static string Decrypt(this string encryptedValue, string encryptionKey)
{
String[] valueParts = encryptedValue.Split(new String[] { AesIvDelimiter }, StringSplitOptions.None);
if (valueParts.Length != 2)
{
SaLogger.Warn("SacsAesIvCrypter.Decrypt Missing IV - assuming not encrypted");
return encryptedValue;
//throw new CryptographicException("Decrypt failure: Missing IV");
}
return Decrypt(Convert.FromBase64String(valueParts[0]), CreateKey(encryptionKey), Convert.FromBase64String(valueParts[1]));
}
//-------------------------------------------------------------------
public static string Decrypt(byte[] cipherText, byte[] key, byte[] iv)
{
if (cipherText == null || cipherText.Length <= 0) throw new ArgumentNullException($"{nameof(cipherText)}");
if (key == null || key.Length <= 0) throw new ArgumentNullException($"{nameof(key)}");
if (iv == null || iv.Length <= 0) throw new ArgumentNullException($"{nameof(iv)}");
string plaintext = "";
using (Aes aes = Aes.Create())
{
aes.Key = key;
aes.IV = iv;
using (MemoryStream memoryStream = new MemoryStream(cipherText))
using (ICryptoTransform decryptor = aes.CreateDecryptor())
using (CryptoStream cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read))
using (StreamReader streamReader = new StreamReader(cryptoStream))
plaintext = streamReader.ReadToEnd();
}
return plaintext;
}
//-------------------------------------------------------------------
}
//=======================================================================
}
@@ -0,0 +1,109 @@
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 ---
}
}
@@ -1,26 +1,24 @@
using Sarand.Net.Standard.Base.Core;
using Sarand.Net.Standard.Base.Utils;
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
namespace Sarand.Net.Standard.Base.Helpers
{
//=======================================================================
public static class SacsEncryptionHelper
{
//-------------------------------------------------------------------
// --- Definitions ---
/// <summary>Number of byes in key. [32 bytes = 256 bits]</summary>
public static int KeyLength { get; } = 32;
/// <summary>Default encoding: UTF8.</summary>
public static Encoding DefaultEncoding { get; } = Encoding.UTF8;
//-------------------------------------------------------------------
// --- Key Management ---
// Symmetric Encryption in .NET Core
// System.Security.Cryptography implements and provides the APIs for encryption in .NET Core.
// Ref. https://damienbod.com/2020/08/19/symmetric-and-asymmetric-encryption-in-net-core/
//-------------------------------------------------------------------
public static bool IsBase64(String value, int expectedLength = 0)
{
try
@@ -46,48 +44,19 @@ namespace Sarand.Net.Standard.Base.Helpers
return Convert.ToBase64String(encoding.GetBytes(plainKey));
}
//-------------------------------------------------------------------
// Generate random key and IV.
// Generate random key.
// NB Can use own key if suitable - 32 character value in base-64 format.
public static String InitSymmetricEncryptionKey()
{
var key = GetEncodedRandomString(KeyLength); // 256 bits.
return key;
}
public static string InitSymmetricEncryptionIV(String key)
{
using Aes cipher = CreateCipher(key);
var IVBase64 = Convert.ToBase64String(cipher.IV);
return IVBase64;
}
public static (string Key, string IVBase64) InitSymmetricEncryptionKeyIV()
{
var key = InitSymmetricEncryptionKey();
var IVBase64 = InitSymmetricEncryptionIV(key);
return (key, IVBase64);
}
//-------------------------------------------------------------------
public static string GetEncodedRandomString(int length)
{
var base64 = Convert.ToBase64String(GenerateRandomBytes(length));
return base64;
}
private static Aes CreateCipher(string keyBase64)
{
// Default values: Keysize 256 bits, Padding PKC27.
Aes cipher = Aes.Create();
// Ensure the integrity of the ciphertext if using CBC
cipher.Mode = CipherMode.CBC;
cipher.Padding = PaddingMode.ISO10126;
byte[] rawKey = Convert.FromBase64String(keyBase64);
cipher.Key = rawKey;
return cipher;
}
private static byte[] GenerateRandomBytes(int length)
{
var byteArray = new byte[length];
@@ -95,33 +64,18 @@ namespace Sarand.Net.Standard.Base.Helpers
return byteArray;
}
//-------------------------------------------------------------------
public static string Encrypt(string text, string IV, string key)
// --- Authenticated Encryption ---
public static String EncryptAuthenticated(String text, String key, String purpose)
{
using Aes cipher = CreateCipher(key);
cipher.IV = Convert.FromBase64String(IV);
using ICryptoTransform cryptTransform = cipher.CreateEncryptor();
byte[] plaintext = Encoding.UTF8.GetBytes(text);
byte[] cipherText = cryptTransform.TransformFinalBlock(plaintext, 0, plaintext.Length);
return Convert.ToBase64String(cipherText);
return SacsAuthenticatedEncryption.Encrypt(text, Convert.FromBase64String(key), purpose);
}
//-------------------------------------------------------------------
public static string Decrypt(string encryptedText, string IV, string key)
public static String DecryptAuthenticated(String encryptedText, String key, String purpose)
{
using Aes cipher = CreateCipher(key);
cipher.IV = Convert.FromBase64String(IV);
using ICryptoTransform cryptTransform = cipher.CreateDecryptor();
byte[] encryptedBytes = Convert.FromBase64String(encryptedText);
byte[] plainBytes = cryptTransform.TransformFinalBlock(encryptedBytes, 0, encryptedBytes.Length);
return Encoding.UTF8.GetString(plainBytes);
return SacsAuthenticatedEncryption.Decrypt(encryptedText, Convert.FromBase64String(key), purpose);
}
//-------------------------------------------------------------------
// --- The End ---
}
//=======================================================================
}