pwned 1.2.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pwned/password_base"
4
+
5
+ module Pwned
6
+ ##
7
+ # This class represents a hashed password. It does all the work of talking to the
8
+ # Pwned Passwords API to find out if the password has been pwned.
9
+ # @see https://haveibeenpwned.com/API/v2#PwnedPasswords
10
+ class HashedPassword
11
+ include PasswordBase
12
+ ##
13
+ # Creates a new hashed password object.
14
+ #
15
+ # @example A simple password with the default request options
16
+ # password = Pwned::HashedPassword.new("ABC123")
17
+ # @example Setting the user agent and the read timeout of the request
18
+ # password = Pwned::HashedPassword.new("ABC123", headers: { "User-Agent" => "My user agent" }, read_timout: 10)
19
+ #
20
+ # @param hashed_password [String] The hash of the password you want to check against the API.
21
+ # @param [Hash] request_options Options that can be passed to +Net::HTTP.start+ when
22
+ # calling the API
23
+ # @option request_options [Symbol] :headers ({ "User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}" })
24
+ # HTTP headers to include in the request
25
+ # @raise [TypeError] if the password is not a string.
26
+ # @since 2.1.0
27
+ def initialize(hashed_password, request_options={})
28
+ raise TypeError, "hashed_password must be of type String" unless hashed_password.is_a? String
29
+ @hashed_password = hashed_password.upcase
30
+ @request_options = Hash(request_options).dup
31
+ @request_headers = Hash(request_options.delete(:headers))
32
+ @request_headers = DEFAULT_REQUEST_HEADERS.merge(@request_headers)
33
+ @request_proxy = URI(request_options.delete(:proxy)) if request_options.key?(:proxy)
34
+ end
35
+ end
36
+ end
@@ -79,12 +79,12 @@ class NotPwnedValidator < ActiveModel::EachValidator
79
79
  begin
80
80
  pwned_check = Pwned::Password.new(value, request_options)
81
81
  if pwned_check.pwned_count > threshold
82
- record.errors.add(attribute, :not_pwned, options.merge(count: pwned_check.pwned_count))
82
+ record.errors.add(attribute, :not_pwned, **options.merge(count: pwned_check.pwned_count))
83
83
  end
84
84
  rescue Pwned::Error => error
85
85
  case on_error
86
86
  when :invalid
87
- record.errors.add(attribute, :pwned_error, options.merge(message: options[:error_message]))
87
+ record.errors.add(attribute, :pwned_error, **options.merge(message: options[:error_message]))
88
88
  when :valid
89
89
  # Do nothing, consider the record valid
90
90
  when Proc
@@ -129,4 +129,4 @@ end
129
129
  #
130
130
  # @since 1.1.0
131
131
  class PwnedValidator < NotPwnedValidator
132
- end
132
+ end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "digest"
4
- require "open-uri"
3
+ require "pwned/password_base"
5
4
 
6
5
  module Pwned
7
6
  ##
@@ -9,27 +8,7 @@ module Pwned
9
8
  # Pwned Passwords API to find out if the password has been pwned.
10
9
  # @see https://haveibeenpwned.com/API/v2#PwnedPasswords
11
10
  class Password
12
- ##
13
- # The base URL for the Pwned Passwords API
14
- API_URL = "https://api.pwnedpasswords.com/range/"
15
-
16
- ##
17
- # The number of characters from the start of the hash of the password that
18
- # are used to search for the range of passwords.
19
- HASH_PREFIX_LENGTH = 5
20
-
21
- ##
22
- # The total length of a SHA1 hash
23
- SHA1_LENGTH = 40
24
-
25
- ##
26
- # The default request options that are used to make HTTP requests to the
27
- # API. A user agent is provided as requested in the documentation.
28
- # @see https://haveibeenpwned.com/API/v2#UserAgent
29
- DEFAULT_REQUEST_OPTIONS = {
30
- "User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}"
31
- }.freeze
32
-
11
+ include PasswordBase
33
12
  ##
34
13
  # @return [String] the password that is being checked.
35
14
  # @since 1.0.0
@@ -40,88 +19,25 @@ module Pwned
40
19
  #
41
20
  # @example A simple password with the default request options
42
21
  # password = Pwned::Password.new("password")
43
- # @example Setting the user agent and the read timeout of the reques
44
- # password = Pwned::Password.new("password", "User-Agent" => "My user agent", :read_timout => 10)
22
+ # @example Setting the user agent and the read timeout of the request
23
+ # password = Pwned::Password.new("password", headers: { "User-Agent" => "My user agent" }, read_timout: 10)
45
24
  #
46
25
  # @param password [String] The password you want to check against the API.
47
- # @param [Hash] request_options Options that can be passed to +open+ when
26
+ # @param [Hash] request_options Options that can be passed to +Net::HTTP.start+ when
48
27
  # calling the API
