C# lacks a common off-the-shelf encryption and decryption interface for dependency injection. Nearly every codebase I've ever opened has a couple of extension methods for hashing strings and so on. This is always frustrating because it makes it difficult to change the cryptography in one hit. It seems blindingly obvious that there should be a simple interface for encrypting and decrypting strings. So here's one. It can be used for dependency injection.
public interface IOneWayEncryptionService
{
string Encrypt(string text);
}
public interface ICryptographyService : IOneWayEncryptionService
{
string Decrypt(string text);
}
Here are a couple of implementations
public class MD5CryptographyService : IOneWayEncryptionService
{
#region Implementation
public string Encrypt(string text)
{
var md5CryptoServiceProvider = new MD5CryptoServiceProvider();
md5CryptoServiceProvider.ComputeHash(Encoding.UTF8.GetBytes(text));
return md5CryptoServiceProvider.Hash.ToHexStringFromByteArray();
}
#endregion
}
public class SymmetricAlgorithmCryptographyService : ICryptographyService, IDisposable
{
#region Public Properties
public SymmetricAlgorithm SymmetricAlgorithm { get; }
#endregion
#region Constructors
public SymmetricAlgorithmCryptographyService(SymmetricAlgorithm symmetricAlgorithm)
{
SymmetricAlgorithm = symmetricAlgorithm ?? throw new ArgumentNullException(nameof(SymmetricAlgorithm));
if (symmetricAlgorithm.Key == null || symmetricAlgorithm.Key.Length <= 0)
throw new ArgumentNullException(nameof(SymmetricAlgorithm.Key));
SymmetricAlgorithm = symmetricAlgorithm;
}
#endregion
#region Implementation
public string Decrypt(string text)
{
var cipherTextData = text.ToByteArrayFromHex();
var hex = DecryptStringFromBytes(cipherTextData);
return hex;
}
public string Encrypt(string text)
{
return EncryptStringToBytes(text).ToHexStringFromByteArray();
}
public void Dispose()
{
SymmetricAlgorithm.Dispose();
}
#endregion
#region Private Methods
private IEnumerable<byte> EncryptStringToBytes(string plainText)
{
if (plainText == null || plainText.Length <= 0) throw new ArgumentNullException(nameof(plainText));
var cryptoTransform = SymmetricAlgorithm.CreateEncryptor(SymmetricAlgorithm.Key, SymmetricAlgorithm.IV);
using (var msEncrypt = new MemoryStream())
{
using (var csEncrypt = new CryptoStream(msEncrypt, cryptoTransform, CryptoStreamMode.Write))
{
using (var swEncrypt = new StreamWriter(csEncrypt))
{
swEncrypt.Write(plainText);
}
return msEncrypt.ToArray();
}
}
}
private string DecryptStringFromBytes(byte[] cipherTextData)
{
if (cipherTextData == null || cipherTextData.Length <= 0) throw new ArgumentNullException(nameof(cipherTextData));
var cryptoTransform = SymmetricAlgorithm.CreateDecryptor(SymmetricAlgorithm.Key, SymmetricAlgorithm.IV);
using (var msDecrypt = new MemoryStream(cipherTextData))
{
using (var csDecrypt = new CryptoStream(msDecrypt, cryptoTransform, CryptoStreamMode.Read))
{
using (var srDecrypt = new StreamReader(csDecrypt))
{
return srDecrypt.ReadToEnd();
}
}
}
}
#endregion
}
Here is an example usage:
public class EncryptedPersister
{
#region Fields
private readonly ICryptographyService _cryptographyService;
private readonly IIOService _ioService;
#endregion
#region Constructor
public EncryptedPersister(ICryptographyService cryptographyService, IIOService ioService)
{
_cryptographyService = cryptographyService;
_ioService = ioService;
}
#endregion
#region Public Methods
public async Task SaveAsync<T>(string filename, T model)
{
var json = JsonConvert.SerializeObject(model);
var encryptedJson = _cryptographyService.Encrypt(json);
await _ioService.SaveAsync(filename, encryptedJson);
}
public async Task<T> LoadAsync<T>(string filename)
{
var encryptedJson = await _ioService.LoadAsync(filename);
var plainTextJson = _cryptographyService.Decrypt(encryptedJson);
var model = JsonConvert.DeserializeObject<T>(plainTextJson);
return model;
}
#endregion
}
private static async Task SaveAndLoadCredentials()
{
const string filename = "Credentials.txt";
var encryptedPersister = new EncryptedPersister(CryptographyService, new FileIOService());
await encryptedPersister.SaveAsync(filename, new Credentials { Username = "Bob", Password = "Password123" });
var credentials = await encryptedPersister.LoadAsync<Credentials>(filename);
Console.WriteLine($"AES Decrypted Credentials Username: {credentials.Username} Password: {credentials.Password}");
}
This leads to nice unit testing:
public class EncryptedPersisterTests : AESEncryptionTestsBase
{
#region Fields
private const string filename = "test";
private const string Username = "Bob";
private const string Password = "Password123";
private const string EncryptedText = "f4e9a35aa90ba645474e53271ee6cff4e0cb1379f10ae6a784eb5f00ce6666d06d9c0562dabb3758975006fd8aa7b7e3";
#endregion
#region Tests
[Test]
public async Task TestSaveAsync()
{
var ioServiceMock = new Mock<IIOService>();
var encryptedPersister = new EncryptedPersister(_symmetricAlgorithmCryptographyService, ioServiceMock.Object);
await encryptedPersister.SaveAsync(filename, new Credentials { Username = Username, Password = Password });
ioServiceMock.Verify(s => s.SaveAsync(filename, EncryptedText), Times.Once);
ioServiceMock.VerifyNoOtherCalls();
}
[Test]
public async Task TestLoadAsync()
{
var ioServiceMock = new Mock<IIOService>();
ioServiceMock.Setup(s => s.LoadAsync(filename)).Returns(Task.FromResult(EncryptedText));
var encryptedPersister = new EncryptedPersister(_symmetricAlgorithmCryptographyService, ioServiceMock.Object);
var credentials = await encryptedPersister.LoadAsync<Credentials>(filename);
ioServiceMock.Verify(s => s.LoadAsync(filename), Times.Once);
ioServiceMock.VerifyNoOtherCalls();
Assert.AreEqual(Username, credentials.Username);
Assert.AreEqual(Password, credentials.Password);
}
#endregion
}
Are there any holes in this? What have I not thought of?