devise-argon2 1.1.0 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +66 -0
  3. data/.gitignore +3 -0
  4. data/CHANGELOG.md +32 -0
  5. data/Gemfile +9 -4
  6. data/README.md +120 -32
  7. data/devise-argon2.gemspec +17 -14
  8. data/lib/devise-argon2/model.rb +101 -0
  9. data/lib/devise-argon2/version.rb +5 -0
  10. data/lib/devise-argon2.rb +8 -3
  11. data/spec/devise-argon2_spec.rb +273 -27
  12. data/spec/orm/active_record.rb +1 -0
  13. data/spec/orm/mongoid.rb +5 -0
  14. data/spec/rails_app/Rakefile +6 -0
  15. data/spec/rails_app/app/active_record/old_user.rb +3 -0
  16. data/spec/rails_app/app/active_record/user.rb +3 -0
  17. data/spec/rails_app/app/controllers/application_controller.rb +3 -0
  18. data/spec/rails_app/app/mongoid/old_user.rb +12 -0
  19. data/spec/rails_app/app/mongoid/user.rb +10 -0
  20. data/spec/rails_app/bin/bundle +109 -0
  21. data/spec/rails_app/bin/rails +4 -0
  22. data/spec/rails_app/bin/rake +4 -0
  23. data/spec/rails_app/bin/setup +33 -0
  24. data/spec/rails_app/config/application.rb +24 -0
  25. data/spec/rails_app/config/boot.rb +5 -0
  26. data/spec/rails_app/config/database.yml +14 -0
  27. data/spec/rails_app/config/environment.rb +7 -0
  28. data/spec/rails_app/config/initializers/devise.rb +6 -0
  29. data/spec/rails_app/config/mongoid.yml +10 -0
  30. data/spec/rails_app/config.ru +6 -0
  31. data/spec/rails_app/db/migrate/20230617201921_devise_create_users.rb +15 -0
  32. data/spec/rails_app/db/migrate/20231004084147_devise_create_old_users.rb +17 -0
  33. data/spec/rails_app/db/schema.rb +31 -0
  34. data/spec/spec_helper.rb +7 -3
  35. metadata +64 -31
  36. data/.travis.yml +0 -13
  37. data/lib/devise/encryptable/encryptors/argon2/version.rb +0 -7
  38. data/lib/devise/encryptable/encryptors/argon2.rb +0 -17
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6710d39a7495e895f798cb907b4d4e4d67fa0f39fd3141d2a4d76d6da0c0b7a4
4
- data.tar.gz: 48086d611acd10f4d91b2ccfa229bac0abe6f587abdae1a3b3df154c6d2d6408
3
+ metadata.gz: d036bff0c949c49457df0df4a4ac902d4ed0e65e84fd26f2940bfcc973b6bcc3
4
+ data.tar.gz: 82024dfd476f476514c5548b4aac5a93c49ffffad6fe33252c153381d0b803c1
5
5
  SHA512:
6
- metadata.gz: 9e1be2f0b26ca41bb61ea3f42d1bfae79e59b1a670fe23574d1453138173caa8e4267266a169d9bfcc15bead58be9fb8009d70d183bf18a22967682ba58c095a
7
- data.tar.gz: 22671a23d33b2de4c6c5ee5e50d1e5eb6a46009c338612256138b8ad72ff05cef442854c9f6ceab4f7a42934fbbafad0d213ce898c9c6ce6916c58625bed0335
6
+ metadata.gz: fb3857086fc9f31fd22bec613c3fe9e93534234036db242c49b1e5aae6ac9340611916e62ec92f84e67b8fafe97610b6d947c98df7846e62d91d9e550586689b
7
+ data.tar.gz: b7e523688dab140c94d9aed10232a57a1dcb144b437073d8ec41952fe0595f8713215d5ed9658701927f50182d2cf49adb0fd7bc5792d21255edaf70ffa603f5
@@ -0,0 +1,66 @@
1
+ name: Test suite
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ test:
7
+ runs-on: ubuntu-latest
8
+ strategy:
9
+ matrix:
10
+ ruby-version: ['2.7', '3.0', '3.1', '3.2', 'ruby-head']
11
+ rails-version: ['~> 7.0', '~> 6.1']
12
+ argon2-version: ['2.2', '2.3']
13
+ orm:
14
+ - adapter: active_record
15
+ - adapter: mongoid
16
+ mongoid-version: 8.1.2
17
+ - adapter: mongoid
18
+ mongoid-version: 8.0.6
19
+ - adapter: mongoid
20
+ mongoid-version: 7.5.4
21
+ include:
22
+ - rails-version: '~> 6.1'
23
+ ruby-version: '3.1'
24
+ argon2-version: '2.3'
25
+ devise-version: '4.8'
26
+ orm:
27
+ adapter: active_record
28
+ - rails-version: '~> 7.1'
29
+ ruby-version: '3.1'
30
+ argon2-version: '2.3'
31
+ devise-version: '4.9'
32
+ orm:
33
+ adapter: active_record
34
+ - rails-version: '~> 7.1'
35
+ ruby-version: '3.2'
36
+ argon2-version: '2.3'
37
+ devise-version: '4.9'
38
+ orm:
39
+ adapter: active_record
40
+ - rails-version: '~> 7.1'
41
+ ruby-version: '3.1'
42
+ argon2-version: '2.1'
43
+ devise-version: '4.9'
44
+ orm:
45
+ adapter: active_record
46
+ env:
47
+ RAILS_VERSION: ${{ matrix.rails-version || '~> 7.0'}}
48
+ MONGOID_VERSION: ${{ matrix.orm.mongoid-version || '8.1.2'}}
49
+ ORM: ${{ matrix.orm.adapter }}
50
+ ARGON2_VERSION: ${{ matrix.argon2-version }}
51
+ DEVISE_VERSION: ${{ matrix.devise-version || '~> 4.9' }}
52
+ steps:
53
+ - uses: actions/checkout@v4
54
+ - name: Set up Ruby ${{ matrix.ruby-version }}
55
+ uses: ruby/setup-ruby@v1
56
+ with:
57
+ ruby-version: ${{ matrix.ruby-version }}
58
+ bundler-cache: true
59
+ - uses: supercharge/mongodb-github-action@1.10.0
60
+ if: ${{ matrix.orm.adapter == 'mongoid' }}
61
+ - name: Setup rails test environment
62
+ run: |
63
+ cd spec/rails_app
64
+ RAILS_ENV=test bin/rails db:setup
65
+ - name: Run tests
66
+ run: bundle exec rspec
data/.gitignore CHANGED
@@ -12,6 +12,9 @@ lib/bundler/man
12
12
  pkg
13
13
  rdoc
14
14
  spec/reports