49
- # @option request_options [String] 'User-Agent' ("Ruby Pwned::Password #{Pwned::VERSION}")
50
- # The user agent used when making an API request.
28
+ # @option request_options [Symbol] :headers ({ "User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}" })
29
+ # HTTP headers to include in the request
51
30
  # @return [Boolean] Whether the password appears in the data breaches or not.
52
31
  # @raise [TypeError] if the password is not a string.
53
32
  # @since 1.1.0
54
33
  def initialize(password, request_options={})
55
34
  raise TypeError, "password must be of type String" unless password.is_a? String
56
35
  @password = password
57
- @request_options = DEFAULT_REQUEST_OPTIONS.merge(request_options)
58
- end
59
-
60
- ##
61
- # Returns the full SHA1 hash of the given password in uppercase.
62
- # @return [String] The full SHA1 hash of the given password.
63
- # @since 1.0.0
64
- def hashed_password
65
- @hashed_password ||= Digest::SHA1.hexdigest(password).upcase
66
- end
67
-
68
- ##
69
- # @example
70
- # password = Pwned::Password.new("password")
71
- # password.pwned? #=> true
72
- #
73
- # @return [Boolean] +true+ when the password has been pwned.
74
- # @raise [Pwned::Error] if there are errors with the HTTP request.
75
- # @raise [Pwned::TimeoutError] if the HTTP request times out.
76
- # @since 1.0.0
77
- def pwned?
78
- pwned_count > 0
79
- end
80
-
81
- ##
82
- # @example
83
- # password = Pwned::Password.new("password")
84
- # password.pwned_count #=> 3303003
85
- #
86
- # @return [Integer] the number of times the password has been pwned.
87
- # @raise [Pwned::Error] if there are errors with the HTTP request.
88
- # @raise [Pwned::TimeoutError] if the HTTP request times out.
89
- # @since 1.0.0
90
- def pwned_count
91
- @pwned_count ||= fetch_pwned_count
92
- end
93
-
94
- private
95
-
96
- def fetch_pwned_count
97
- for_each_response_line do |line|
98
- next unless line.start_with?(hashed_password_suffix)
99
- # Count starts after the suffix, followed by a colon
100
- return line[(SHA1_LENGTH-HASH_PREFIX_LENGTH+1)..-1].to_i
101
- end
102
-
103
- # The hash was not found, we can assume the password is not pwned [yet]
104
- 0
105
- end
106
-
107
- def for_each_response_line(&block)
108
- begin
109
- open("#{API_URL}#{hashed_password_prefix}", @request_options) do |io|
110
- io.each_line(&block)
111
- end
112
- rescue Timeout::Error => e
113
- raise Pwned::TimeoutError, e.message
114
- rescue => e
115
- raise Pwned::Error, e.message
116
- end
117
- end
118
-
119
- def hashed_password_prefix
120
- hashed_password[0...HASH_PREFIX_LENGTH]
121
- end
122
-
123
- def hashed_password_suffix
124
- hashed_password[HASH_PREFIX_LENGTH..-1]
36
+ @hashed_password = Pwned.hash_password(password)
37
+ @request_options = Hash(request_options).dup
38
+ @request_headers = Hash(request_options.delete(:headers))
39
+ @request_headers = DEFAULT_REQUEST_HEADERS.merge(@request_headers)
40
+ @request_proxy = URI(request_options.delete(:proxy)) if request_options.key?(:proxy)
125
41
  end
126
42
  end
127
43
  end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "net/http"
