Skip to content

Instantly share code, notes, and snippets.

@dontmitch
Last active January 17, 2019 15:00
Show Gist options
  • Save dontmitch/bfcadac8dd45cc92f183068a10212b1a to your computer and use it in GitHub Desktop.
Save dontmitch/bfcadac8dd45cc92f183068a10212b1a to your computer and use it in GitHub Desktop.
Pwned Passwords Checker for LastPass

Pwned Passwords Checker for LastPass

This Ruby script allows you to easily check if any of your LastPass passwords appear in the Pwned Passwords database, indicating they've been compromised in one or more data breaches.

You can read more about the Pwned Passwords database and version two of the API in Troy Hunt's blog post.

Importantly, this script uses the Range API. That means that your actual passwords do not leave your computer. Instead, a short prefix of the SHA1 hash of your password is used to query for potentially matching SHA1 hashes of passwords that have been compromised. The full SHA1 hash of your password is then compared to those results locally to see if there is a match. You can read more about this system, known as k-anonymity, in the blog post.

To use this checker:

  1. Export your LastPass passwords using the CLI (lpass export > lastpass.csv) or the plugin (More Options → Advanced → Export).
  2. Save the Ruby script and the CSV in the same directory.
  3. Run the Ruby script using the command ruby check-passwords -a.

Any matched passwords will be printed along with its number of occurrences in the Pwned Passwords database.

require 'csv'
require 'digest/sha1'
require 'net/http'
# Assumes there is a passwords.csv file generated from a LastPass export
# in this directory, and that the passwords are in the third column.
def unique_passwords
CSV.read('./passwords.csv', headers: false).map { |r| r[2] }.compact.uniq
end
def fetch_range(hash_prefix)
uri = URI("https://api.pwnedpasswords.com/range/#{hash_prefix}")
req = Net::HTTP::Get.new(uri, { 'User-Agent' => 'dontmitch-pwned-password-checker' })
https = Net::HTTP.new(uri.hostname, uri.port)
https.use_ssl = true
resp = https.request(req)
if resp.is_a?(Net::HTTPSuccess)
resp.body
else
puts "#{resp.code}: #{resp.message}\n#{resp.body}"
raise "API Error"
end
end
def check_passwords!
num_checked = 0
unique_passwords.each do |password|
hashed_password = Digest::SHA1.hexdigest(password)
prefix, suffix = hashed_password[0..4], hashed_password[5..-1]
passwords_in_range = fetch_range(prefix).downcase.split("\r\n").map { |r| r.split(':') }.to_h
if count = passwords_in_range[suffix.downcase]
puts "#{count} #{count > 1 ? 'occurences' : 'occurence'} found of the password #{password}."
end
num_checked += 1
end
puts "#{num_checked} #{num_checked > 1 ? 'passwords' : 'password'} checked."
end
if ARGV[0] == '-a'
check_passwords!
else
puts "Usage:"
puts "\t`ruby check-passwords -a` to run for all passwords."
end
@OlivierJaquemet
Copy link

👍 Thanks @dontmitch for this script.

One thing I had to fix to make it work in my latest environment, replace :
if count = passwords_in_range[suffix.downcase]
with
if count = passwords_in_range[suffix.downcase].to_i

Regards
Olivier

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