pwned 2.0.1 → 2.3.0

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