nacl_password 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cab426388ecd1f11ff9f5832d6b5fe75a56e8e249eaec25c136c86bba89c6dc5
4
+ data.tar.gz: 9aa167457870e442f30550f1811979a8a78140979c7a01f1d86c50e3312219da
5
+ SHA512:
6
+ metadata.gz: 544e6b5af9ca3af49950858ba5844b08ec64784794a8988ddbc5709827a6a54b5b86f67845ff1bf350c10aa41864355f71d59b41bb9b9532ab4146e09dbacb22
7
+ data.tar.gz: c2080b635da0588db6206fb4f5922c6f2f32ddb9c71d80e202640d0599aa92d4085d4f4a40e652b06e68501291fe9e5acda9cda15a6ca87aff907acece00c604
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2020 Sampson Crowley
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # NaClPassword
2
+ Short description and motivation.
3
+
4
+ ## Usage
5
+ How to use my plugin.
6
+
7
+ ## Installation
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'nacl_password'
12
+ ```
13
+
14
+ And then execute:
15
+ ```bash
16
+ $ bundle
17
+ ```
18
+
19
+ Or install it yourself as:
20
+ ```bash
21
+ $ gem install nacl_password
22
+ ```
23
+
24
+ ## Contributing
25
+ Contribution directions go here.
26
+
27
+ ## License
28
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,190 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'coerce_boolean'
5
+
6
+ module NaClPassword
7
+ INVALID_OPTION_MESSAGE = "predefined options are :min, :interactive, :moderate, :sensitive, and :max"
8
+ UNCOERCEABLE_OPTION_MESSAGE = "value must be coercable to an integer or one of the predefined options (:min | :interactive | :moderate | :sensitive | :max)"
9
+
10
+ # Load rbnacl gem only when nacl_password is used. This is to avoid
11
+ # the entire app using this gem being dependent on a binary library.
12
+ def self.load_gem
13
+ require "rbnacl"
14
+ rescue LoadError, NameError
15
+ $stderr.puts <<-ERROR
16
+ You don't have rbnacl installed in your application.
17
+ Please add it to your Gemfile and run bundle install
18
+ ERROR
19
+
20
+ raise
21
+ end
22
+
23
+ # The Argon2 hash function can handle maximum 2^32 bytes, but system available
24
+ # memory probably cannot.
25
+ # A password of length 1024 bytes is way more than any user would ever need.
26
+ # Put a restriction on password length that keeps memory usage in the available
27
+ # range, but is more than anyone would ever need.
28
+ # other defaults are tested ranges of functional values for libsodium that
29
+ # allow maximum possible security without crashing the system
30
+ def self.setup
31
+ load_gem
32
+ ::NaClPassword::Argon2 ||= RbNaCl::PasswordHash::Argon2
33
+ ::NaClPassword::MAX_PASSWORD_LENGTH ||= 1024
34
+ ::NaClPassword::OPS_LIMIT_RANGE ||= 3..20
35
+ ::NaClPassword::MEM_LIMIT_RANGE ||= (2**25)..(2**32) #32 MB - 4 GB
36
+ ::NaClPassword::DIGEST_SIZE_RANGE ||= 64..512
37
+ self
38
+ end
39
+
40
+ def self.const_missing(name)
41
+ NaClPassword.setup
42
+ if const_defined?(name)
43
+ const_get(name)
44
+ else
45
+ super
46
+ end
47
+ end
48
+
49
+ class << self
50
+ attr_reader :ops_limit # :nodoc:
51
+ attr_reader :mem_limit # :nodoc:
52
+ attr_reader :digest_size # :nodoc:
53
+
54
+ def ops_limit=(value)
55
+ @ops_limit = get_ops_limit(value)
56
+ end
57
+
58
+ def mem_limit=(value)
59
+ @mem_limit = get_mem_limit(value)
60
+ end
61
+
62
+ def digest_size=(value)
63
+ @digest_size = get_digest_size(value)
64
+ digest_size
65
+ end
66
+
67
+ def generate(password)
68
+ salt = RbNaCl::Random.random_bytes(Argon2::SALTBYTES)
69
+ ops = get_ops_limit
70
+ mem = get_mem_limit
71
+ size = get_digest_size
72
+ "#{
73
+ Base64.strict_encode64(encrypt(password, salt, ops, mem, size))
74
+ }.#{
75
+ Base64.strict_encode64(salt)
76
+ }.#{
77
+ ops
78
+ }.#{
79
+ mem
80
+ }.#{
81
+ size
82
+ }"
83
+ end
84
+
85
+ def authenticate(encoded, password)
86
+ digest, *metadata = decode(encoded)
87
+ RbNaCl::PasswordHash.argon2id(password, *metadata) == digest
88
+ end
89
+
90
+ def self.const_missing(name)
91
+ NaClPassword.const_missing(name)
92
+ end
93
+
94
+ private
95
+ def encrypt(...)
96
+ RbNaCl::PasswordHash.argon2id(...)
97
+ end
98
+
99
+ def decode(encoded)
100
+ digest_64, salt_64, ops, mem, size = encoded.split(".")
101
+ digest = Base64.strict_decode64(digest_64)
102
+ salt = Base64.strict_decode64(salt_64)
103
+ ops = get_ops_limit(ops)
104
+ mem = get_mem_limit(mem)
105
+ size = get_digest_size(size)
106
+ [ digest, salt, ops, mem, size ]
107
+ end
108
+
109
+ def get_ops_limit(ops = NaClPassword.ops_limit)
110
+ ops = :moderate unless CoerceBoolean.from(ops)
111
+
112
+ case ops
113
+ when :min then OPS_LIMIT_RANGE.min
114
+ when :interactive then 5
115
+ when :moderate then 10
116
+ when :sensitive then 15
117
+ when :max then OPS_LIMIT_RANGE.max
118
+ when Symbol
119
+ raise ArgumentError, INVALID_OPTION_MESSAGE
120
+ else
121
+ case ops = ops.to_i
122
+ when OPS_LIMIT_RANGE then ops
123
+ else
124
+ raise \
125
+ ArgumentError,
126
+ "ops_limit must be within the range #{OPS_LIMIT_RANGE}"
127
+ end
128
+ end
129
+ rescue NoMethodError
130
+ raise ArgumentError, UNCOERCEABLE_OPTION_MESSAGE
131
+ end
132
+
133
+ def get_mem_limit(mem = NaClPassword.mem_limit)
134
+ mem = :moderate unless CoerceBoolean.from(mem)
135
+
136
+ case mem
137
+ when :min then MEM_LIMIT_RANGE.min # 32mb
138
+ when :interactive then (2**26) # 64mb
139
+ when :moderate then (2**28) # 256mb
140
+ when :sensitive then (2**30) # 1024mb
141
+ when :max then MEM_LIMIT_RANGE.max # 4096mb
142
+ when Symbol
143
+ raise ArgumentError, INVALID_OPTION_MESSAGE
144
+ else
145
+ case mem = mem.to_i
146
+ when MEM_LIMIT_RANGE then mem
147
+ else
148
+ raise \
149
+ ArgumentError,
150
+ "mem_limit must be within the range #{MEM_LIMIT_RANGE}"
151
+ end
152
+ end
153
+ rescue NoMethodError
154
+ raise ArgumentError, UNCOERCEABLE_OPTION_MESSAGE
155
+ end
156
+
157
+ def get_digest_size(size = NaClPassword.digest_size)
158
+ size = :moderate unless CoerceBoolean.from(size)
159
+
160
+ case size
161
+ when :min then DIGEST_SIZE_RANGE.min
162
+ when :interactive then DIGEST_SIZE_RANGE.min * 2
163
+ when :moderate then DIGEST_SIZE_RANGE.min * 4
164
+ when :sensitive then DIGEST_SIZE_RANGE.min * 8
165
+ when :max then DIGEST_SIZE_RANGE.max
166
+ when Symbol
167
+ raise ArgumentError, INVALID_OPTION_MESSAGE
168
+ else
169
+ case size = size.to_i
170
+ when DIGEST_SIZE_RANGE
171
+ unless size % 64 == 0
172
+ raise ArgumentError, "digest_size must be a multiple of 64"
173
+ end
174
+
175
+ size
176
+ else
177
+ raise ArgumentError, "digest_size must be within the range #{DIGEST_SIZE_RANGE}"
178
+ end
179
+ end
180
+ rescue NoMethodError
181
+ raise ArgumentError, UNCOERCEABLE_OPTION_MESSAGE
182
+ end
183
+ end
184
+
185
+ self.ops_limit = :moderate
186
+ self.mem_limit = :moderate
187
+ self.digest_size = :moderate
188
+ end
189
+
190
+ require "nacl_password/railtie" if defined? Rails
@@ -0,0 +1,190 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'nacl_password'
5
+
6
+ module NaClPassword
7
+ module Concern
8
+ extend ActiveSupport::Concern
9
+
10
+ module ClassMethods
11
+ # Adds methods to set and authenticate against a Argon2 password.
12
+ # This mechanism requires you to have a +XXX_digest+ attribute.
13
+ # Where +XXX+ is the attribute name of your desired password.
14
+ # the +digest+ attribute to use can be set by passing
15
+ # `digest_attribute: non_standard_attribute` to `nacl_password`
16
+ #
17
+ # The following validations are added automatically:
18
+ # * Password must be present on creation
19
+ # * Password length should be less than or equal to 1024 bytes
20
+ # * Confirmation of password (using a +XXX_confirmation+ attribute)
21
+ #
22
+ # If confirmation validation is not needed, simply leave out the
23
+ # value for +XXX_confirmation+ (i.e. don't provide a form field for
24
+ # it). When this attribute has a +nil+ value, the validation will not be
25
+ # triggered.
26
+ #
27
+ # It is also possible to suppress the default validations completely by
28
+ # passing skip_validations: true as an argument.
29
+ #
30
+ # Add rbnacl (~> 7.1) to Gemfile to use #nacl_password:
31
+ #
32
+ # gem "rbnacl", "~> 7.1"
33
+ #
34
+ # Example:
35
+ #
36
+ # # Schema: User(name:string, password_digest:string, recovery_password_digest:string)
37
+ # class User < ActiveRecord::Base
38
+ # include NaClPassword::Concern
39
+ # nacl_password
40
+ # nacl_password :recovery_password, validations: false
41
+ # end
42
+ #
43
+ # user = User.new(name: 'david', password: '', password_confirmation: 'nomatch')
44
+ # user.save # => false, password required
45
+ # user.password = 'mUc3m00RsqyRe'
46
+ # user.save # => false, confirmation doesn't match
47
+ # user.password_confirmation = 'mUc3m00RsqyRe'
48
+ # user.save # => true
49
+ # user.recovery_password = "42password"
50
+ # user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC"
51
+ # user.save # => true
52
+ # user.authenticate('notright') # => false
53
+ # user.authenticate('mUc3m00RsqyRe') # => user
54
+ # user.authenticate_recovery_password('42password') # => user
55
+ # User.find_by(name: 'david')&.authenticate('notright') # => false
56
+ # User.find_by(name: 'david')&.authenticate('mUc3m00RsqyRe') # => user
57
+ def nacl_password(attribute = :password, digest_attribute: nil, **opts)
58
+ NaClPassword.setup
59
+
60
+ digest_attribute ||= "#{attribute}_digest"
61
+
62
+ attribute = attribute.to_sym
63
+ digest_attribute = digest_attribute.to_sym
64
+
65
+ if digest_attribute.to_s == attribute.to_s
66
+ raise ArgumentError, "Digest Attribute Name can't be the same as Password Attribute Name"
67
+ end
68
+
69
+ skip_validations =
70
+ CoerceBoolean.from(opts[:skip_validations]) &&
71
+ (opts[:skip_validations] != :blank)
72
+
73
+ length_options =
74
+ skip_validations ? {} :
75
+ { maximum: NaClPassword::MAX_PASSWORD_LENGTH }.
76
+ merge(
77
+ opts[:min_length] == :none \
78
+ ? {} \
79
+ : { minimum: opts[:min_length].presence&.to_i || 8 }
80
+ )
81
+
82
+
83
+ include InstanceMethodsOnActivation.new(attribute.to_sym, digest_attribute, **length_options)
84
+
85
+ unless skip_validations
86
+ include ActiveModel::Validations
87
+
88
+ # This ensures the model has a password by checking whether the password_digest
89
+ # is present, so that this works with both new and existing records. However,
90
+ # when there is an error, the message is added to the password attribute instead
91
+ # so that the error message will make sense to the end-user.
92
+ unless opts[:skip_validations] == :blank
93
+ validate do |record|
94
+ unless record.__send__(digest_attribute).present?
95
+ record.errors.add(attribute, :blank)
96
+ end
97
+ end
98
+ end
99
+
100
+ validates_length_of attribute, **length_options, allow_blank: true
101
+
102
+ validates_confirmation_of attribute, allow_blank: true
103
+ end
104
+ end
105
+ end
106
+
107
+ class InstanceMethodsOnActivation < Module
108
+ def initialize(attribute, digest_attribute, **attribute_opts)
109
+ # == Constants ============================================================
110
+ confirmation_var = "@#{attribute}_confirmation"
111
+
112
+ # == Attributes ===========================================================
113
+ attr_reader attribute
114
+
115
+ define_method("#{attribute}=") do |given_password|
116
+ instance_variable_set("@#{attribute}", given_password.presence)
117
+
118
+ if given_password.nil? || given_password.empty?
119
+ self.__send__("#{digest_attribute}=", nil)
120
+ else
121
+ self.__send__(
122
+ "#{digest_attribute}=",
123
+ NaClPassword.generate(given_password)
124
+ )
125
+ end
126
+ end
127
+
128
+ define_method("#{attribute}_confirmation=") do |given_password|
129
+ instance_variable_set(confirmation_var, given_password)
130
+ end
131
+
132
+ # == Boolean Methods ======================================================
133
+ define_method("#{attribute}_length_valid?") do
134
+ value = self.__send__(attribute)
135
+ invalid = false
136
+
137
+ if attribute_opts[:maximum].present?
138
+ invalid ||= (value.length > attribute_opts[:maximum])
139
+ end
140
+
141
+ if attribute_opts[:minimum].present?
142
+ invalid ||= (value.length < attribute_opts[:minimum])
143
+ end
144
+
145
+ !invalid
146
+ end
147
+
148
+ define_method("#{attribute}_confirmed?") do
149
+ self.__send__(attribute) == self.instance_variable_get(confirmation_var)
150
+ end
151
+
152
+ define_method("#{attribute}_ready?") do |require_confirmation = false|
153
+ value = self.__send__(attribute)
154
+ if value.nil? || value.empty?
155
+ self.__send__("#{digest_attribute}").present?
156
+ else
157
+ confirmation = self.instance_variable_get(confirmation_var)
158
+ if !require_confirmation && (confirmation.nil? || confirmation.empty?)
159
+ self.__send__("#{attribute}_length_valid?")
160
+ else
161
+ self.__send__("#{attribute}_confirmed?") \
162
+ && self.__send__("#{attribute}_length_valid?")
163
+ end
164
+ end
165
+ end
166
+
167
+ # == Instance Methods =====================================================
168
+
169
+ # Returns +self+ if the password is correct, otherwise +nil+.
170
+ #
171
+ # class User < ActiveRecord::Base
172
+ # nacl_password validations: false
173
+ # end
174
+ #
175
+ # user = User.new(name: 'david', password: 'mUc3m00RsqyRe')
176
+ # user.save
177
+ # user.authenticate_password('mUc3m00RsqyRe') # => user
178
+ # user.authenticate_password('notright') # => nil
179
+ define_method("authenticate_#{attribute}") do |given_password|
180
+ return nil unless attribute_digest = __send__(digest_attribute)
181
+ if NaClPassword.authenticate(attribute_digest, given_password)
182
+ self
183
+ end
184
+ end
185
+
186
+ alias_method :authenticate, :authenticate_password if attribute == :password
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,10 @@
1
+ module NaClPassword
2
+ class Railtie < ::Rails::Railtie
3
+ initializer 'nacl_password.include_concern' do
4
+ ActiveSupport.on_load(:active_record) do
5
+ require 'nacl_password/concern'
6
+ ActiveRecord::Base.send :include, NaClPassword::Concern
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ module NaClPassword
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :nacl_password do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,115 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nacl_password
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sampson Crowley
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-04-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: coerce_boolean
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rbnacl
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '7.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '7.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '6.0'
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: 6.0.2.2
51
+ type: :development
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - "~>"
56
+ - !ruby/object:Gem::Version
57
+ version: '6.0'
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 6.0.2.2
61
+ - !ruby/object:Gem::Dependency
62
+ name: sqlite3
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ description: |2
76
+ Easier encryption and decryption with libsodium while remaining configurable
77
+ with validated options. Also includes a concern for a "has_secure_password"
78
+ style one-line setup
79
+ email:
80
+ - sampsonsprojects@gmail.com
81
+ executables: []
82
+ extensions: []
83
+ extra_rdoc_files: []
84
+ files:
85
+ - MIT-LICENSE
86
+ - README.md
87
+ - lib/nacl_password.rb
88
+ - lib/nacl_password/concern.rb
89
+ - lib/nacl_password/railtie.rb
90
+ - lib/nacl_password/version.rb
91
+ - lib/tasks/nacl_password_tasks.rake
92
+ homepage: https://github.com/SampsonCrowley/nacl_password
93
+ licenses:
94
+ - MIT
95
+ metadata: {}
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubygems_version: 3.1.2
112
+ signing_key:
113
+ specification_version: 4
114
+ summary: RbNaCl on Rails
115
+ test_files: []