Password Entropy in C#

Password entropy is a measurement of how unpredictable a password is. Password entropy is based on the character set used (which is expansible by using lowercase, uppercase, numbers as well as symbols) as well as password length. Password entropy predicts how difficult a given password would be to crack through guessing, brute force cracking, dictionary attacks or other common methods.

Source here.

⚠️Warning

Keep in mind this is just a simple calculator and do not rely in this metric only to check if a password is strong or not. Use the default ASP .NET Identity along with Have I Been Pwned Api to make sure you only accept strong mixed passwords.

How to design this in C#? Let's first split the logic to understand what we want to have. As big picture we want something easy, reusable, that will tell us exactly what we need to know about a password strength.

Password Entropy initial idea

As initial idea, let's write the enum, interface and objects from the scratch. Also, you can find all this source code in my GitHub repository.

public enum PasswordEntropyStrength
{
    Blank = 0,
    VeryWeak = 1,
    Weak = 2,
    Good = 3,
    Strong = 4
}

public class PasswordEntropyModel
{
    public double Entropy { get; init; }
    public PasswordEntropyStrength Strength { get; init; }
}

public interface IPasswordEntropy
{
    PasswordEntropyStrength GetStrength(string password);
    PasswordEntropyModel Calculate(string password);
}

With that defined and out of the way, now we just need to implement those. Keep in mind these results are not the initial ones, they are a combination of multiple attempts and try to fit as generic as possible. Just a quick note as well, I've decided to make this modular as possible to be easy to:

  • maintain
  • test
  • scale

This will allow me to do faster changes in future, by now taking a little extra time to make it. For that, I will create a new interface that allowed me to calculate several entropy types, called IPasswordEntropyType.

internal interface IPasswordEntropyType
{
    public bool ContainsMatch(string password);
    int CalculateValue(string password);
}

With this IPasswordEntropyType definition I can add multiple implementations with some options and have this dynamic and modular. From a code perspective, these types cannot be outside rather within the IPasswordEntropy implementation. Because most of my implementations will use regular expressions, I can easily create a base class for this type.

public class PasswordEntropyOptions
{
    public bool UseMinimumCharacters { get; set; } = true;
    public bool UseLowerCase { get; set; } = true;
    public bool UseUpperCase { get; set; } = true;
    public bool UseNumeric { get; set; } = true;
    public bool UseSymbols { get; set; } = true; 
    public bool UseHexadecimal { get; set; } = false;
}

internal abstract class RegularExpPasswordEntropyType : IPasswordEntropyType
{
    protected abstract Regex Regex { get; }
    protected abstract int Size { get; }

    public int CalculateValue(string password)
    {
        return ContainsMatch(password) ? Size : 0;
    }

    public bool ContainsMatch(string password)
    {
        return Regex.IsMatch(password);
    }
}

After that, I can easily use my own type implementations.

internal class LowerCasePasswordEntropyType : RegularExpPasswordEntropyType
{
    protected override Regex Regex => new("[a-z]", RegexOptions.Compiled);
    protected override int Size => 26;
}

internal class UpperCasePasswordEntropyType : RegularExpPasswordEntropyType
{
    protected override Regex Regex => new("[A-Z]", RegexOptions.Compiled);
    protected override int Size => 26;
}

internal class NumericPasswordEntropyType : RegularExpPasswordEntropyType
{
    protected override Regex Regex => new("[0-9]", RegexOptions.Compiled);
    protected override int Size => 10;
}

internal class HexadecimalPasswordEntropyType : RegularExpPasswordEntropyType
{
    protected override Regex Regex => new("[a-fA-F0-9]", RegexOptions.Compiled);
    protected override int Size => 16;
}

internal class SymbolsPasswordEntropyType : IPasswordEntropyType
{
    protected static string Symbols => ",.?!\"£$%^&*()-_=+[]{};:\'@#~<>/\\|`¬¦";

    public int CalculateValue(string password)
    {
        return ContainsMatch(password) ? password.Length : 0;
    }

    public bool ContainsMatch(string password)
    {
        return Symbols.Any(s => password.Contains(s));
    }
}

