pwned 2.0.1 → 2.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b7e2d94dbe8ca81e5e0d8024cb614d2039a4b8cbd835a805131a9d98c8215327
4
- data.tar.gz: 73d6a46ff19e904ba94590de3526709183e43956152d048352263c0b4a0246c3
3
+ metadata.gz: f4b8270eaf162b50ef112371c2e35dd41141dec39e4e11d5a76936119e2ca569
4
+ data.tar.gz: fdec9b67cc6465fa64062697253e6cf078ddb2b2deb71cf829a919dca7953f48
5
5
  SHA512:
6
- metadata.gz: 9fc9f07b450fddcc3dff391640bbce98589de8779254d470413111d5198b50ace2c71264f7dd6f81c06caec4262a7be360bb5b6e2dd9699262be72fa0905726f
7
- data.tar.gz: f03aec89ab299234ef7823097c4ffd943576019381249aaf097c38e74651ad5b4da1a66136320257729a9442b8a6c68a516b71fae3c2c12c9431ac56c90162c5
6
+ metadata.gz: 7ec757852674e3e44ac71a71ed5c31d3503b5f0547871f940d56e8e2d8838b0b89e36742c0e10108f1c00fddc54016454d3ba77348cddd7be659d1ed4fdaf71a
7
+ data.tar.gz: 304b59ce60639f57c7a3a81c5e0f172dc8de9a2128018abe636be7f6d88074af59ac193d2538f86c298d4f8a537b0152bdcab6efb8cf4a27d5c6a13c64b9e311
@@ -0,0 +1 @@
1
+ github: philnash
@@ -0,0 +1,39 @@
1
+ name: tests
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ test:
7
+ runs-on: ubuntu-latest
8
+ strategy:
9
+ fail-fast: false
10
+ matrix:
11
+ ruby: [2.5, 2.6, 2.7, 3.0, head]
12
+ rails: [4.2.11.3, 5.0.7.2, 5.1.7, 5.2.4.4, 6.0.3.4, 6.1.0]
13
+ exclude:
14
+ # Ruby 3.0 and Rails 5 do not get along together.
15
+ - ruby: 3.0
16
+ rails: 5.0.7.2
17
+ - ruby: 3.0
18
+ rails: 5.1.7
19
+ - ruby: 3.0
20
+ rails: 5.2.4.4
21
+ - ruby: head
22
+ rails: 5.0.7.2
23
+ - ruby: head
24
+ rails: 5.1.7
25
+ - ruby: head
26
+ rails: 5.2.4.4
27
+ continue-on-error: ${{ endsWith(matrix.ruby, 'head') }}
28
+ env:
29
+ RAILS_VERSION: ${{ matrix.rails }}
30
+ steps:
31
+ - uses: actions/checkout@v2
32
+ - name: Set up Ruby ${{ matrix.ruby }}
33
+ uses: ruby/setup-ruby@v1
34
+ with:
35
+ ruby-version: ${{ matrix.ruby }}
36
+ - name: "Install dependencies (rails: ${{matrix.rails}})"
37
+ run: bundle install
38
+ - name: Run tests
39
+ run: bundle exec rspec
data/CHANGELOG.md CHANGED
@@ -1,8 +1,39 @@
1
1
  # Changelog for `Pwned`
2
2
 
3
- ## Ongoing [☰](https://github.com/philnash/pwned/compare/v2.0.1...master)
3
+ ## Ongoing [☰](https://github.com/philnash/pwned/compare/v2.2.0...master)
4
4
 
5
- ## 2.0.1 (January 14, 2019) [☰](https://github.com/philnash/pwned/compare/v2.0.0...v2.0.1)
5
+ ## 2.3.0 (August 30, 2021) [☰](https://github.com/philnash/pwned/compare/v2.2.0...v2.3.0)
6
+
7
+ - Minor updates
8
+
9
+ - Restores `Net::HTTP` default behaviour to use environment supplied HTTP
10
+ proxy
11
+ - Adds `ignore_env_proxy` to ignore any proxies set in the environment
12
+
13
+ ## 2.2.0 (March 27, 2021) [☰](https://github.com/philnash/pwned/compare/v2.1.0...v2.2.0)
14
+
15
+ - Minor updates
16
+
17
+ - Adds `:proxy` option to `request_options` to directly set a proxy on the
18
+ request. Fixes #21, thanks [dparpyani](https://github.com/dparpyani).
19
+
20
+ ## 2.1.0 (July 8, 2020) [☰](https://github.com/philnash/pwned/compare/v2.0.2...v2.1.0)
21
+
22
+ - Minor updates
23
+
24
+ - Adds `Pwned::HashedPassword` class which is initializd with a SHA1 hash to
25
+ query the API with so that the lookup can be done in the background without
26
+ storing passwords. Fixes #19, thanks [@paprikati](https://github.com/paprikati).
27
+
28
+ ## 2.0.2 (May 20, 2020) [☰](https://github.com/philnash/pwned/compare/v2.0.1...v2.0.2)
29
+
30
+ - Minor fix
31
+
32
+ - It was found to be possible for reading the lines body of a response to
33
+ result in a `nil` which caused trouble with string concatenation. This
34
+ avoids that scenario. Fixes #18, thanks [@flori](https://github.com/flori).
35
+
36
+ ## 2.0.1 (January 14, 2020) [☰](https://github.com/philnash/pwned/compare/v2.0.0...v2.0.1)
6
37
 