15
+ spec/rails_app/log/*
16
+ spec/rails_app/tmp/*
17
+ spec/rails_app/db/test.sqlite3*
15
18
  test/tmp
16
19
  test/version_tmp
17
20
  tmp
data/CHANGELOG.md ADDED
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ## [2.0.1] - 2023-10-18
6
+
7
+ ### Added
8
+ - Add Argon2 and devise to the test suite
9
+ - Add @moritzhoeppner as an author
10
+
11
+ ### Fixed
12
+ - Fix work factors implementation
13
+
14
+ ## [2.0.0] - 2023-10-16
15
+
16
+ ### Added
17
+ - Expose Argon2 options for configuring hashing work factors
18
+ - Add support for migration v1 hashes
19
+ - Add support for migrating bcrypt hashes
20
+ - Add tests for Mongoid
21
+ - Add Changelog :)
22
+
23
+ ### Changed
24
+ - Change salting / peppering mechanism
25
+ - Change CI from Travis to GitHub Actions
26
+
27
+ ### Removed
28
+ - Remove `devise-encryptable` dependency
29
+ - Remove superflous dependency on devise `password_salt` column
30
+
31
+ Thank you to @moritzhoeppner for the significant contributions to this release!
32
+
data/Gemfile CHANGED
@@ -1,10 +1,15 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- # Specify your gem's dependencies in gvoe_auth-client.gemspec
4
3
  gemspec
5
4
 
6
5
  gem 'rspec'
7
6
  gem 'simplecov'
8
- gem 'activesupport'
9
- gem 'activemodel'
10
- gem 'pry'
7
+ gem 'activerecord'
8
+ gem 'sqlite3'
9
+ gem 'rails', ENV['RAILS_VERSION'] || '~> 7.0'
10
+ gem 'argon2', ENV['ARGON2_VERSION'] || '~> 2.3'
11
+ gem 'devise', ENV['DEVISE_VERSION'] || '~> 4.9'
12
+
13
+ if ENV['ORM'] == 'mongoid'
14
+ gem 'mongoid', ENV['MONGOID_VERSION'] || '~> 7.5'
15
+ end
data/README.md CHANGED
@@ -1,41 +1,125 @@
1
- devise-argon2 [![Build Status](https://secure.travis-ci.org/erdostom/devise-argon2.png)][Continuous Integration] [![Gem Version](https://badge.fury.io/rb/devise-argon2.svg)](https://badge.fury.io/rb/devise-argon2)
2
- =============
1
+ # devise-argon2
2
+ [![Gem Version](https://badge.fury.io/rb/devise-argon2.svg)](https://badge.fury.io/rb/devise-argon2)
3
+ ![](https://github.com/erdostom/devise-argon2/actions/workflows/test.yml/badge.svg)
3
4
 
4
- **A [devise-encryptable] password encryptor that uses [argon2]**
5
+ A ruby gem that gives Devise models which use `database_authenticatable` the ability to hash
6
+ passwords with Argon2id.
5
7
 
6
- * [Continuous Integration]
8
+ ## Installation
7
9
 
8
- [Continuous Integration]: http://travis-ci.org/erdostom/devise-argon2 "Continuous integration @ travis-ci.org"
9
-
10
- [argon2]: https://github.com/technion/ruby-argon2
11
- [devise]: https://github.com/plataformatec/devise
12
- [devise-encryptable]: https://github.com/plataformatec/devise-encryptable
13
-
14
- ## Why use Argon2?
15
-
16
- Argon2 won Password Hashing Competition and offers additional security compared to the default `bcrypt` by adding a memory cost. More info:
17
-
18
- - https://github.com/P-H-C/phc-winner-argon2
19
- - https://hynek.me/articles/storing-passwords/
10
+ ```
11
+ bundle add devise-argon2
12
+ ```
20
13
 
21
14
  ## Usage
22
15
 
23
- Assuming you have [devise] (>= 2.1) and the [devise-encryptable] plugin
24
- set up in your application, add `devise-argon2` to your `Gemfile` and `bundle`:
25
-
26
- gem 'devise-argon2'
16
+ Add `devise :argon2` to your Devise model. For example:
17
+
18
+ ```
19
+ class User < ApplicationRecord
20
+ devise :database_authenticatable, :argon2
21
+ end
22
+ ```
23
+
24
+ Now the password of a newly created user will be hashed with Argon2id. Existing BCrypt hashes will
25
+ continue to work; if the password of a user is hashed with BCrypt, the Argon2id hash will replace
26
+ the existing hash as soon as a user signs in (more specifically: as soon as `valid_password?`
27
+ is called with a valid password).
28
+
29
+ ## Configuration
30
+
31
+ ### Argon2 options
32
+
33
+ For Argon2 hashing the gem [ruby-argon2](https://github.com/technion/ruby-argon2) is used, which
34
+ provides FFI bindings to the
35
+ [Argon 2 reference implementation](https://github.com/P-H-C/phc-winner-argon2).
36
+ `ruby-argon2` can be configured by passing parameters like `profile`, `t_cost`, `m_cost`, `p_cost`,
37
+ or `secret` to `Argon2::Password.new`. These parameters can be set like this:
38
+
39
+ ```
40
+ class User < ApplicationRecord
41
+ devise :database_authenticatable,
42
+ :argon2,
43
+ argon2_options: { t_cost: 3, p_cost: 2 }
44
+ end
45
+ ```
46
+
47
+ If the the configured work factors differ from the work factors of the hash in the database, the
48
+ password will be re-hashed as soon as `valid_password?` is called with a valid password.
49
+
50
+ ### Pepper/secret key
51
+
52
+ The [Argon 2 reference implementation](https://github.com/P-H-C/phc-winner-argon2#library) has a
53
+ built-in pepper which is called `secret`. This Argon2 secret key can be set like this:
54
+
55
+ ```
56
+ class User < ApplicationRecord
57
+ devise :database_authenticatable,
58
+ :argon2,
59
+ argon2_options: { secret: ENV['ARGON2_SECRET_KEY'] }
60
+ end
61
+ ```
62
+
63
+ Traditionally, peppers in Devise are configured by setting `config.pepper` in `devise.rb`. This
64
+ option in honored but `argon2_options[:secret]` takes precedence over `config.pepper`. Specifically:
65
+ - `config.pepper` is used as secret key for new hashes if and only if `argon2_options[:secret]` is
66
+ not set.
67
+ - The verification of existing BCrypt hashes is not touched, so it continues to use `config.pepper`
68
+ as pepper.
69
+
70
+ ## Updating from version 1
71
+
72
+ With version 2 come two major changes: First, `devise-encryptable` is no longer needed. Second, the mechanism for salting
73
+ and peppering has changed: Salts are now managed by Argon2 and the pepper is passed as secret key
74
+ parameter. If you have existing hashes in your database that have been generated by
75
+ devise-argon2 v1, you'll need to set `:migrate_from_devise_argon2_v1` in `argon2_options`.
76
+
77
+ With this option your existing hashes will continue to work as the old mechanism for salting and
78
+ peppering is used if and only if `password_salt` is truthy. The first time you pass a valid
79
+ password to `valid_password?`, the hash will be updated and `password_salt` will be set to `nil`.
80
+ The next time you call `valid_password?` the new salting and peppering mechanism will be used
81
+ because `password_salt` is not truthy anymore.
82
+
83
+ As soon as all `password_salt` fields are set to `nil`, you can delete the column from the database
84
+ and remove `:migrate_from_devise_argon2_v1` from `argon2_options`.
85
+
86
+ Please note that this works only if your database table has a field `password_salt`.
87
+
88
+ ### Upgrade Steps
89
+
90
+ 1. Update your `Gemfile` to use `devise-argon2` version 2: `gem 'devise-argon2', '~> 2.0'`
91
+ 1. Remove `devise-encryptable` from your `Gemfile`
92
+ 1. Run `bundle install`
93
+ 1. Remove the line `config.encryptor = :argon2` from `config/initializers/devise.rb`
94
+ 1. Change your Devise model by removing `:encryptable` and adding `:argon2, argon2_options: { migrate_from_devise_argon2_v1: true }`
95
+ 1. It should now look something like this
96
+
97
+ ```
98
+ class User < ApplicationRecord
99
+ devise :database_authenticatable,
100
+ :argon2,
101
+ argon2_options: { migrate_from_devise_argon2_v1: true }
102
+ end
103
+ ```
104
+
105
+ That's it, you're done! Your users will now be able to log in with their existing passwords and their passwords will be migrated to the V2 format the next time they log in.
106
+
107
+ ---
108
+
109
+ Once all of your users' passwords are migrated to the V2 format:
110
+ 1. Remove the `argon2_options { migrated_from_devise_argon2_v1: true }` line from your Devise model
111
+ 1. Delete the `password_salt` column from your database using a migration like this:
112
+ ```
113
+ class RemovePasswordSaltFromUsers < ActiveRecord::Migration[7.1]
114
+ def change
115
+ remove_column :users, :password_salt, :string
116
+ end
117
+ end
118
+ ```
119
+
120
+ Note: If you do this before all of your users' passwords are migrated to the V2 format, they will be unable to log
121
+ in with their current passwords.
27
122
 
28
- Then open up your [devise] configuration,`config/initializers/devise.rb` and configure your encryptor to be `argon2`:
29
-
30
- # config/initializers/devise.rb
31
- Devise.setup do |config|
32
- # ..
33
- config.encryptor = :argon2
34
- # ...
35
- end
36
-
37
- It is also recommended to uncomment (or add) `config.pepper` with a random
38
- string that will be used in addition to the per-user `password_salt` when hashing.
39
123
 
40
124
  ## Contributing
41
125
 
@@ -45,6 +129,10 @@ string that will be used in addition to the per-user `password_salt` when hashin
45
129
  4. Push to the branch (`git push origin my-new-feature`)
46
130
  5. Create new Pull Request
47
131
 
48
- ## Copyright
132
+ ## Contributors
133
+
134
+ Please see here for full list of contributors: https://github.com/erdostom/devise-argon2/graphs/contributors
135
+
136
+ ## License
49
137
 
50
138
  Released under MIT License.
@@ -1,23 +1,26 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  lib = File.expand_path('../lib', __FILE__)
3
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "devise/encryptable/encryptors/argon2/version"
4
+ require "devise-argon2/version"
5
5
 
6
6
  Gem::Specification.new do |gem|
7
- gem.name = "devise-argon2"
8
- gem.version = Devise::Encryptable::Encryptors::ARGON2_VERSION
9
- gem.authors = ["Tamas Erdos"]
10
- gem.email = ["tamas at tamaserdos com"]
11
- gem.description = %q{A devise-encryptable password encryptor that uses Argon2}
12
- gem.summary = %q{A devise-encryptable password encryptor that uses Argon2}
13
- gem.homepage = "https://github.com/erdostom/devise-argon2"
7
+ gem.name = "devise-argon2"
8
+ gem.version = Devise::Argon2::ARGON2_VERSION
9
+ gem.authors = ["Tamas Erdos", "Moritz Höppner"]
10
+ gem.email = ["tamas at tamaserdos com"]
11
+ gem.description = %q{Enables Devise to hash passwords with Argon2id}
12
+ gem.summary = %q{Enables Devise to hash passwords with Argon2id}
13
+ gem.license = 'MIT'
14
+ gem.homepage = "https://github.com/erdostom/devise-argon2"
14
15
 
15
- gem.files = `git ls-files`.split($/)
16
- gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
- gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
16
+ gem.files = `git ls-files`.split($/)
17
+ gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
18
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
19
  gem.require_paths = ["lib"]
19
20
 
20
- gem.add_dependency 'devise', '>= 2.1.0'
21
- gem.add_dependency 'devise-encryptable', '>= 0.2.0'
22
- gem.add_dependency 'argon2', '~> 2.0'
21
+ gem.add_dependency 'devise', '~> 4.0'
22
+ gem.add_dependency 'argon2', '~> 2.1'
23
+
24
+
25
+ gem.post_install_message = "Version 2 of devise-argon2 introduces breaking changes, please see README.md for details."
23
26
  end
@@ -0,0 +1,101 @@
1
+ require 'argon2'
2
+
3
+ module Devise
4
+ module Models
5
+ module Argon2
6
+ def valid_password?(password)
7
+ is_valid = hash_needs_update = false
8
+
9
+ if ::Argon2::Password.valid_hash?(encrypted_password)
10
+ if migrate_hash_from_devise_argon2_v1?
11
+ is_valid = ::Argon2::Password.verify_password(
12
+ "#{password}#{password_salt}#{self.class.pepper}",
13
+ encrypted_password
14
+ )
15
+ hash_needs_update = true
16
+ else
17
+ argon2_secret = (self.class.argon2_options[:secret] || self.class.pepper)
18
+ is_valid = ::Argon2::Password.verify_password(
19
+ password,
20
+ encrypted_password,
21
+ argon2_secret
22
+ )
23
+ hash_needs_update = outdated_work_factors?
24
+ end
25
+ else
26
+ # Devise models are included in a fixed order, see
27
+ # https://github.com/heartcombo/devise/blob/f6e73e5b5c8f519f4be29ac9069c6ed8a2343ce4/lib/devise/models.rb#L79.
28
+ # Since we don't specify where this model should be inserted when we call add_module,
29
+ # it will be inserted at the end, i.e. after DatabaseAuthenticatable (see
30
+ # https://github.com/heartcombo/devise/blob/f6e73e5b5c8f519f4be29ac9069c6ed8a2343ce4/lib/devise.rb#L393).
31
+ # So we can call DatabaseAuthenticable's valid_password? with super.
32
+ is_valid = super
33
+ hash_needs_update = true
34
+ end
35
+
36
+ update_hash(password) if is_valid && hash_needs_update
37
+
38
+ is_valid
39
+ end
40
+
41
+ protected
42
+
43
+ def password_digest(password)
44
+ hasher_options = self.class.argon2_options.except(:migrate_from_devise_argon2_v1)
45
+ hasher_options[:secret] ||= self.class.pepper
46
+ hasher = ::Argon2::Password.new(hasher_options)
47
+ hasher.create(password)
48
+ end
49
+
50
+ private
51
+
52
+ def update_hash(password)
53
+ attributes = { encrypted_password: password_digest(password) }
54
+ attributes[:password_salt] = nil if migrate_hash_from_devise_argon2_v1?
55
+
56
+ self.assign_attributes(attributes)
57
+ self.save if self.persisted?
58
+ end
59
+
60
+ def outdated_work_factors?
61
+ hash_format = ::Argon2::HashFormat.new(encrypted_password)
62
+ current_work_factors = {
63
+ t_cost: hash_format.t_cost,
64
+ m_cost: hash_format.m_cost,
65
+ p_cost: hash_format.p_cost
66
+ }
67
+ current_work_factors != configured_work_factors
68
+ end
69
+
70
+ def configured_work_factors
71
+ # Since version 2.3.0 the argon2 gem exposes the default work factors via constants, see
72
+ # https://github.com/technion/ruby-argon2/commit/d62ecf8b4ec6b8c1651fade5a5ebdc856e8aef42
73
+ work_factors = {
74
+ t_cost: defined?(::Argon2::Password::DEFAULT_T_COST) ? ::Argon2::Password::DEFAULT_T_COST : 2,
75
+ m_cost: defined?(::Argon2::Password::DEFAULT_M_COST) ? ::Argon2::Password::DEFAULT_M_COST : 16,
76
+ p_cost: defined?(::Argon2::Password::DEFAULT_P_COST) ? ::Argon2::Password::DEFAULT_P_COST : 1
77
+ }.merge(self.class.argon2_options.slice(:t_cost, :m_cost, :p_cost))
78
+
79
+ # Since version 2.3.0 the argon2 gem supports defining work factors with named profiles, see
80
+ # https://github.com/technion/ruby-argon2/commit/6312a8fb3a6c6c5e771a736572e63d47485e8613
81
+ if self.class.argon2_options[:profile] && defined?(::Argon2::Profiles)
82
+ work_factors.merge!(::Argon2::Profiles[self.class.argon2_options[:profile]])
83
+ end
84
+
85
+ work_factors[:m_cost] = (1 << work_factors[:m_cost])
86
+
87
+ work_factors
88
+ end
89
+
90
+ def migrate_hash_from_devise_argon2_v1?
91
+ self.class.argon2_options[:migrate_from_devise_argon2_v1] &&
92
+ defined?(password_salt) &&
93
+ password_salt
94
+ end
95
+
96
+ module ClassMethods
97
+ Devise::Models.config(self, :argon2_options)
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,5 @@
1
+ module Devise
2
+ module Argon2
3
+ ARGON2_VERSION = '2.0.1'
4
+ end
5
+ end
data/lib/devise-argon2.rb CHANGED
@@ -1,4 +1,9 @@
1
1
  require 'devise'
2
- require 'devise-encryptable'
3
- require "devise/encryptable/encryptors/argon2/version"
4
- require "devise/encryptable/encryptors/argon2"
2
+ require 'devise-argon2/version'
3
+
4
+ module Devise
5
+ add_module(:argon2, :model => 'devise-argon2/model')
6
+
7
+ mattr_accessor :argon2_options
8
+ @@argon2_options = {}
9
+ end