internal class MinimumCharacterPasswordEntropyType : IPasswordEntropyType
{
    protected const int MinimumCharacters = 8;

    public int CalculateValue(string password)
    {
        return 0;
    }

    public bool ContainsMatch(string password)
    {
        return password.Length >= MinimumCharacters;
    }
}

Now, I'm going to create some options for this and put all the pieces of the puzzle together.

public class PasswordEntropy : IPasswordEntropy
{
    private const int BlankPasswordEntropy = 0;

    private readonly IReadOnlyCollection<IPasswordEntropyType> _passwordEntropyTypes;

    private readonly PasswordEntropyOptions _options;

    public PasswordEntropy(IOptions<PasswordEntropyOptions> options)
    {
        _options = options.Value;
        _passwordEntropyTypes = GetPasswordEntropyTypes(_options);
    }

    public PasswordEntropyStrength GetStrength(string password)
    {
        if (string.IsNullOrWhiteSpace(password))
            return PasswordEntropyStrength.Blank;

        var entropy = GetEntropy(password);

        return Evaluate(password, entropy);
    }

    public PasswordEntropyModel Calculate(string password)
    {
        double entropy = GetEntropy(password);
        var passwordEntropyStrength = Evaluate(password, entropy);

        return new PasswordEntropyModel
        {
            Strength = passwordEntropyStrength,
            Entropy = entropy
        };
    }

    internal static List<IPasswordEntropyType> GetPasswordEntropyTypes(PasswordEntropyOptions options)
    {
        var types = new List<IPasswordEntropyType>();

        if (options.UseMinimumCharacters)
            types.Add(new MinimumCharacterPasswordEntropyType());

        if (options.UseLowerCase)
            types.Add(new LowerCasePasswordEntropyType());

        if (options.UseUpperCase)
            types.Add(new UpperCasePasswordEntropyType());

        if (options.UseNumeric)
            types.Add(new NumericPasswordEntropyType());

        if (options.UseSymbols)
            types.Add(new SymbolsPasswordEntropyType());

        if (options.UseHexadecimal)
            types.Add(new HexadecimalPasswordEntropyType());

        return types;
    }

    private double GetEntropy(string password)
    {
        int alphabetSize = 0;

        if (string.IsNullOrEmpty(password))
            return BlankPasswordEntropy;

        foreach (var type in _passwordEntropyTypes)
        {
            alphabetSize += type.CalculateValue(password);
        }

        double possibleCombinations = Math.Pow(alphabetSize, password.Length);

        return Math.Log(possibleCombinations, 2);
    }
    
    private static PasswordEntropyStrength Evaluate(string password, double entropy)
    {
        if (string.IsNullOrWhiteSpace(password))
            return PasswordEntropyStrength.Blank;

        if (entropy < 20)
            return PasswordEntropyStrength.VeryWeak;

        if (entropy < 50)
            return PasswordEntropyStrength.Weak;

        if (entropy < 60)
            return PasswordEntropyStrength.Good;

        return PasswordEntropyStrength.Strong;
    }
}

Now, the easy part. Let's use this. First we need to register our interface.

services.AddScoped<IPasswordEntropy, PasswordEntropy>();

Every time I want to use this, I just need to inject it from the constructor.

public class Foo
{
    private readonly IPasswordEntropy _passwordEntropy;

    public Foo(IPasswordEntropy passwordEntropy)
    {
        _passwordEntropy = passwordEntropy;
    }
}

And the usage is very simple:

public void Usage()
{
    const string pwd = "my-awesome-password-here";

    // PasswordEntropyStrength strength = _passwordEntropy.GetStrength(pwd);
    // model.Strength is the same as '_passwordEntropy.GetStrength(pwd)'
    PasswordEntropyModel model = _passwordEntropy.Calculate(pwd);

    if (model.Entropy < 80)
        throw new Exception("Week password");

    if (model.Strength != PasswordEntropyStrength.Good 
            && model.Strength != PasswordEntropyStrength.Strong)
        throw new Exception("Week password");
}

Here's the source code where you can clone, add pull requests, download and do whatever you feel like with the code.