5
+
6
+ module Pwned
7
+ ##
8
+ # This class represents a password. It does all the work of talking to the
9
+ # Pwned Passwords API to find out if the password has been pwned.
10
+ # @see https://haveibeenpwned.com/API/v2#PwnedPasswords
11
+ module PasswordBase
12
+ ##
13
+ # The base URL for the Pwned Passwords API
14
+ API_URL = "https://api.pwnedpasswords.com/range/"
15
+
16
+ ##
17
+ # The number of characters from the start of the hash of the password that
18
+ # are used to search for the range of passwords.
19
+ HASH_PREFIX_LENGTH = 5
20
+
21
+ ##
22
+ # The total length of a SHA1 hash
23
+ SHA1_LENGTH = 40
24
+
25
+ ##
26
+ # The default request headers that are used to make HTTP requests to the
27
+ # API. A user agent is provided as requested in the documentation.
28
+ # @see https://haveibeenpwned.com/API/v2#UserAgent
29
+ DEFAULT_REQUEST_HEADERS = {
30
+ "User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}"
31
+ }.freeze
32
+
33
+ ##
34
+ # @example
35
+ # password = Pwned::Password.new("password")
36
+ # password.pwned? #=> true
37
+ # password.pwned? #=> true
38
+ #
39
+ # @return [Boolean] +true+ when the password has been pwned.
40
+ # @raise [Pwned::Error] if there are errors with the HTTP request.
41
+ # @raise [Pwned::TimeoutError] if the HTTP request times out.
42
+ # @since 1.0.0
43
+ def pwned?
44
+ pwned_count > 0
45
+ end
46
+
47
+ ##
48
+ # @example
49
+ # password = Pwned::Password.new("password")
50
+ # password.pwned_count #=> 3303003
51
+ #
52
+ # @return [Integer] the number of times the password has been pwned.
53
+ # @raise [Pwned::Error] if there are errors with the HTTP request.
54
+ # @raise [Pwned::TimeoutError] if the HTTP request times out.
55
+ # @since 1.0.0
56
+ def pwned_count
57
+ @pwned_count ||= fetch_pwned_count
58
+ end
59
+
60
+ ##
61
+ # Returns the full SHA1 hash of the given password in uppercase.
62
+ # @return [String] The full SHA1 hash of the given password.
63
+ # @since 1.0.0
64
+ attr_reader :hashed_password
65
+
66
+ private
67
+
68
+ attr_reader :request_options, :request_headers, :request_proxy
69
+
70
+ def fetch_pwned_count
71
+ for_each_response_line do |line|
72
+ next unless line.start_with?(hashed_password_suffix)
73
+ # Count starts after the suffix, followed by a colon
74
+ return line[(SHA1_LENGTH-HASH_PREFIX_LENGTH+1)..-1].to_i
75
+ end
76
+
77
+ # The hash was not found, we can assume the password is not pwned [yet]
78
+ 0
79
+ end
80
+
81
+ def for_each_response_line(&block)
82
+ begin
83
+ with_http_response "#{API_URL}#{hashed_password_prefix}" do |response|
84
+ response.value # raise if request was unsuccessful
85
+ stream_response_lines(response, &block)
86
+ end
87
+ rescue Timeout::Error => e
88
+ raise Pwned::TimeoutError, e.message
89
+ rescue => e
90
+ raise Pwned::Error, e.message
91
+ end
92
+ end
93
+
94
+ def hashed_password_prefix
95
+ @hashed_password[0...HASH_PREFIX_LENGTH]
96
+ end
97
+
98
+ def hashed_password_suffix
99
+ @hashed_password[HASH_PREFIX_LENGTH..-1]
100
+ end
101
+
102
+ # Make a HTTP GET request given the url and headers.
103
+ # Yields a `Net::HTTPResponse`.
104
+ def with_http_response(url, &block)
105
+ uri = URI(url)
106
+
107
+ request = Net::HTTP::Get.new(uri)
108
+ request.initialize_http_header(request_headers)
109
+ request_options[:use_ssl] = true
110
+
111
+ Net::HTTP.start(
112
+ uri.host,
113
+ uri.port,
114
+ request_proxy&.host,
115
+ request_proxy&.port,
116
+ request_proxy&.user,
117
+ request_proxy&.password,
118
+ request_options
119
+ ) do |http|
120
+ http.request(request, &block)
121
+ end
122
+ end
123
+
124
+ # Stream a Net::HTTPResponse by line, handling lines that cross chunks.
125
+ def stream_response_lines(response, &block)
126
+ last_line = ""
127
+
128
+ response.read_body do |chunk|
129
+ chunk_lines = (last_line + chunk).lines
130
+ # This could end with half a line, so save it for next time. If
131
+ # chunk_lines is empty, pop returns nil, so this also ensures last_line
132
+ # is always a string.
133
+ last_line = chunk_lines.pop || ""
134
+ chunk_lines.each(&block)
135
+ end
136
+
137
+ yield last_line unless last_line.empty?
138
+ end
139
+
140
+ end
141
+ end
data/lib/pwned/version.rb CHANGED
@@ -3,5 +3,5 @@
3
3
  module Pwned
4
4
  ##
5
5
  # The current version of the +pwned+ gem.
6
- VERSION = "1.2.1"
6
+ VERSION = "2.2.0"
7
7
  end
data/pwned.gemspec CHANGED
@@ -13,13 +13,22 @@ Gem::Specification.new do |spec|
13
13
  spec.homepage = "https://github.com/philnash/pwned"
14
14
  spec.license = "MIT"
15
15
 
16
+ spec.metadata = {
17
+ "bug_tracker_uri" => "https://github.com/philnash/pwned/issues",
18
+ "change_log_uri" => "https://github.com/philnash/pwned/blob/master/CHANGELOG.md",
19
+ "documentation_uri" => "https://www.rubydoc.info/gems/pwned",
20
+ "homepage_uri" => "https://github.com/philnash/pwned",
21
+ "source_code_uri" => "https://github.com/philnash/pwned"
22
+ }
23
+
16
24
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
25
  f.match(%r{^(test|spec|features)/})
