in src/PwnedPasswordsSearch/PwnedSearch.cs [21:66]
public static bool IsPwnedPassword(string plaintext)
{
try
{
SHA1 sha = new SHA1CryptoServiceProvider();
byte[] data = sha.ComputeHash(Encoding.UTF8.GetBytes(plaintext));
// Loop through each byte of the hashed data and format each one as a hexadecimal string.
var sBuilder = new StringBuilder();
for (int i = 0; i < data.Length; i++)
sBuilder.Append(data[i].ToString("x2"));
var result = sBuilder.ToString().ToUpper();
// Get a list of all the possible password hashes where the first 5 bytes of the hash are the same
var url = "https://api.pwnedpasswords.com/range/" + result.Substring(0, 5);
WebRequest request = WebRequest.Create(url);
using var response = request.GetResponse().GetResponseStream();
using var reader = new StreamReader(response);
// Iterate through all possible matches and compare the rest of the hash to see if there is a full match
// TODO: optimize-async this
string hashToCheck = result.Substring(5);
string line = null;
do
{
line = reader.ReadLine();
if (line != null)
{
string[] parts = line.Split(':');
if (parts[0] == hashToCheck) // This is a full match: plaintext compromised!!!!
{
System.Diagnostics.Debug.Print("The password '{plaintext}' is publicly known and can be used in dictionary attacks");
return true;
}
}
} while (line != null);
// We've run through all the candidates and none of them is a full match
return false; // This plaintext is not publicly known
}
catch (Exception)
{
// If any weird things happens, it is safer to suppose this plaintext is compromised (hence not to be used).
return true; // Better safe than sorry.
}
}