pwned 1.2.1 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []