Replace legacy encryption with AES-GCM
This commit is contained in:
@@ -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 ---
|
||||
}
|
||||
//=======================================================================
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user