pwned 2.0.2 → 2.1.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: 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