Skip to content

Instantly share code, notes, and snippets.

@jbtule
Last active September 18, 2020 04:36
Show Gist options
  • Save jbtule/d98faec0abddea3d50180df54c33b617 to your computer and use it in GitHub Desktop.
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
#!/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