Search code examples
c#design-patternsdependency-injectionasp.net-coreinversion-of-control

Dependency Injection ASP.NET : Unaware about the dependencies until Runtime


I am using ASP.NET Core and using DI to build Hashing functionality. Since I don't know the type of hashing used at this stage (we are storing it in persistent storage).

The ICrypto Contract

public interface ICrypto
{
    string HashPassword(string plainPassword);
    bool VerifyHashedPassword(string hashedPassword, string providedPassword);     
}

I have several Implementations of ICrypto and they just wrap other libraries and provide the implementation of ICrypto Signatures. For example:

  • CryptoMD5
  • CryptoSHA1
  • CytpoOther

Now, in UserService I inject the ICyrpto to hash passwords for example:

Public class UserService
{
     ICrypto _crypto;

     public UserService(ICrypto crypto)
     {
         _crypto = crypto;
     }

    public bool Login (string username, string password)
    {   
        //code omitted
         var hash = _crypto.HashPassword(password);
    }
}

Adding Dependencies to the Container in Startup class

//get encryption type stored in cache, db or somewhere
   var cryptoType = //get param
   if (cryptoType  = "SHA1")
   {
       services.AddTransient<ICrypto, CryptoSHA1>();
   }
   else if (cryptoType  = "MD5")
   {
       services.AddTransient<ICrypto, CryptoMD5>();
   }

I am looking for a way to do this according to best practices and will reflect what Steves has mentioned.


Solution

  • In case the cryptoType value you get from the database is constant during the lifetime of the application (which means, if you want to change it, you're fine with restarting the application), this means that the cryptoType is a configuration value and you can simply wire your application as you described:

    var cryptoType = //get param
    if (cryptoType = "SHA1")
    {
        services.AddTransient<ICrypto, CryptoSHA1>();
    }
    else if (cryptoType = "MD5")
    {
        services.AddTransient<ICrypto, CryptoMD5>();
    }
    

    If however you need to swap implementations dynamically (which I find very unlikely in your specific case, but let's assume for the sake of argument), the solution is to implement a proxy and wrap the real implementations. Example:

    public interface DatabaseCryptoSelectorProxy : ICrypto
    {
        private readonly CryptoSHA1 sha;
        private readonly CryptoMD5 md5;
    
        public DatabaseCryptoSelectorProxy(CryptoSHA1 sha, CryptoMD5 md5) {
            this.sha = sha;
            this.md5 = md5;
        }
    
        public string HashPassword(string plainPassword) =>
            GetCrypto().HashPasswords(plainPassword);
    
        public bool VerifyHashedPassword(string hashedPassword, string providedPassword) =>
            GetCrypto().VerifyHashedPassword(hashedPassword, providedPassword);
    
        private ICrypto GetCrypto() {
            var cryptoType = // get param
            if (cryptoType = "SHA1") return this.sha;
            if (cryptoType = "MD5") return this.md5;
            throw new InvalidOperationException("Unknown cryptotype: " + cryptotype);
        }
    }
    

    This proxy has a few clear advantages:

    • It makes the consumers of ICrypto oblivious to the fact that some complex dispatching is happening based on some data from a database.
    • It prevents having to execute this query during object graph construction, since this would make this process unreliable and hard to verify.

    Some notes though about your design around password hashing from a security perspective. I don't see any strong reason to switch from crypto methods the way you are doing, and especially not the switch to algorithms like MD5. Instead, I advise using PBKDF2 in the form of Rfc2898DeriveBytes. An example of how to do this can be shown here. By concatenating the number of hash iterations to the hashed password (for instance by simply doing + "|" + iterations), you can later on increase the number of used iterations by keeping up with the industry standard and it allows you to automatically rehash the user's password on login if his number if you detect the number of used iterations an old value.

    Additionally, if you think that you ever need to move away from PBKDF2, you can prefix the hash with the used algorithm, this way you can again use a proxy that passes a hashed password on to the right implementation based on the algorithm-prefix. By storing the algorithm in the password hash in the database, you can migrate transparently without having to convert all existing passwords at once (which is impossible, because you can't decrypt a hashed password).