Last active
March 13, 2019 17:01
-
-
Save jbtule/7dab1a1031590209bdcfac88ee71cb2b to your computer and use it in GitHub Desktop.
haveibeenpwned.com script to check a password
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env dotnet-script | |
/* | |
* This work (haveibeenpwnded.csx by James Tuley), | |
* identified by James Tuley, is free of known copyright restrictions | |
* Source: https://gist.github.com/jbtule/7dab1a1031590209bdcfac88ee71cb2b | |
* http://creativecommons.org/publicdomain/mark/1.0/ | |
* | |
* This script uses the Troy Hunt's HaveIBeenPwned.com range api, to search for passwords, | |
* without revealing what you are searching for. | |
* | |
* Requirements: | |
* .net core 2.1 installed. | |
* https://dotnet.microsoft.com/download/dotnet-core/2.1 | |
* dotnet-script tool -- it can be installed via `dotnet tool install -g dotnet-script` | |
* https://github.com/filipw/dotnet-script | |
* | |
* Usage: | |
* linux & mac: ./haveibeenpwned.csx | |
* windows: dotnet script haveibeenpwned.csx | |
*/ | |
using System; | |
using System.Net; | |
using System.Net.Http; | |
using System.Net.Http.Headers; | |
using System.Runtime.InteropServices; | |
using System.Security; | |
using System.Security.Cryptography; | |
/* ReadPassword | |
Reads keypresses into secure buffer. | |
*/ | |
public static SecureString ReadPassword(bool showKeyPress = false) { | |
var password = new SecureString(); | |
ConsoleKeyInfo nextKey; | |
var finished = false; | |
do { | |
nextKey = Console.ReadKey(intercept:true); | |
switch (nextKey.Key) { | |
case ConsoleKey.Backspace: | |
if (password.Length > 0) { | |
password.RemoveAt(password.Length - 1); | |
Console.SetCursorPosition(Console.CursorLeft-1,Console.CursorTop); | |
Console.Write(' '); | |
Console.SetCursorPosition(Console.CursorLeft-1,Console.CursorTop); | |
} | |
break; | |
case ConsoleKey.Enter: | |
finished = true; | |
Console.WriteLine(); | |
break; | |
default: | |
password.AppendChar(nextKey.KeyChar); | |
Console.Write(showKeyPress ? nextKey.KeyChar : '*'); | |
break; | |
} | |
} while(!finished); | |
password.MakeReadOnly(); | |
return password; | |
} | |
/* GetPasswordHash | |
Tries to make a sha1 hash of a raw password while reducing allocations | |
Tries to clean up raw password copies afterward | |
*/ | |
public static string GetPasswordHash() { | |
Console.Write("Password To Check: "); | |
//dispose of secure password when finsihed. | |
using(var securePassword = ReadPassword()) { | |
//Copies securePassword to unmanaged memory | |
IntPtr rawPassword = Marshal.SecureStringToGlobalAllocUnicode(securePassword); | |
try { | |
var charLength = securePassword.Length; | |
ReadOnlySpan<char> charPassword; | |
unsafe { | |
charPassword = new ReadOnlySpan<char>((void*)rawPassword, charLength); | |
} | |
//encode as utf8 | |
var utf8Length = Encoding.UTF8.GetByteCount(charPassword); | |
Span<byte> utf8Password = stackalloc byte[utf8Length]; | |
//compute sha1 hash | |
try{ | |
var _ = Encoding.UTF8.GetBytes(charPassword, utf8Password); | |
using(var sha1Alg = HashAlgorithm.Create("SHA1")) { | |
var hashLength = sha1Alg.HashSize / 8; | |
var hashOfPassword = new byte[hashLength]; | |
if (!sha1Alg.TryComputeHash(utf8Password, hashOfPassword, out var _)) { | |
throw new Exception("Failed to hash Password"); | |
} | |
//convert to hexadecimal | |
string HexValue(byte b) => b.ToString("X2"); | |
return String.Concat(Array.ConvertAll(hashOfPassword, HexValue)); | |
} | |
} finally { | |
//zeroout utf8 byte password when finished | |
utf8Password.Clear(); | |
} | |
} finally { | |
//zeros out password from unmanaged memory | |
Marshal.ZeroFreeGlobalAllocUnicode(rawPassword); | |
} | |
} | |
} | |
public enum Status{ | |
Okay = 0, | |
Pwned, | |
Error | |
} | |
public static Status Run() { | |
Console.WriteLine("Have I been Pwned?"); | |
using(var client = new HttpClient()) { | |
//Using https://haveibeenpwned.com/API/v2#PwnedPasswords | |
client.BaseAddress = new Uri("https://api.pwnedpasswords.com/range/"); | |
var hash = GetPasswordHash(); | |
var hashPrefix = hash.Substring(0,5); | |
var hashSuffix = hash.Substring(5); | |
/* CheckPwned | |
uses Password range search of haveibeenpwnded | |
to keep password from being shared with third party. | |
*/ | |
async Task<Status> CheckPwned(int tries) { | |
var response = await client.GetAsync(hashPrefix); | |
if (!response.IsSuccessStatusCode) | |
{ | |
if (response.StatusCode == HttpStatusCode.TooManyRequests && tries < 5) | |
{ | |
var retryAfter = response.Headers.RetryAfter.Delta; | |
if (retryAfter is TimeSpan rA) { | |
System.Threading.Thread.Sleep(rA.Milliseconds); | |
return await CheckPwned(tries + 1); | |
} | |
} | |
Console.WriteLine($"Error {response.StatusCode} Checking Site. Network Problem?"); | |
return Status.Error; | |
} | |
using (var body = await response.Content.ReadAsStreamAsync()) | |
using (var reader = new StreamReader(body)) { | |
while (reader.Peek() > -1) { | |
var foundHash = await reader.ReadLineAsync(); | |
if (foundHash.StartsWith(hashSuffix, StringComparison.OrdinalIgnoreCase)) { | |
var countString = foundHash.Substring(foundHash.IndexOf(':') + 1); | |
var count = Int32.Parse(countString); | |
Console.WriteLine($"Found {count:N0} times! This Password is Pwned!!"); | |
return Status.Pwned; | |
} | |
} | |
} | |
Console.WriteLine("Not Found! Keep it secret, keep it safe!"); | |
return Status.Okay; | |
} | |
return CheckPwned(tries:0).Result; //Call Synchronously | |
} | |
} | |
Environment.Exit((int)Run()); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment