pwned 1.2.0 → 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: 1e197213ac23ae94598dbf91b7a09fadd740db379d5abefdb8ad7c9623d9514b
4
- data.tar.gz: 960c6e4ab1d856480dbc236059abea531dc747f4a1d59a7e7db52534c2958866
3
+ metadata.gz: 4661790082f543ba897baf211da660c7a4f654444121f4ff3ba08542c08c412b
4
+ data.tar.gz: f52a3f3cf36d461e8704a632f97829a5d9f871d9916a55687d4a0b2156b44b75
5
5
  SHA512:
6
- metadata.gz: dede2324974438b89612b443e50c8d7c06fe9b35d3e5c6f36661ad73d28513ebb62eedbc60f0a72d346c935141b96f30b98a6ad21cb2950f45a418c76d33c6b1
7
- data.tar.gz: 32a0659ce0a7b80967ebf68809bf5881623f473ab561dfcba9f131eb86af60b9f0cb4031603c85c295d37f9d7aae10ce03ae3a580c3cc437c486cff099a4f962
6
+ metadata.gz: c114c3ca6e7667d1760ad2ae5dabcc7bf8d14b91e42788f7e36bba716eecd9bef6e1847e93dd12df4f8afed19460d26a068dc22ffb2270ceef8dc342f81690e0
7
+ data.tar.gz: c19d20d765cd57e64468c27a3e8f134e53d8f6e9ae22497c2d94a315a584e2e19b1913d47b37e89c9525ce80d85d10d43437a623d211766aa7242dbe1144e906
@@ -3,21 +3,25 @@ language: ruby
3
3
 
4
4
  env:
5
5
  matrix:
6
- - RAILS_VERSION=4.2.0
7
- - RAILS_VERSION=5.0.0
8
- - RAILS_VERSION=5.1.0
9
- - RAILS_VERSION=5.2.0.rc1
6
+ - RAILS_VERSION=4.2.11.1
7
+ - RAILS_VERSION=5.0.7.2
8
+ - RAILS_VERSION=5.1.7
9
+ - RAILS_VERSION=5.2.3
10
+ - RAILS_VERSION=6.0.0
10
11
 
11
12
  rvm:
12
- - 2.5.0
13
- - 2.4.0
14
- - 2.3.0
13
+ - 2.7
14
+ - 2.6
15
+ - 2.5
16
+ - 2.4
15
17
  - jruby
16
18
  - ruby-head
17
19
 
18
- before_install: gem install bundler -v 1.16.1
20
+ before_install: gem install bundler
19
21
 
20
22
  matrix:
21
23
  allow_failures:
22
24
  - rvm: ruby-head
23
- - env: RAILS_VERSION=5.2.0.rc1
25
+ exclude:
26
+ - rvm: 2.4
27
+ env: RAILS_VERSION=6.0.0
@@ -0,0 +1 @@
1
+ --output-dir docs
@@ -1,26 +1,81 @@
1
1
  # Changelog for `Pwned`
2
2
 
3
- ## Ongoing [☰](https://github.com/philnash/pwned/compare/v1.1.0...master)
3
+ ## Ongoing [☰](https://github.com/philnash/pwned/compare/v2.0.2...master)
4
4
 
5
- ...
5
+ ## 2.1.0 (July 8, 2020) [☰](https://github.com/philnash/pwned/compare/v2.0.2...v2.1.0)
6
6
 
7
- ## 1.2.0 (March 15, 2018) [☰](https://github.com/philnash/pwned/commits/v1.2.0)
7
+ - Minor updates
8
8
 
9
- * Major updates
10
- * Changes `PwnedValidator` to `NotPwnedValidator`, so that the validation looks like `validates :password, not_pwned: true`. `PwnedValidator` now subclasses `NotPwnedValidator` for backwards compatibility with version 1.1.0 but is deprecated.
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).
11
12
 
12
- ## 1.1.0 (March 12, 2018) [☰](https://github.com/philnash/pwned/commits/v1.1.0)
13
+ ## 2.0.2 (May 20, 2020) [☰](https://github.com/philnash/pwned/compare/v2.0.1...v2.0.2)
13
14
 
14
- * Major updates
15
- * Refactors exception handling with built in Ruby method ([PR #1](https://github.com/philnash/pwned/pull/1) thanks [@kpumuk](https://github.com/kpumuk))
16
- * Passwords must be strings, the initializer will raise a `TypeError` unless `password.is_a? String`. ([dbf7697](https://github.com/philnash/pwned/commit/dbf7697e878d87ac74aed1e715cee19b73473369))
17
- * Added Ruby on Rails validator ([PR #3](https://github.com/philnash/pwned/pull/3) & [PR #6](https://github.com/philnash/pwned/pull/6))
18
- * Added simplified accessors `Pwned.pwned?` and `Pwned.pwned_count` ([PR #4](https://github.com/philnash/pwned/pull/4))
15
+ - Minor fix
19
16
 
20
- * Minor updates
21
- * SHA1 is only calculated once
22
- * Frozen string literal to make sure Ruby does not copy strings over and over again
23
- * Removal of `@match_data`, since we only use it to retrieve the counter. Caching the counter instead (all [PR #2](https://github.com/philnash/pwned/pull/2) thanks [@kpumuk](https://github.com/kpumuk))
17
+ - It was found to be possible for reading the lines body of a response to
18
+ result in a `nil` which caused trouble with string concatenation. This
19
+ avoids that scenario. Fixes #18, thanks [@flori](https://github.com/flori).
20
+
21
+ ## 2.0.1 (January 14, 2020) [☰](https://github.com/philnash/pwned/compare/v2.0.0...v2.0.1)
22
+
23
+ - Minor updates
24
+
25
+ - Adds double-splat to ActiveModel::Errors#add calls with options to make Ruby 2.7 happy.
26
+ - Detects presence of Net::HTTPClientException in tests to remove deprecation warning.
27
+
28
+ ## 2.0.0 (October 1, 2019) [☰](https://github.com/philnash/pwned/compare/v1.2.1...v2.0.0)
29
+
30
+ - Major updates
31
+
32
+ - Switches from `open-uri` to `Net::HTTP`. This is a potentially breaking change.
33
+ - `request_options` are now used to configure `Net::HTTP.start`.
34
+ - Rather than using all string keys from `request_options`, HTTP headers are now
35
+ specified in their own `headers` hash. To upgrade, any options intended as
36
+ headers need to be extracted into a `headers` hash, e.g.
37
+
38
+ ```diff
39
+ validates :password, not_pwned: {
40
+ - request_options: { read_timeout: 5, open_timeout: 1, "User-Agent" => "Super fun user agent" }
41
+ + request_options: { read_timeout: 5, open_timeout: 1, headers: { "User-Agent" => "Super fun user agent" } }
42
+ }
43
+
44
+ - password = Pwned::Password.new("password", 'User-Agent' => 'Super fun new user agent')
45
+ + password = Pwned::Password.new("password", headers: { 'User-Agent' => 'Super fun new user agent' }, read_timeout: 10)
46
+ ```
47
+
48
+ - Adds a CLI to let you check passwords on the command line
49
+
50
+ ```bash
51
+ $ pwned password
52
+ Pwned!
53
+ The password has been found in public breaches 3730471 times.
54
+ ```
55
+
56
+ ## 1.2.1 (March 17, 2018) [☰](https://github.com/philnash/pwned/compare/v1.2.0...v1.2.1)
57
+
58
+ - Minor updates
59
+ - Validator no longer raises `TypeError` when password is `nil`
60
+
61
+ ## 1.2.0 (March 15, 2018) [☰](https://github.com/philnash/pwned/compare/v1.1.0...v1.2.0)
62
+
63
+ - Major updates
64
+ - Changes `PwnedValidator` to `NotPwnedValidator`, so that the validation looks like `validates :password, not_pwned: true`. `PwnedValidator` now subclasses `NotPwnedValidator` for backwards compatibility with version 1.1.0 but is deprecated.
65
+
66
+ ## 1.1.0 (March 12, 2018) [☰](https://github.com/philnash/pwned/compare/v1.0.0...v1.1.0)
67
+
68
+ - Major updates
69
+
70
+ - Refactors exception handling with built in Ruby method ([PR #1](https://github.com/philnash/pwned/pull/1) thanks [@kpumuk](https://github.com/kpumuk))
71
+ - Passwords must be strings, the initializer will raise a `TypeError` unless `password.is_a? String`. ([dbf7697](https://github.com/philnash/pwned/commit/dbf7697e878d87ac74aed1e715cee19b73473369))
72
+ - Added Ruby on Rails validator ([PR #3](https://github.com/philnash/pwned/pull/3) & [PR #6](https://github.com/philnash/pwned/pull/6))
73
+ - Added simplified accessors `Pwned.pwned?` and `Pwned.pwned_count` ([PR #4](https://github.com/philnash/pwned/pull/4))
74
+
75
+ - Minor updates
76
+ - SHA1 is only calculated once
77
+ - Frozen string literal to make sure Ruby does not copy strings over and over again
78
+ - Removal of `@match_data`, since we only use it to retrieve the counter. Caching the counter instead (all [PR #2](https://github.com/philnash/pwned/pull/2) thanks [@kpumuk](https://github.com/kpumuk))
24
79
 
25
80
  ## 1.0.0 (March 6, 2018) [☰](https://github.com/philnash/pwned/commits/v1.0.0)
26
81
 
data/README.md CHANGED
@@ -2,9 +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)
6
-
7
- [API docs](https://philnash.github.io/pwned/) | [GitHub repo](https://github.com/philnash/pwned)
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
+
7
+ [API docs](https://www.rubydoc.info/gems/pwned) | [GitHub repo](https://github.com/philnash/pwned)
8
+
9
+ ## Table of Contents
10
+
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)
8
31
 
9
32
  ## About
10
33
 
@@ -14,6 +37,8 @@ Troy Hunt's [Pwned Passwords API V2](https://haveibeenpwned.com/API/v2#PwnedPass
14
37
 
15
38
  The data from this API is provided by [Have I been pwned?](https://haveibeenpwned.com/). Before using the API, please check [the acceptable uses and license of the API](https://haveibeenpwned.com/API/v2#AcceptableUse).
16
39
 
40
+ Here is a blog post I wrote on [how to use this gem in your Ruby applications to make your users' passwords better](https://www.twilio.com/blog/2018/03/better-passwords-in-ruby-applications-pwned-passwords-api.html).
41
+
17
42
  ## Installation
18
43
 
19
44
  Add this line to your application's Gemfile:
@@ -32,6 +57,14 @@ Or install it yourself as:
32
57
 
33
58
  ## Usage
34
59
 
60
+ There are a few ways you can use this gem:
61
+
62
+ 1. [Plain Ruby](#plain-ruby)
63
+ 2. [Rails](#activerecord-validator)
64
+ 3. [Rails and Devise](#devise)
65
+
66
+ ### Plain Ruby
67
+
35
68
  To test a password against the API, instantiate a `Pwned::Password` object and then ask if it is `pwned?`.
36
69
 
37
70
  ```ruby
@@ -72,10 +105,11 @@ Pwned.pwned_count("password")
72
105
 
73
106
  #### Advanced
74
107
 
75
- You can set options and headers to be used with `open-uri` when making the request to the API. HTTP headers must be string keys and the [other options are available in the `OpenURI::OpenRead` module](https://ruby-doc.org/stdlib-2.5.0/libdoc/open-uri/rdoc/OpenURI/OpenRead.html#method-i-open).
108
+ You can set http request options to be used with `Net::HTTP.start` when making the request to the API. These options are
109
+ 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.
76
110
 
77
111
  ```ruby
78
- password = Pwned::Password.new("password", { 'User-Agent' => 'Super fun new user agent' })
112
+ password = Pwned::Password.new("password", headers: { 'User-Agent' => 'Super fun new user agent' }, read_timeout: 10)
79
113
  ```
80
114
 
81
115
  ### ActiveRecord Validator
@@ -113,7 +147,7 @@ class User < ApplicationRecord
113
147
  end
114
148
  ```
115
149
 
116
- #### Network Errors Handling
150
+ #### Network Error Handling
117
151
 
118
152
  By default the record will be treated as valid when we cannot reach the [haveibeenpwned.com](https://haveibeenpwned.com/) servers. This can be changed with the `:on_error` validator parameter:
119
153
 
@@ -145,17 +179,112 @@ end
145
179
 
146
180
  #### Custom Request Options
147
181
 
148
- You can configure network requests made from the validator using `:request_options` (see [OpenURI::OpenRead#open](http://ruby-doc.org/stdlib-2.5.0/libdoc/open-uri/rdoc/OpenURI/OpenRead.html#method-i-open) for the list of available options, string keys represent custom network request headers, e.g. `"User-Agent"`):
182
+ 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).
183
+ In addition to these options, HTTP headers can be specified with the `:headers` key, e.g. `"User-Agent"`):
149
184
 
150
185
  ```ruby
151
186
  validates :password, not_pwned: {
152
- request_options: { read_timeout: 5, open_timeout: 1, "User-Agent" => "Super fun user agent" }
187
+ request_options: { read_timeout: 5, open_timeout: 1, headers: { "User-Agent" => "Super fun user agent" } }
153
188
  }
154
189
  ```
155
190
 
156
- ## TODO
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
+
202
+ ### Devise
203
+
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.
205
+
206
+ ### Command line
207
+
208
+ The gem provides a command line utility for checking passwords. You can call it from your terminal application like this:
209
+
210
+ ```bash
211
+ $ pwned password
212
+ Pwned!
213
+ The password has been found in public breaches 3645804 times.
214
+ ```
215
+
216
+ If you don't want the password you are checking to be visible, call:
157
217
 
158
- - [ ] Devise plugin
218
+ ```bash
219
+ $ pwned --secret
220
+ ```
221
+
222
+ You will be prompted for the password, but it won't be displayed.
223
+
224
+ ## How Pwned is Pi?
225
+
226
+ [@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.
227
+
228
+ ```ruby
229
+ require 'pwned'
230
+
231
+ PI = '3.14159265358979323846264338327950288419716939937510582097494459230781640628620899862803482534211706798214808651328230664709384460955058223172535940812848111'
232
+
233
+ for n in 1..40
234
+ password = Pwned::Password.new PI[0..(n + 1)]
235
+ str = [ n.to_s.rjust(2) ]
236
+ str << (password.pwned? ? '😡' : '😃')
237
+ str << password.pwned_count.to_s.rjust(4)
238
+ str << password.password
239
+
240
+ puts str.join ' '
241
+ end
242
+ ```
243
+
244
+ The results may, or may not, surprise you.
245
+
246
+ ```
247
+ 1 😡 16 3.1
248
+ 2 😡 238 3.14
249
+ 3 😡 34 3.141
250
+ 4 😡 1345 3.1415
251
+ 5 😡 2552 3.14159
252
+ 6 😡 791 3.141592
253
+ 7 😡 9582 3.1415926
254
+ 8 😡 1591 3.14159265
255
+ 9 😡 637 3.141592653
256
+ 10 😡 873 3.1415926535
257
+ 11 😡 137 3.14159265358
258
+ 12 😡 103 3.141592653589
259
+ 13 😡 65 3.1415926535897
260
+ 14 😡 201 3.14159265358979
261
+ 15 😡 41 3.141592653589793
262
+ 16 😡 57 3.1415926535897932
263
+ 17 😡 28 3.14159265358979323
264
+ 18 😡 29 3.141592653589793238
265
+ 19 😡 1 3.1415926535897932384
266
+ 20 😡 7 3.14159265358979323846
267
+ 21 😡 5 3.141592653589793238462
268
+ 22 😡 2 3.1415926535897932384626
269
+ 23 😡 2 3.14159265358979323846264
270
+ 24 😃 0 3.141592653589793238462643
271
+ 25 😡 3 3.1415926535897932384626433
272
+ 26 😃 0 3.14159265358979323846264338
273
+ 27 😃 0 3.141592653589793238462643383
274
+ 28 😃 0 3.1415926535897932384626433832
275
+ 29 😃 0 3.14159265358979323846264338327
276
+ 30 😃 0 3.141592653589793238462643383279
277
+ 31 😃 0 3.1415926535897932384626433832795
278
+ 32 😃 0 3.14159265358979323846264338327950
279
+ 33 😃 0 3.141592653589793238462643383279502
280
+ 34 😃 0 3.1415926535897932384626433832795028
281
+ 35 😃 0 3.14159265358979323846264338327950288
282
+ 36 😃 0 3.141592653589793238462643383279502884
283
+ 37 😃 0 3.1415926535897932384626433832795028841
284
+ 38 😃 0 3.14159265358979323846264338327950288419
285
+ 39 😃 0 3.141592653589793238462643383279502884197
286
+ 40 😃 0 3.1415926535897932384626433832795028841971
287
+ ```
159
288
 
160
289
  ## Development
161
290
 
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "pwned"
4
+ require "optparse"
5
+ require "io/console"
6
+
7
+ options = {}
8
+ parser = OptionParser.new do |opts|
9
+ opts.banner = <<-USAGE
10
+ Usage: pwned <password>
11
+
12
+ Tests a password against the Pwned Passwords API using the k-anonymity model,
13
+ which avoids sending the entire password to the service.opts
14
+
15
+ If the password has been found in a publicly available breach then this tool
16
+ will report how many times it has been seen. Otherwise the tool will report that
17
+ the password has not been found in a public breach yet.
18
+
19
+ USAGE
20
+
21
+ opts.version = Pwned::VERSION
22
+
23
+ opts.on("-s", "--secret", "Enter password without displaying characters.\n#{" "* 37}Overrides provided arguments.")
24
+ opts.on_tail("-h", "--help", "Show help.")
25
+ opts.on_tail("-v", "--version", "Show version number.\n\n")
26
+ end
27
+
28
+ parser.parse!(ARGV, into: options)
29
+
30
+ if options[:help]
31
+ puts parser.help
32
+ exit
33
+ end
34
+ if options[:version]
35
+ puts parser.ver
36
+ exit
37
+ end
38
+ password_to_test = ARGV.first
39
+ if options[:secret]
40
+ password_to_test = STDIN.getpass("Password: ")
41
+ end
42
+ if !password_to_test || password_to_test.strip == ""
43
+ puts parser.help
44
+ exit
45
+ end
46
+ password = Pwned::Password.new(password_to_test || ARGV.first)
47
+ if password.pwned?
48
+ puts "Pwned!\nThe password has been found in public breaches #{password.pwned_count} times."
49
+ else
50
+ puts "The password has not been found in a public breach."
51
+ end
52
+
@@ -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
@@ -29,10 +31,10 @@ module Pwned
29
31
  # Pwned.pwned?("pwned::password") #=> false
30
32
  #
31
33
  # @param password [String] The password you want to check against the API.
32
- # @param [Hash] request_options Options that can be passed to +open+ when
34
+ # @param [Hash] request_options Options that can be passed to +Net::HTTP.start+ when
33
35
  # calling the API
34
- # @option request_options [String] 'User-Agent' ("Ruby Pwned::Password #{Pwned::VERSION}")
35
- # The user agent used when making an API request.
36
+ # @option request_options [Symbol] :headers ({ "User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}" })
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
38
40
  def self.pwned?(password, request_options={})
@@ -47,14 +49,28 @@ module Pwned
47
49
  # Pwned.pwned_count("pwned::password") #=> 0
48
50
  #
49
51
  # @param password [String] The password you want to check against the API.
50
- # @param [Hash] request_options Options that can be passed to +open+ when
52
+ # @param [Hash] request_options Options that can be passed to +Net::HTTP.start+ when
51
53
  # calling the API
52
- # @option request_options [String] 'User-Agent' ("Ruby Pwned::Password #{Pwned::VERSION}")
53
- # The user agent used when making an API request.
54
+ # @option request_options [Symbol] :headers ({ "User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}" })
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.
56
58
  # @since 1.1.0
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