devise-argon2 1.1.0 → 2.0.1

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.
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