pwned 2.0.2 → 2.1.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: 1a4cfc835ea737e4c8f06f0abc5681f36321d94cb0224a1628ba3bbc46276d68
4
- data.tar.gz: 2c48c27b0e9308431fe7c47d00d74318e55f2bff60456d8222fc108b7915318c
3
+ metadata.gz: 4661790082f543ba897baf211da660c7a4f654444121f4ff3ba08542c08c412b
4
+ data.tar.gz: f52a3f3cf36d461e8704a632f97829a5d9f871d9916a55687d4a0b2156b44b75
5
5
  SHA512:
6
- metadata.gz: 5aaa679b1268b7e2eaa17bebab180a071c286080965602cd50d0e2c1f5b6b92b58c970fbf12e5678c10d4570907632b4346a345ea904820370505f205b7d51d6
7
- data.tar.gz: cf95e423abffe290bf3de0bfe6c1dff7da73eb8c6c0b574ca97ab63ad82d27a417caa35136cccf3dd4407437b8e00f77421144ad50487b5d8a8502438fa660cb
6
+ metadata.gz: c114c3ca6e7667d1760ad2ae5dabcc7bf8d14b91e42788f7e36bba716eecd9bef6e1847e93dd12df4f8afed19460d26a068dc22ffb2270ceef8dc342f81690e0
7
+ data.tar.gz: c19d20d765cd57e64468c27a3e8f134e53d8f6e9ae22497c2d94a315a584e2e19b1913d47b37e89c9525ce80d85d10d43437a623d211766aa7242dbe1144e906
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## Ongoing [☰](https://github.com/philnash/pwned/compare/v2.0.2...master)
4
4
 
5
+ ## 2.1.0 (July 8, 2020) [☰](https://github.com/philnash/pwned/compare/v2.0.2...v2.1.0)
6
+
7
+ - Minor updates
8
+
9
+ - Adds `Pwned::HashedPassword` class which is initializd with a SHA1 hash to
10
+ query the API with so that the lookup can be done in the background without
11
+ storing passwords. Fixes #19, thanks [@paprikati](https://github.com/paprikati).
12
+
5
13
  ## 2.0.2 (May 20, 2020) [☰](https://github.com/philnash/pwned/compare/v2.0.1...v2.0.2)
6
14
 
7
15
  - Minor fix
@@ -10,7 +18,7 @@
10
18
  result in a `nil` which caused trouble with string concatenation. This
11
19
  avoids that scenario. Fixes #18, thanks [@flori](https://github.com/flori).
12
20
 
13
- ## 2.0.1 (January 14, 2019) [☰](https://github.com/philnash/pwned/compare/v2.0.0...v2.0.1)
21
+ ## 2.0.1 (January 14, 2020) [☰](https://github.com/philnash/pwned/compare/v2.0.0...v2.0.1)
14
22
 
15
23
  - Minor updates
16
24
 
data/README.md CHANGED
@@ -4,22 +4,30 @@ An easy, Ruby way to use the Pwned Passwords API.
4
4
 
5
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)
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
- * [About](#about)
12
- * [Installation](#installation)
13
- * [Usage](#usage)
14
- * [Plain Ruby](#plain-ruby)
15
- * [Rails (ActiveRecord)](#activerecord-validator)
16
- * [Devise](#devise)
17
- * [Command line](#command-line)
18
- * [How Pwned is Pi?](#how-pwned-is-pi)
19
- * [Development](#development)
20
- * [Contributing](#contributing)
21
- * [License](#license)
22
- * [Code of Conduct](#code-of-conduct)
11
+ - [Pwned](#pwned)
12
+ - [Table of Contents](#table-of-contents)
13
+ - [About](#about)
14
+ - [Installation](#installation)
15
+ - [Usage](#usage)
16
+ - [Plain Ruby](#plain-ruby)
17
+ - [Advanced](#advanced)
18
+ - [ActiveRecord Validator](#activerecord-validator)
19
+ - [I18n](#i18n)
20
+ - [Threshold](#threshold)
21
+ - [Network Error Handling](#network-error-handling)
22
+ - [Custom Request Options](#custom-request-options)
23
+ - [Using Asynchronously](#using-asynchronously)
24
+ - [Devise](#devise)
25
+ - [Command line](#command-line)
26
+ - [How Pwned is Pi?](#how-pwned-is-pi)
27
+ - [Development](#development)
28
+ - [Contributing](#contributing)
29
+ - [License](#license)
30
+ - [Code of Conduct](#code-of-conduct)
23
31
 
24
32
  ## About
25
33
 
@@ -180,6 +188,17 @@ In addition to these options, HTTP headers can be specified with the `:headers`
180
188
  }
181
189
  ```
182
190
 
191
+ ### Using Asynchronously
192
+
193
+ You may have a use case for hashing the password in advance, and then making the call to the Pwned api later
194
+ (for example if you want to enqueue a job without storing the plaintext password):
195
+
196
+ ```ruby
197
+ hashed_password = Pwned.hash_password(password)
198
+ # some time later
199
+ Pwned::HashPassword.new(hashed_password, request_options).pwned?
200
+ ```
201
+
183
202
  ### Devise
184
203
 
185
204
  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.
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|
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "digest"
3
4
  require "pwned/version"
4
5
  require "pwned/error"
5
6
  require "pwned/password"
7
+ require "pwned/hashed_password"
6
8
 
7
9
  begin
8
10
  # Load Rails and our custom validator
@@ -31,7 +33,7 @@ module Pwned
31
33
  # @param password [String] The password you want to check against the API.
32
34
  # @param [Hash] request_options Options that can be passed to +Net::HTTP.start+ when
33
35
  # calling the API
34
- # @option request_options [Symbol] :headers ({ "User-Agent" => '"Ruby Pwned::Password #{Pwned::VERSION}" })
36
+ # @option request_options [Symbol] :headers ({ "User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}" })
35
37
  # HTTP headers to include in the request
36
38
  # @return [Boolean] Whether the password appears in the data breaches or not.
37
39
  # @since 1.1.0
@@ -49,7 +51,7 @@ module Pwned
49
51
  # @param password [String] The password you want to check against the API.
50
52
  # @param [Hash] request_options Options that can be passed to +Net::HTTP.start+ when
51
53
  # calling the API
52
- # @option request_options [Symbol] :headers ({ "User-Agent" => '"Ruby Pwned::Password #{Pwned::VERSION}" })
54
+ # @option request_options [Symbol] :headers ({ "User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}" })
53
55
  # HTTP headers to include in the request
54
56
  # @return [Integer] The number of times the password has appeared in the data
55
57
  # breaches.
@@ -57,4 +59,18 @@ module Pwned
57
59
  def self.pwned_count(password, request_options={})
58
60
  Pwned::Password.new(password, request_options).pwned_count
59
61
  end
62
+
63
+ ##
64
+ # Returns the full SHA1 hash of the given password in uppercase. This can be safely passed around your code
65
+ # before making the pwned request (e.g. dropped into a queue table).
66
+ #
67
+ # @example
68
+ # Pwned.hash_password("password") #=> 5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8
69
+ #
70
+ # @param password [String] The password you want to check against the API
71
+ # @return [String] An uppercase SHA1 hash of the password
72
+ # @since 2.1.0
73
+ def self.hash_password(password)
74
+ Digest::SHA1.hexdigest(password).upcase
75
+ end
60
76
  end
@@ -0,0 +1,35 @@
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
+ end
34
+ end
35
+ 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,7 +25,7 @@ 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
30
  # @return [Boolean] Whether the password appears in the data breaches or not.
52
31
  # @raise [TypeError] if the password is not a string.
@@ -54,110 +33,10 @@ module Pwned
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
36
+ @hashed_password = Pwned.hash_password(password)
57
37
  @request_options = Hash(request_options).dup
58
38
  @request_headers = Hash(request_options.delete(:headers))
59
39
  @request_headers = DEFAULT_REQUEST_HEADERS.merge(@request_headers)
60
40
  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. If
153
- # chunk_lines is empty, pop returns nil, so this also ensures last_line
154
- # is always a string.
155
- last_line = chunk_lines.pop || ''
156
- chunk_lines.each(&block)
157
- end
158
-
159
- yield last_line unless last_line.empty?
160
- end
161
-
162
41
  end
163
42
  end
@@ -0,0 +1,133 @@
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
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(uri.host, uri.port, request_options) do |http|
112
+ http.request(request, &block)
113
+ end
114
+ end
115
+
116
+ # Stream a Net::HTTPResponse by line, handling lines that cross chunks.
117
+ def stream_response_lines(response, &block)
118
+ last_line = ""
119
+
120
+ response.read_body do |chunk|
121
+ chunk_lines = (last_line + chunk).lines
122
+ # This could end with half a line, so save it for next time. If
123
+ # chunk_lines is empty, pop returns nil, so this also ensures last_line
124
+ # is always a string.
125
+ last_line = chunk_lines.pop || ""
126
+ chunk_lines.each(&block)
127
+ end
128
+
129
+ yield last_line unless last_line.empty?
130
+ end
131
+
132
+ end
133
+ end