4
\$\begingroup\$

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);
}

Code Reference

Full codebase with unit tests

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
}

Code Reference

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
}

Code Reference

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
}

Code Reference

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
}

Code Reference

Are there any holes in this? What have I not thought of?

\$\endgroup\$

2 Answers 2

5
\$\begingroup\$

People usually pay not much attention to that, but redundant *Manager, *Processor, *Service suffixes are just some form of signal noise. It is also questionable that two way is really needed the same time at the same place on the consuming side. I would go with:

interface IEncryptor 
{
     byte[] Encrypt(byte array);
}

public static class Encryptor
{
     public static string Encrypt(this IEncryptor encryptor, string text) =>
         (Hex)encryptor.Encrypt((UTF8)text); 
} 

And:

interface IDecryptor 
{
     byte[] Decrypt (byte array);
}

public static class Decryptor
{
     public static string Decrypt(this IDecryptor encryptor, string text) =>
         (UTF8)encryptor.Decrypt((Hex)text); 
} 

The helper would be:

class Hex
{
    public static explicit operator Hex(string hex) => ...
    public static explicit operator Hex(byte[] bytes) => ...

    public static implicit operator string(Hex hex) => ...
    public static implicit operator byte[](Hex hex) => ...
}

And:

class UTF8
{
    public static explicit operator UTF8(string hex) => ...
    public static explicit operator UTF8(byte[] bytes) => ...

    public static implicit operator string(UTF8 utf) => ...
    public static implicit operator byte[](UTF8 utf) => ...
}

Now we will have:

public interface IMD5CSP : IEncryptor { … }

public class MD5CSP : IMD5CSP, IDisposable { … }

And I would consume it allowing to be precise in ctor, but having no concrete dependencies later in the code:

class MyService 
{           
      public MyService(IMD5CSP csp) => Encryptor = csp;
      IEncryptor Encryptor { get; }
}
\$\endgroup\$
2
\$\begingroup\$
public interface IOneWayEncryptionService
{
    string Encrypt(string text);
}

public interface ICryptographyService : IOneWayEncryptionService
{
    string Decrypt(string text);
}

These seem to be crippled. The basic operations on which almost everything else can be built are

byte[] Encrypt(byte[] plaintext, byte[] key, byte[] iv)
byte[] Decrypt(byte[] ciphertext, byte[] key, byte[] iv)

or

byte[] Encrypt(byte[] plaintext, byte[] key)
byte[] Decrypt(byte[] ciphertext, byte[] key)

if the implementations are designed to generate a random IV and to include it as a prefix of the ciphertext.

However, if you want to use the full power of modern cipher modes (e.g. Galois Counter Mode) you get something like

byte[] Encrypt(byte[] plaintext, byte[] authenticated_data, byte[] key, byte[] iv)
(byte[] plaintext, byte[] authenticated_data) Decrypt(byte[] ciphertext, byte[] key, byte[] iv)

and need to document clearly that the output of Encrypt doesn't include the authenticated_data.


public class MD5CryptographyService : IOneWayEncryptionService

No, no, no!!! Hashing is not encryption. I know Microsoft's System.Security.Cryptography sets a bad example with ICryptoTransform, but they do at least distinguish HashAlgorithm from SymmetricAlgorithm. If you want to write maintainable code, you should do everything in your power to prevent the two concepts from being confused with each other.

\$\endgroup\$
5
  • \$\begingroup\$ Why would you pass the extra parameters in to the methods when that data could be injected in to the classes via the constructor? By passing those values as parameters the interfaces become less flexible. \$\endgroup\$ Commented Oct 14, 2019 at 10:09
  • \$\begingroup\$ It can only be injected via the constructor if it's known at that point in time and guaranteed to be immutable. In general, I don't think that either of those are true. If they're true in your particular use case, the point still stands that using string rather than byte[] cripples the API. \$\endgroup\$ Commented Oct 14, 2019 at 10:13
  • \$\begingroup\$ I'm not sure I agree. The majority of the time that encryption is necessary, it is to encrypt a string. If the encoding is not done as part of the process, it just becomes an extra line of code outside the API that litters the codebase. \$\endgroup\$ Commented Oct 14, 2019 at 10:22
  • \$\begingroup\$ (1) You can turn an arbitrary string into a sequence of bytes efficiently using UTF8Encoder. But turning an arbitrary sequence of bytes into a string efficiently is messier. (2) The implementations themselves are all defined in terms of bytes. (Conclusion) The best way to do it is to define the interface in terms of bytes and have extension methods which package together the string encoding/decoding and the encryption. \$\endgroup\$ Commented Oct 14, 2019 at 10:26
  • \$\begingroup\$ You're right in a sense, but that's the way the Microsoft classes were originally designed, and what you find is coders patching over them left right and centre everywhere. This answer doesn't solve the issues that what I put together tried to solve. It would however have been a good design for the original c# encryption classes. \$\endgroup\$ Commented Oct 14, 2019 at 11:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.