18
26
  end
19
27
  spec.require_paths = ["lib"]
28
+ spec.executables = ["pwned"]
20
29
 
21
- spec.add_development_dependency "bundler", "~> 1.16"
22
- spec.add_development_dependency "rake", "~> 10.0"
30
+ spec.add_development_dependency "bundler", ">= 1.16", "< 3.0"
31
+ spec.add_development_dependency "rake", "~> 13.0"
23
32
  spec.add_development_dependency "rspec", "~> 3.0"
24
33
  spec.add_development_dependency "webmock", "~> 3.3"
25
34
  spec.add_development_dependency "yard", "~> 0.9.12"
metadata CHANGED
@@ -1,43 +1,49 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pwned
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Phil Nash
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-03-16 00:00:00.000000000 Z
11
+ date: 2021-03-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '1.16'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '3.0'
20
23
  type: :development
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
- - - "~>"
27
+ - - ">="
25
28
  - !ruby/object:Gem::Version
26
29
  version: '1.16'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.0'
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: rake
29
35
  requirement: !ruby/object:Gem::Requirement
30
36
  requirements:
31
37
  - - "~>"
32
38
  - !ruby/object:Gem::Version
33
- version: '10.0'
39
+ version: '13.0'
34
40
  type: :development
35
41
  prerelease: false
36
42
  version_requirements: !ruby/object:Gem::Requirement
37
43
  requirements:
38
44
  - - "~>"
39
45
  - !ruby/object:Gem::Version
40
- version: '10.0'
46
+ version: '13.0'
41
47
  - !ruby/object:Gem::Dependency
42
48
  name: rspec
43
49
  requirement: !ruby/object:Gem::Requirement
@@ -83,13 +89,15 @@ dependencies:
83
89
  description: Tools to use the Pwned Passwords API.
84
90
  email:
85
91
  - philnash@gmail.com
86
- executables: []
92
+ executables:
93
+ - pwned
87
94
  extensions: []
88
95
  extra_rdoc_files: []
89
96
  files:
97
+ - ".github/FUNDING.yml"
98
+ - ".github/workflows/tests.yml"
90
99
  - ".gitignore"
91
100
  - ".rspec"
92
- - ".travis.yml"
93
101
  - ".yardopts"
94
102
  - CHANGELOG.md
95
103
  - CODE_OF_CONDUCT.md
@@ -98,39 +106,27 @@ files:
98
106
  - README.md
99
107
  - Rakefile
100
108
  - bin/console
109
+ - bin/pwned
101
110
  - bin/setup
102
- - docs/NotPwnedValidator.html
103
- - docs/Pwned.html
104
- - docs/Pwned/Error.html
105
- - docs/Pwned/Password.html
106
- - docs/Pwned/TimeoutError.html
107
- - docs/PwnedValidator.html
108
- - docs/_index.html
109
- - docs/class_list.html
110
- - docs/css/common.css
111
- - docs/css/full_list.css
112
- - docs/css/style.css
113
- - docs/file.README.html
114
- - docs/file_list.html
115
- - docs/frames.html
116
- - docs/index.html
117
- - docs/js/app.js
118
- - docs/js/full_list.js
119
- - docs/js/jquery.js
120
- - docs/method_list.html
121
- - docs/top-level-namespace.html
122
111
  - lib/locale/en.yml
123
112
  - lib/pwned.rb
124
113
  - lib/pwned/error.rb
114
+ - lib/pwned/hashed_password.rb
125
115
  - lib/pwned/not_pwned_validator.rb
126
116
  - lib/pwned/password.rb
117
+ - lib/pwned/password_base.rb
127
118
  - lib/pwned/version.rb
128
119
  - pwned.gemspec
129
120
  homepage: https://github.com/philnash/pwned
130
121
  licenses:
131
122
  - MIT
132
- metadata: {}
133
- post_install_message:
123
+ metadata:
124
+ bug_tracker_uri: https://github.com/philnash/pwned/issues
125
+ change_log_uri: https://github.com/philnash/pwned/blob/master/CHANGELOG.md
126
+ documentation_uri: https://www.rubydoc.info/gems/pwned
127
+ homepage_uri: https://github.com/philnash/pwned
128
+ source_code_uri: https://github.com/philnash/pwned
129
+ post_install_message:
134
130
  rdoc_options: []
135
131
  require_paths:
136
132
  - lib
@@ -145,9 +141,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
145
141
  - !ruby/object:Gem::Version
146
142
  version: '0'
147
143
  requirements: []
148
- rubyforge_project:
149
- rubygems_version: 2.7.6
150
- signing_key:
144
+ rubygems_version: 3.2.3
145
+ signing_key:
151
146
  specification_version: 4
152
147
  summary: Tools to use the Pwned Passwords API.
153
148
  test_files: []