Class: PassForge::BreachChecker

Inherits:
Object
  • Object
show all
Defined in:
lib/passforge/breach_checker.rb

Overview

Breach checker using HaveIBeenPwned API Uses k-anonymity to preserve privacy (only sends first 5 chars of hash)

Constant Summary collapse

API_URL =
"https://api.pwnedpasswords.com/range/"

Class Method Summary collapse

Class Method Details

.check(password) ⇒ Hash

Check if password has been breached

Examples:

Check a password

result = PassForge::BreachChecker.check("password123")
result[:breached]  # => true
result[:count]     # => 2_389_234

Parameters:

  • password (String)

    Password to check

Returns:

  • (Hash)

    Breach information



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/passforge/breach_checker.rb', line 23

def self.check(password)
  raise ArgumentError, "Password cannot be empty" if password.nil? || password.empty?

  # Generate SHA-1 hash
  hash = Digest::SHA1.hexdigest(password).upcase
  prefix = hash[0..4]
  suffix = hash[5..-1]

  # Query API with prefix only (k-anonymity)
  response = query_api(prefix)
  
  return { breached: false, count: 0 } if response.nil?

  # Check if our suffix appears in the response
  count = parse_response(response, suffix)
  
  {
    breached: count > 0,
    count: count
  }
rescue StandardError => e
  # Return safe default on error
  {
    breached: nil,
    count: 0,
    error: e.message
  }
end

.parse_response(response, suffix) ⇒ Object

Parse API response to find suffix match



74
75
76
77
78
79
80
81
# File 'lib/passforge/breach_checker.rb', line 74

def self.parse_response(response, suffix)
  response.each_line do |line|
    hash_suffix, count = line.strip.split(":")
    return count.to_i if hash_suffix == suffix
  end
  
  0
end

.query_api(prefix) ⇒ Object

Query HaveIBeenPwned API



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/passforge/breach_checker.rb', line 54

def self.query_api(prefix)
  uri = URI("#{API_URL}#{prefix}")
  
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true
  http.open_timeout = 5
  http.read_timeout = 5
  
  request = Net::HTTP::Get.new(uri)
  request["User-Agent"] = "PassForge-RubyGem"
  
  response = http.request(request)
  
  return nil unless response.is_a?(Net::HTTPSuccess)
  
  response.body
end