Last active
September 18, 2020 04:36
-
-
Save jbtule/d98faec0abddea3d50180df54c33b617 to your computer and use it in GitHub Desktop.
This script uses the Troy Hunt's HaveIBeenPwned.com range api, to search for passwords
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 -S dotnet fsi --langversion:preview | |
(* | |
* This work (haveibeenpwned.fsx by James Tuley), | |
* identified by James Tuley, is free of known copyright restrictions | |
* Source: https://gist.github.com/jbtule/d98faec0abddea3d50180df54c33b617 | |
* 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 5.0 installed. | |
* https://dotnet.microsoft.com/download/dotnet/5.0 | |
* | |
* Usage: | |
* linux & mac: ./haveibeenpwned.fsx | |
* windows: dotnet fsi --langversion:preview haveibeenpwned.csx | |
*) | |
#nowarn "9" //doing unsafe things with pointers to protect passwords | |
#r "nuget: TaskBuilder.fs, Version=2.1.0" | |
open System | |
open System.IO | |
open System.Net | |
open System.Net.Http | |
open System.Net.Http.Headers | |
open System.Runtime.InteropServices | |
open System.Security | |
open System.Security.Cryptography | |
open System.Text | |
open System.Threading.Tasks | |
open FSharp.NativeInterop | |
open FSharp.Control.Tasks.V2 | |
type Password = | |
static member SecurePrompt(?showKeyPress) = | |
// secure string only encrypts on windows, | |
// but still better than a string on other platforms | |
let password = new SecureString() | |
let showKeyPress' = defaultArg showKeyPress false | |
let mutable finished = false | |
while not finished do | |
let nextKey = Console.ReadKey(intercept = true) | |
match nextKey.Key with | |
| ConsoleKey.Backspace -> | |
if password.Length > 0 then | |
password.RemoveAt(password.Length - 1) | |
Console.SetCursorPosition(Console.CursorLeft - 1, Console.CursorTop) | |
Console.Write(' ') | |
Console.SetCursorPosition(Console.CursorLeft - 1, Console.CursorTop) | |
| ConsoleKey.Enter -> | |
finished <- true | |
Console.WriteLine() | |
| _ -> | |
password.AppendChar(nextKey.KeyChar) | |
Console.Write(if showKeyPress' then nextKey.KeyChar else '*') | |
password.MakeReadOnly() | |
password | |
///Some helpers for dealing with spans | |
/// = **note**: don't pipeline these!!! | |
let inline makeSpan<'a when 'a : unmanaged> length (ptr:'a nativeptr) = Span<'a>(ptr |> NativePtr.toVoidPtr, length) | |
let inline makeROSpan<'a when 'a : unmanaged> length (ptr:'a nativeptr) = ReadOnlySpan<'a>(ptr |> NativePtr.toVoidPtr, length) | |
/// promptAndHash | |
/// - Tries to make a sha1 hash of a raw password while reducing allocations | |
/// - Tries to clean up raw password copies afterward | |
let promptAndHash () = | |
Console.Write("Password To Check: ") | |
// dispose of secure password when finsihed. | |
use securePassword = Password.SecurePrompt() | |
// Copies securePassword to unmanaged memory | |
let rawPassword = Marshal.SecureStringToGlobalAllocUnicode(securePassword) | |
try | |
let charLength = securePassword.Length | |
let charPassword = makeROSpan<char> charLength (NativePtr.ofNativeInt rawPassword) | |
// encode as utf8 | |
let utf8Length = Encoding.UTF8.GetByteCount(charPassword) | |
let utf8Password = makeSpan<byte> utf8Length (NativePtr.stackalloc utf8Length) | |
// compute sha1 hash | |
try | |
Encoding.UTF8.GetBytes(charPassword, utf8Password) |> ignore | |
use sha1Alg = HashAlgorithm.Create("SHA1") | |
let hashLength = sha1Alg.HashSize / 8 | |
let hashOfPassword = Array.zeroCreate<byte> hashLength | |
let success, _ = sha1Alg.TryComputeHash(Span<_>.op_Implicit utf8Password, Span hashOfPassword) | |
if not success then failwith "Failed to hash Password" | |
// convert to hexadecimal | |
hashOfPassword | |
|> Array.map (sprintf "%02X") | |
|> String.concat String.Empty | |
finally | |
// zeroout utf8 byte password when finished | |
utf8Password.Clear() | |
finally | |
// zeros out password from unmanaged memory | |
Marshal.ZeroFreeGlobalAllocUnicode(rawPassword) | |
type Status = | Okay = 0 | Pwned = 1 | Error = -1 | |
let run = | |
async { | |
Console.WriteLine "Have I been Pwned?" | |
use client = new HttpClient() | |
// Using https://haveibeenpwned.com/API/v3#SearchingPwnedPasswordsByRange | |
client.BaseAddress <- Uri("https://api.pwnedpasswords.com/range/") | |
let hash = promptAndHash () | |
let hashPrefix = hash.[0..4] | |
let hashSuffix = hash.[5..] | |
/// CheckPwned | |
/// - uses Password range search of haveibeenpwnded | |
/// to protect password from being shared with third party. | |
let rec checkPwned tries = | |
task { | |
let! response = client.GetAsync(hashPrefix) | |
match response.IsSuccessStatusCode, response.StatusCode with | |
| _, HttpStatusCode.TooManyRequests when tries < 5 -> | |
let retryAfter = | |
defaultValueArg | |
(response.Headers.RetryAfter.Delta |> ValueOption.ofNullable) | |
(TimeSpan.FromSeconds 1.0) | |
do! Task.Delay(retryAfter) | |
return! checkPwned (tries + 1) | |
| false, _ -> | |
Console.WriteLine($"Error %O{response.StatusCode} Checking Site. Network Problem?") | |
return Status.Error | |
| true, _ -> | |
use! body = response.Content.ReadAsStreamAsync() | |
use reader = new StreamReader(body) | |
let mutable count = None | |
while reader.Peek() > -1 && count |> Option.isNone do | |
let! foundHash = reader.ReadLineAsync() | |
if foundHash.StartsWith(hashSuffix, StringComparison.OrdinalIgnoreCase) then | |
let countString = | |
foundHash.[foundHash.IndexOf(":") + 1..] | |
count <- Some <| Int32.Parse(countString) | |
match count with | |
| Some c -> | |
Console.WriteLine($"Found %0i{c} times! This Password is Pwned!!") | |
return Status.Pwned | |
| None -> | |
Console.WriteLine("Not Found! Keep it secret, keep it safe!") | |
return Status.Okay | |
} | |
return! 0 |> checkPwned |> Async.AwaitTask | |
} | |
run |> Async.RunSynchronously |> int |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment