Skip to content

Instantly share code, notes, and snippets.

@urmastalimaa
Last active January 17, 2019 10:47
Show Gist options
  • Save urmastalimaa/2c9b94cda155c8fe8876471cf65f17bd to your computer and use it in GitHub Desktop.
Save urmastalimaa/2c9b94cda155c8fe8876471cf65f17bd to your computer and use it in GitHub Desktop.
Have I been pwned check via a ruby script
# frozen_string_literal: true
# Requires ruby 2.1 or newer
#
# This script checks a single password or a list of passwords against
# pwnedpasswords HTTP API. The passwords are hashed and anonymized before
# sending them over the wire. Each password that has been pwned will be printed
# to standard out in clear text with the known pwnage count.
#
# Read more at https://haveibeenpwned.com/API/v2#PwnedPasswords
# Checks whether a password has been pwned against Troy Hunt's pwnedpasswords
# HTTP API. If you want to check multiple passwords, reuse the instance to use
# the same HTTP connection.
#
# @example single password
# > ruby ./pwned_check.rb secret
#
# @example multiple passwords from file, delimiter is used to split the file
# > ruby ./pwned_check.rb ./my_passwords.txt
#
# @example multiple passwords from arguments
# > ruby ./pwned_check.rb --delimiter=, secret,sauce
if RUBY_VERSION.split('.').first.to_i < 2
raise UnsupportedRubyVersion('Need ruby version 2.1 or newer')
end
# @example
# passwords = ['secret', 'verysecret']
# checker = PwnedChecker.new
# passwords.each do |password|
# puts "#{password} has been pwned #{checker.pwned_count('secret')} times"
# end
class PwnedChecker
require 'digest/sha1'
require 'net/http'
PARTIAL_HASH_LENGTH = 5
def initialize
@conn = Net::HTTP.new('api.pwnedpasswords.com', 443)
@conn.use_ssl = true
end
def pwned_count(password)
if (occurrence = find_occurrence(password))
occurrence.split(':')[1].to_i
else
0
end
end
private
def find_occurrence(password)
password_hash = sha1(password)
hash_head = password_hash[0...PARTIAL_HASH_LENGTH]
hash_tail = password_hash[PARTIAL_HASH_LENGTH..-1]
partial_occurrences(hash_head).find { |occ| occ.start_with?(hash_tail) }
end
def sha1(password)
Digest::SHA1.hexdigest(password).upcase
end
def partial_occurrences(hash_head)
@conn.start unless @conn.started?
@conn.get("/range/#{hash_head}").body.split("\r\n")
end
end
# Expands given input to create a list of passwords.
#
# If opts.input is a file, reads the file and splits it by opts.delimiter.
# If it is not a file, splits opts.input by opts.delimiter.
# In any case, the resulting passwords are stripped of leading and trailing
# whitespace and quotes.
class PasswordReader
def self.read!(opts)
new.read!(opts)
end
def read!(opts)
expand_input(opts).map(&method(:strip))
end
private
def strip(word)
word
.strip
.sub(/^'/, '')
.chomp("'")
.sub(/^"/, '')
.chomp('"')
end
def expand_input(opts)
raw_input = opts.input
if File.file?(raw_input)
File.read(raw_input).split(opts.delimiter)
else
raw_input.split(opts.delimiter)
end
end
end
def read_opts!
require 'optparse'
require 'ostruct'
delimiter = Regexp.compile(/[\n,\r]+/)
opt_parser = OptionParser.new do |parser|
parser.banner = "Usage: ruby ./pwned_check.rb secret\nUsage: ruby ./pwned_check.rb my_passwords.txt"
parser.on('-d=delimiter', '--delimiter=delimiter', String, 'Password delimiter, used both when reading from arguments or file. Defaults to \n or \r\n') do |input_delimiter|
delimiter = input_delimiter
end
parser.on('-h', '--help', 'Display this screen') do
puts parser
exit
end
end
input = opt_parser.parse!.first
OpenStruct.new(input: input, delimiter: delimiter)
end
pwned_checker = PwnedChecker.new
passwords = PasswordReader.read!(read_opts!)
passwords.each do |password|
count = pwned_checker.pwned_count(password)
puts "#{password}: pwned #{count} times" if count > 0
end
@urmastalimaa
Copy link
Author

Quick & dirty, can be expanded to check multiple passwords

@urmastalimaa
Copy link
Author

The first command line argument can be:

  • password to check
  • file of passwords, all passwords will be checked. Passwords must be separated by the newline \n charatacter

@urmastalimaa
Copy link
Author

Made more robust and lessened ruby version requirements, allowed specifying delimiter.

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