7
38
  - Minor updates
8
39
 
data/README.md CHANGED
@@ -2,19 +2,32 @@
2
2
 
3
3
  An easy, Ruby way to use the Pwned Passwords API.
4
4
 
5
- [![Gem Version](https://badge.fury.io/rb/pwned.svg)](https://rubygems.org/gems/pwned) [![Build Status](https://travis-ci.org/philnash/pwned.svg?branch=master)](https://travis-ci.org/philnash/pwned) [![Maintainability](https://codeclimate.com/github/philnash/pwned/badges/gpa.svg)](https://codeclimate.com/github/philnash/pwned/maintainability) [![Inline docs](https://inch-ci.org/github/philnash/pwned.svg?branch=master)](https://inch-ci.org/github/philnash/pwned)
5
+ [![Gem Version](https://badge.fury.io/rb/pwned.svg)](https://rubygems.org/gems/pwned) ![Build Status](https://github.com/philnash/pwned/workflows/tests/badge.svg) [![Maintainability](https://codeclimate.com/github/philnash/pwned/badges/gpa.svg)](https://codeclimate.com/github/philnash/pwned/maintainability) [![Inline docs](https://inch-ci.org/github/philnash/pwned.svg?branch=master)](https://inch-ci.org/github/philnash/pwned)
6
6
 
7
- [API docs](https://philnash.github.io/pwned/) | [GitHub repo](https://github.com/philnash/pwned)
7
+ [API docs](https://www.rubydoc.info/gems/pwned) | [GitHub repo](https://github.com/philnash/pwned)
8
8
 
9
9
  ## Table of Contents
10
10
 
11
+ * [Table of Contents](#table-of-contents)
11
12
  * [About](#about)
12
13
  * [Installation](#installation)
13
14
  * [Usage](#usage)
14
15
  * [Plain Ruby](#plain-ruby)
15
- * [Rails (ActiveRecord)](#activerecord-validator)
16
+ * [Custom request options](#custom-request-options)
17
+ * [HTTP Headers](#http-headers)
18
+ * [HTTP Proxy](#http-proxy)
19
+ * [ActiveRecord Validator](#activerecord-validator)
20
+ * [I18n](#i18n)
21
+ * [Threshold](#threshold)
22
+ * [Network Error Handling](#network-error-handling)
23
+ * [Custom Request Options](#custom-request-options-1)
24
+ * [HTTP Headers](#http-headers-1)
25
+ * [HTTP Proxy](#http-proxy-1)
26
+ * [Using Asynchronously](#using-asynchronously)
16
27
  * [Devise](#devise)
28
+ * [Rodauth](#rodauth)
17
29
  * [Command line](#command-line)
30
+ * [Unpwn](#unpwn)
18
31
  * [How Pwned is Pi?](#how-pwned-is-pi)
19
32
  * [Development](#development)
20
33
  * [Contributing](#contributing)
@@ -95,13 +108,49 @@ Pwned.pwned_count("password")
95
108
  #=> 3303003
96
109
  ```
97
110
 
98
- #### Advanced
111
+ #### Custom request options
99
112
 
100
- You can set http request options to be used with `Net::HTTP.start` when making the request to the API. These options are
101
- documented in the [`Net::HTTP.start` documentation](http://ruby-doc.org/stdlib-2.6.3/libdoc/net/http/rdoc/Net/HTTP.html#method-c-start). The `:headers` option defines defines HTTP headers. These headers must be string keys.
113
+ You can set http request options to be used with `Net::HTTP.start` when making the request to the API. These options are documented in the [`Net::HTTP.start` documentation](https://ruby-doc.org/stdlib-3.0.0/libdoc/net/http/rdoc/Net/HTTP.html#method-c-start). For example:
102
114
 
103
115
  ```ruby
104
- password = Pwned::Password.new("password", headers: { 'User-Agent' => 'Super fun new user agent' }, read_timeout: 10)
116
+ password = Pwned::Password.new("password", read_timeout: 10)
117
+ ```
118
+
119
+ ##### HTTP Headers
120
+
121
+ The `:headers` option defines defines HTTP headers. These headers must be string keys.
122
+
123
+ ```ruby
124
+ password = Pwned::Password.new("password", headers: {
125
+ 'User-Agent' => 'Super fun new user agent'
126
+ })
127
+ ```
128
+
129
+ ##### HTTP Proxy
130
+
131
+ An HTTP proxy can be set using the `http_proxy` or `HTTP_PROXY` environment variable. This is the same way that `Net::HTTP` handles HTTP proxies if no proxy options are given. See [`URI::Generic#find_proxy`](https://ruby-doc.org/stdlib-3.0.1/libdoc/uri/rdoc/URI/Generic.html#method-i-find_proxy) for full details on how Ruby detects a proxy from the environment.
132
+
133
+ ```ruby
134
+ # Set in the environment
135
+ ENV["http_proxy"] = "https://username:password@example.com:12345"
136
+
137
+ # Will use the above proxy
138
+ password = Pwned::Password.new("password")
139
+ ```
140
+
141
+ You can specify a custom HTTP proxy with the `:proxy` option:
142
+
143
+ ```ruby
144
+ password = Pwned::Password.new(
145
+ "password",
146
+ proxy: "https://username:password@example.com:12345"
147
+ )
148
+ ```
149
+
150
+ If you don't want to set a proxy and you don't want a proxy to be inferred from the environment, set the `:ignore_env_proxy` key:
151
+
152
+ ```ruby
153
+ password = Pwned::Password.new("password", ignore_env_proxy: true)
105
154
  ```
106
155
 
107
156
  ### ActiveRecord Validator
@@ -171,18 +220,81 @@ end
171
220
 
172
221
  #### Custom Request Options
173
222
 
174
- You can configure network requests made from the validator using `:request_options` (see [Net::HTTP.start](http://ruby-doc.org/stdlib-2.6.3/libdoc/net/http/rdoc/Net/HTTP.html#method-c-start) for the list of available options).
175
- In addition to these options, HTTP headers can be specified with the `:headers` key, e.g. `"User-Agent"`):
223
+ You can configure network requests made from the validator using `:request_options` (see [Net::HTTP.start](http://ruby-doc.org/stdlib-2.6.3/libdoc/net/http/rdoc/Net/HTTP.html#method-c-start) for the list of available options).
224
+
225
+ ```ruby
226
+ validates :password, not_pwned: {
227
+ request_options: {
228
+ read_timeout: 5,
229
+ open_timeout: 1
230
+ }
231
+ }
232
+ ```
233
+
234
+ In addition to these options, you can also set the following:
235
+
236
+ ##### HTTP Headers
237
+
238
+ HTTP headers can be specified with the `:headers` key (e.g. `"User-Agent"`)
176
239
 
177
240
  ```ruby
178
241
  validates :password, not_pwned: {
179
- request_options: { read_timeout: 5, open_timeout: 1, headers: { "User-Agent" => "Super fun user agent" } }
242
+ request_options: {
243
+ headers: { "User-Agent" => "Super fun user agent" }
244
+ }
180
245
  }
181
246
  ```
182
247
 
248
+ ##### HTTP Proxy
249
+
250
+ An HTTP proxy can be set using the `http_proxy` or `HTTP_PROXY` environment variable. This is the same way that `Net::HTTP` handles HTTP proxies if no proxy options are given. See [`URI::Generic#find_proxy`](https://ruby-doc.org/stdlib-3.0.1/libdoc/uri/rdoc/URI/Generic.html#method-i-find_proxy) for full details on how Ruby detects a proxy from the environment.
251
+
252
+ ```ruby
253
+ # Set in the environment
254
+ ENV["http_proxy"] = "https://username:password@example.com:12345"
255
+
256
+ validates :password, not_pwned: true
257
+ ```
258
+
259
+ You can specify a custom HTTP proxy with the `:proxy` key:
260
+
261
+ ```ruby
262
+ validates :password, not_pwned: {
263
+ request_options: {
264
+ proxy: "https://username:password@example.com:12345"
265
+ }
266
+ }
267
+ ```
268
+
269
+ If you don't want to set a proxy and you don't want a proxy to be inferred from the environment, set the `:ignore_env_proxy` key:
270
+
271
+ ```ruby
272
+ validates :password, not_pwned: {
273
+ request_options: {
274
+ ignore_env_proxy: true
275
+ }
276
+ }
277
+ ```
278
+
279
+ ### Using Asynchronously
280
+
281
+ You may have a use case for hashing the password in advance, and then making the call to the Pwned Passwords API later (for example if you want to enqueue a job without storing the plaintext password). To do this, you can hash the password with the `Pwned.hash_password` method and then initialize the `Pwned::HashPassword` class with the hash, like this:
282
+
283
+ ```ruby
284
+ hashed_password = Pwned.hash_password(password)
285
+ # some time later
286
+ Pwned::HashPassword.new(hashed_password, request_options).pwned?
287
+ ```
288
+
289
+ The `Pwned::HashPassword` constructor takes all the same options as the regular `Pwned::Password` contructor.
290
+
183
291
  ### Devise
184
292
 
185
- If you are using Devise I recommend you use the [devise-pwned_password extension](https://github.com/michaelbanfield/devise-pwned_password) which is now powered by this gem.
293
+ If you are using [Devise](https://github.com/heartcombo/devise) I recommend you use the [devise-pwned_password extension](https://github.com/michaelbanfield/devise-pwned_password) which is now powered by this gem.
294
+
295
+ ### Rodauth
296
+
297
+ If you are using [Rodauth](https://github.com/jeremyevans/rodauth) then you can use the [rodauth-pwned](https://github.com/janko/rodauth-pwned) feature which is powered by this gem.
186
298
 
187
299
  ### Command line
188
300
 
@@ -202,6 +314,10 @@ $ pwned --secret
202
314
 
203
315
  You will be prompted for the password, but it won't be displayed.
204
316
 
317
+ ### Unpwn
318
+
319
+ To cut down on unnecessary network requests, [the unpwn project](https://github.com/indirect/unpwn) uses a list of the top one million passwords to check passwords against. Only if a password is not included in the top million is it then checked against the Pwned Passwords API.
320
+
205
321
  ## How Pwned is Pi?
206
322
 
207
323
  [@daz](https://github.com/daz) [shared](https://twitter.com/dazonic/status/1074647842046660609) a fantastic example of using this gem to show how many times the digits of Pi have been used as passwords and leaked.
data/bin/pwned CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'pwned'
4
- require 'optparse'
5
- require 'io/console'
3
+ require "pwned"
4
+ require "optparse"
5
+ require "io/console"
6
6
 
7
7
  options = {}
8
8
  parser = OptionParser.new do |opts|
@@ -0,0 +1,40 @@
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
+ # @option request_options [Symbol] :ignore_env_proxy (false) The library
26
+ # will try to infer an HTTP proxy from the `http_proxy` environment
27
+ # variable. If you do not want this behaviour, set this option to true.
28
+ # @raise [TypeError] if the password is not a string.
29
+ # @since 2.1.0
30
+ def initialize(hashed_password, request_options={})
31
+ raise TypeError, "hashed_password must be of type String" unless hashed_password.is_a? String
32
+ @hashed_password = hashed_password.upcase
33
+ @request_options = Hash(request_options).dup
34
+ @request_headers = Hash(request_options.delete(:headers))
35
+ @request_headers = DEFAULT_REQUEST_HEADERS.merge(@request_headers)
36
+ @request_proxy = URI(request_options.delete(:proxy)) if request_options.key?(:proxy)
37
+ @ignore_env_proxy = request_options.delete(:ignore_env_proxy) || false
38
+ end
39
+ end
40
+ end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "digest"
4
- require 'net/http'
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 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
-
11
+ include PasswordBase
33
12
  ##
34
13
  # @return [String] the password that is being checked.
35
14
  # @since 1.0.0
@@ -46,115 +25,22 @@ module Pwned
46
25
  # @param password [String] The password you want to check against the API.
47
26
  # @param [Hash] request_options Options that can be passed to +Net::HTTP.start+ when
48
27
  # calling the API
49
- # @option request_options [Symbol] :headers ({ "User-Agent" => '"Ruby Pwned::Password #{Pwned::VERSION}" })
28
+ # @option request_options [Symbol] :headers ({ "User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}" })
50
29
  # HTTP headers to include in the request
51
- # @return [Boolean] Whether the password appears in the data breaches or not.
30
+ # @option request_options [Symbol] :ignore_env_proxy (false) The library
31
+ # will try to infer an HTTP proxy from the `http_proxy` environment
32
+ # variable. If you do not want this behaviour, set this option to true.
52
33
  # @raise [TypeError] if the password is not a string.
53
34
  # @since 1.1.0
54
35
  def initialize(password, request_options={})
55
36
  raise TypeError, "password must be of type String" unless password.is_a? String
56
37
  @password = password
38
+ @hashed_password = Pwned.hash_password(password)
57
39
  @request_options = Hash(request_options).dup
58
40
  @request_headers = Hash(request_options.delete(:headers))
59
41
  @request_headers = DEFAULT_REQUEST_HEADERS.merge(@request_headers)
42
+ @request_proxy = URI(request_options.delete(:proxy)) if request_options.key?(:proxy)
43
+ @ignore_env_proxy = request_options.delete(:ignore_env_proxy) || false
60
44
  end
61
-
62
- ##
63
- # Returns the full SHA1 hash of the given password in uppercase.
64
- # @return [String] The full SHA1 hash of the given password.
65
- # @since 1.0.0
66
- def hashed_password
67
- @hashed_password ||= Digest::SHA1.hexdigest(password).upcase
68
- end
69
-
70
- ##
71
- # @example
72
- # password = Pwned::Password.new("password")
73
- # password.pwned? #=> true
74
- #
75
- # @return [Boolean] +true+ when the password has been pwned.
76
- # @raise [Pwned::Error] if there are errors with the HTTP request.
77
- # @raise [Pwned::TimeoutError] if the HTTP request times out.
78
- # @since 1.0.0
79
- def pwned?
80
- pwned_count > 0
81
- end
82
-
83
- ##
84
- # @example
85
- # password = Pwned::Password.new("password")
86
- # password.pwned_count #=> 3303003
87
- #
88
- # @return [Integer] the number of times the password has been pwned.
89
- # @raise [Pwned::Error] if there are errors with the HTTP request.
90
- # @raise [Pwned::TimeoutError] if the HTTP request times out.
91
- # @since 1.0.0
92
- def pwned_count
93
- @pwned_count ||= fetch_pwned_count
94
- end
95
-
96
- private
97
-
98
- attr_reader :request_options, :request_headers
99
-
100
- def fetch_pwned_count
101
- for_each_response_line do |line|
102
- next unless line.start_with?(hashed_password_suffix)
103
- # Count starts after the suffix, followed by a colon
104
- return line[(SHA1_LENGTH-HASH_PREFIX_LENGTH+1)..-1].to_i
105
- end
106
-
107
- # The hash was not found, we can assume the password is not pwned [yet]
108
- 0
109
- end
110
-
111
- def for_each_response_line(&block)
112
- begin
113
- with_http_response "#{API_URL}#{hashed_password_prefix}" do |response|
114
- response.value # raise if request was unsuccessful
115
- stream_response_lines(response, &block)
116
- end
117
- rescue Timeout::Error => e
118
- raise Pwned::TimeoutError, e.message
119
- rescue => e
120
- raise Pwned::Error, e.message
121
- end
122
- end
123
-
124
- def hashed_password_prefix
125
- hashed_password[0...HASH_PREFIX_LENGTH]
126
- end
127
-
128
- def hashed_password_suffix
129
- hashed_password[HASH_PREFIX_LENGTH..-1]
130
- end
131
-
132
- # Make a HTTP GET request given the url and headers.
133
- # Yields a `Net::HTTPResponse`.
134
- def with_http_response(url, &block)
135
- uri = URI(url)
136
-
137
- request = Net::HTTP::Get.new(uri)
138
- request.initialize_http_header(request_headers)
139
- request_options[:use_ssl] = true
140
-
141
- Net::HTTP.start(uri.host, uri.port, request_options) do |http|
142
- http.request(request, &block)
143
- end
144
- end
145
-
146
- # Stream a Net::HTTPResponse by line, handling lines that cross chunks.
147
- def stream_response_lines(response, &block)
148
- last_line = ''
149
-
150
- response.read_body do |chunk|
151
- chunk_lines = (last_line + chunk).lines
152
- # This could end with half a line, so save it for next time
153
- last_line = chunk_lines.pop
154
- chunk_lines.each(&block)
155
- end
156
- yield last_line
157
- end
158
-
159
45
  end
160
46
  end