Last active
January 17, 2019 10:47
-
-
Save urmastalimaa/2c9b94cda155c8fe8876471cf65f17bd to your computer and use it in GitHub Desktop.
Have I been pwned check via a ruby script
This file contains hidden or 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
# 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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Made more robust and lessened ruby version requirements, allowed specifying delimiter.