Skip to content

Instantly share code, notes, and snippets.

@xt0rted
Last active September 18, 2023 20:27
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save xt0rted/ec780f5d404fb2b6967b18d05da033ca to your computer and use it in GitHub Desktop.
Save xt0rted/ec780f5d404fb2b6967b18d05da033ca to your computer and use it in GitHub Desktop.
C# port of Bitwarden's random password algorithm

README

This is a C# port of Bitwarden's random password algorithm.

A couple minor changes to this version from the original are:

  • The exclusion of all vowels
  • The addition of a no consecutive character check
  • No ambiguous characters by default
void Main()
{
var options = new PasswordOptions
{
Length = 13,
MinLowercase = 2,
MinNumbers = 2,
MinSpecial = 1,
MinUppercase = 2,
UseLowercase = true,
UseNumbers = true,
UseSpecial = true,
UseUppercase = true,
};
RandomPasswordGenerator.Generate(options).Dump();
}
public class RandomPasswordGenerator
{
public static string Generate(PasswordOptions options)
{
const string lowercaseChars = "bcdfghjkmnpqrstvwxyz"; // no aeiou
const string uppercaseChars = "BCDFGHJKLMNPQRSTVWXYZ"; // no AEIOU
const string numberChars = "23456789"; // no 01
const string specialChars = "!@#$%^&*";
var allChars = "";
var positions = new List<char>();
if (options.UseLowercase)
{
allChars += lowercaseChars;
if (options.MinLowercase > 0)
{
for (var i = 0; i < options.MinLowercase; i++)
{
positions.Add('l');
}
}
}
if (options.UseUppercase)
{
allChars += uppercaseChars;
if (options.MinUppercase > 0)
{
for (var i = 0; i < options.MinUppercase; i++)
{
positions.Add('u');
}
}
}
if (options.UseNumbers)
{
allChars += numberChars;
if (options.MinNumbers > 0)
{
for (var i = 0; i < options.MinNumbers; i++)
{
positions.Add('n');
}
}
}
if (options.UseSpecial)
{
allChars += specialChars;
if (options.MinSpecial > 0)
{
for (var i = 0; i < options.MinSpecial; i++)
{
positions.Add('s');
}
}
}
while (positions.Count < options.Length)
{
positions.Add('a');
}
var rnd = new CryptoRandom();
ShuffleList(rnd, positions);
var password = "";
for (var i = 0; i < positions.Count; i++)
{
string positionChars = null;
switch (positions[i])
{
case 'l':
positionChars = lowercaseChars;
break;
case 'u':
positionChars = uppercaseChars;
break;
case 'n':
positionChars = numberChars;
break;
case 's':
positionChars = specialChars;
break;
case 'a':
positionChars = allChars;
break;
default:
break;
}
var randomCharIndex = rnd.Next(0, positionChars.Length - 1);
var randomChar = positionChars[randomCharIndex];
// no consecutive characters in a row
if (i > 0 && password[i - 1] == randomChar)
{
i--;
continue;
}
password += randomChar;
}
return password;
}
private static void ShuffleList<T>(Random rnd, List<T> list)
{
var length = list.Count - 1;
while (length > 0)
{
var i = rnd.Next(0, length);
(list[i], list[length]) = (list[length], list[i]);
length--;
}
}
}
public class PasswordOptions
{
public bool UseLowercase { get; set; }
public bool UseUppercase { get; set; }
public bool UseNumbers { get; set; }
public bool UseSpecial { get; set; }
public int MinLowercase { get; set; }
public int MinUppercase { get; set; }
public int MinNumbers { get; set; }
public int MinSpecial { get; set; }
public int Length { get; set; }
}
// Copied from https://docs.microsoft.com/en-us/archive/msdn-magazine/2007/september/net-matters-tales-from-the-cryptorandom
public class CryptoRandom : Random
{
private RNGCryptoServiceProvider _rng = new RNGCryptoServiceProvider();
private byte[] _uint32Buffer = new byte[4];
public CryptoRandom()
{
}
#pragma warning disable IDE0060 // Remove unused parameter
public CryptoRandom(int ignoredSeed)
{
}
#pragma warning restore IDE0060 // Remove unused parameter
public override int Next()
{
_rng.GetBytes(_uint32Buffer);
return BitConverter.ToInt32(_uint32Buffer, 0) & 0x7FFFFFFF;
}
public override int Next(int maxValue)
{
if (maxValue < 0) throw new ArgumentOutOfRangeException(nameof(maxValue));
return Next(0, maxValue);
}
public override int Next(int minValue, int maxValue)
{
if (minValue > maxValue) throw new ArgumentOutOfRangeException(nameof(minValue));
if (minValue == maxValue) return minValue;
var diff = maxValue - minValue;
while (true)
{
_rng.GetBytes(_uint32Buffer);
var rand = BitConverter.ToUInt32(_uint32Buffer, 0);
var max = 1 + (long)uint.MaxValue;
var remainder = max % diff;
if (rand < max - remainder)
{
return (int)(minValue + (rand % diff));
}
}
}
public override double NextDouble()
{
_rng.GetBytes(_uint32Buffer);
var rand = BitConverter.ToUInt32(_uint32Buffer, 0);
return rand / (1.0 + uint.MaxValue);
}
public override void NextBytes(byte[] buffer)
{
if (buffer is null) throw new ArgumentNullException(nameof(buffer));
_rng.GetBytes(buffer);
}
}
@dantclee
Copy link

@xt0rted Why do you exclude all vowels?

@xt0rted
Copy link
Author

xt0rted commented Sep 11, 2022

@dantclee I used this to generate temporary passwords that were sent to users. Based on prior experiences it was safer to exclude vowels so they couldn't contain offensive words than it was to risk an angry user.

@dantclee
Copy link

@dantclee I used this to generate temporary passwords that were sent to users. Based on prior experiences it was safer to exclude vowels so they couldn't contain offensive words than it was to risk an angry user.

Thank you for your reply.

@dt250209
Copy link

dt250209 commented Sep 18, 2023

There is a bug in the random next() - it never returns the max value...
I changed it to this
public override int Next(int minValue, int maxValue)
{
if (minValue > maxValue) throw new ArgumentOutOfRangeException(nameof(minValue));
if (minValue == maxValue) return minValue;

var diff = maxValue - minValue;

    _rng.GetBytes(_uint32Buffer);

    var rand = BitConverter.ToUInt32(_uint32Buffer, 0);
    var remainder = rand % (diff + 1);
    return minValue + (int) remainder;

}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment