From abf8ec0dfc6aa962d0a45c23a7f8fa12da76fcb5 Mon Sep 17 00:00:00 2001 From: Drew Blessing <drew@gitlab.com> Date: Mon, 20 Jun 2022 10:46:14 -0500 Subject: [PATCH] Vendor new gem to support PBKDF2+SHA512 password hashing New gem based on devise-encryptable that supports PBKDF2+SHA512. In the future this gem may be released individually, or contributed upstream to devise-encryptable. --- .gitlab/ci/rules.gitlab-ci.yml | 6 + .gitlab/ci/vendored-gems.gitlab-ci.yml | 8 + Gemfile | 2 + Gemfile.lock | 7 + .../devise-pbkdf2-encryptable/.gitlab-ci.yml | 26 +++ vendor/gems/devise-pbkdf2-encryptable/Gemfile | 5 + .../devise-pbkdf2-encryptable/Gemfile.lock | 104 +++++++++ vendor/gems/devise-pbkdf2-encryptable/LICENSE | 201 ++++++++++++++++++ .../devise-pbkdf2-encryptable.gemspec | 25 +++ .../lib/devise-pbkdf2-encryptable.rb | 1 + .../devise/pbkdf2_encryptable/encryptable.rb | 16 ++ .../pbkdf2_encryptable/encryptors/base.rb | 52 +++++ .../encryptors/pbkdf2_sha512.rb | 41 ++++ .../lib/devise/pbkdf2_encryptable/model.rb | 77 +++++++ .../encryptors/pbkdf2_sha512_spec.rb | 80 +++++++ .../spec/lib/pbkdf2_encryptable/model_spec.rb | 127 +++++++++++ .../spec/spec_helper.rb | 4 + 17 files changed, 782 insertions(+) create mode 100644 vendor/gems/devise-pbkdf2-encryptable/.gitlab-ci.yml create mode 100644 vendor/gems/devise-pbkdf2-encryptable/Gemfile create mode 100644 vendor/gems/devise-pbkdf2-encryptable/Gemfile.lock create mode 100644 vendor/gems/devise-pbkdf2-encryptable/LICENSE create mode 100644 vendor/gems/devise-pbkdf2-encryptable/devise-pbkdf2-encryptable.gemspec create mode 100644 vendor/gems/devise-pbkdf2-encryptable/lib/devise-pbkdf2-encryptable.rb create mode 100644 vendor/gems/devise-pbkdf2-encryptable/lib/devise/pbkdf2_encryptable/encryptable.rb create mode 100644 vendor/gems/devise-pbkdf2-encryptable/lib/devise/pbkdf2_encryptable/encryptors/base.rb create mode 100644 vendor/gems/devise-pbkdf2-encryptable/lib/devise/pbkdf2_encryptable/encryptors/pbkdf2_sha512.rb create mode 100644 vendor/gems/devise-pbkdf2-encryptable/lib/devise/pbkdf2_encryptable/model.rb create mode 100644 vendor/gems/devise-pbkdf2-encryptable/spec/lib/pbkdf2_encryptable/encryptors/pbkdf2_sha512_spec.rb create mode 100644 vendor/gems/devise-pbkdf2-encryptable/spec/lib/pbkdf2_encryptable/model_spec.rb create mode 100644 vendor/gems/devise-pbkdf2-encryptable/spec/spec_helper.rb diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index e188475485bef..a2dd646d89513 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -1507,6 +1507,12 @@ changes: ["vendor/gems/omniauth-gitlab/**/*"] - <<: *if-merge-request-labels-run-all-rspec +.vendor:rules:devise-pbkdf2-encryptable: + rules: + - <<: *if-merge-request + changes: ["vendor/gems/devise-pbkdf2-encryptable/**/*"] + - <<: *if-merge-request-labels-run-all-rspec + ################## # Releases rules # ################## diff --git a/.gitlab/ci/vendored-gems.gitlab-ci.yml b/.gitlab/ci/vendored-gems.gitlab-ci.yml index 6dd6e19a81860..1f89186ed946f 100644 --- a/.gitlab/ci/vendored-gems.gitlab-ci.yml +++ b/.gitlab/ci/vendored-gems.gitlab-ci.yml @@ -21,3 +21,11 @@ vendor omniauth-gitlab: trigger: include: vendor/gems/omniauth-gitlab/.gitlab-ci.yml strategy: depend + +vendor devise-pbkdf2-encryptable: + extends: + - .vendor:rules:devise-pbkdf2-encryptable + needs: [] + trigger: + include: vendor/gems/devise-pbkdf2-encryptable/.gitlab-ci.yml + strategy: depend diff --git a/Gemfile b/Gemfile index 52cd1bd953d5a..81791a3edab52 100644 --- a/Gemfile +++ b/Gemfile @@ -30,6 +30,8 @@ gem 'declarative_policy', '~> 1.1.0' # Authentication libraries gem 'devise', '~> 4.7.2' +gem 'devise-pbkdf2-encryptable', '~> 0.0.0', path: 'vendor/gems/devise-pbkdf2-encryptable' + gem 'bcrypt', '~> 3.1', '>= 3.1.14' gem 'doorkeeper', '~> 5.5.0.rc2' gem 'doorkeeper-openid_connect', '~> 1.7.5' diff --git a/Gemfile.lock b/Gemfile.lock index 6f6b98c0876ee..d0add73fd52aa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,6 +4,12 @@ PATH error_tracking_open_api (1.0.0) typhoeus (~> 1.0, >= 1.0.1) +PATH + remote: vendor/gems/devise-pbkdf2-encryptable + specs: + devise-pbkdf2-encryptable (0.0.0) + devise (~> 4.0) + PATH remote: vendor/gems/ipynbdiff specs: @@ -1514,6 +1520,7 @@ DEPENDENCIES derailed_benchmarks device_detector devise (~> 4.7.2) + devise-pbkdf2-encryptable (~> 0.0.0)! devise-two-factor (~> 4.0.2) diff_match_patch (~> 0.1.0) diffy (~> 3.3) diff --git a/vendor/gems/devise-pbkdf2-encryptable/.gitlab-ci.yml b/vendor/gems/devise-pbkdf2-encryptable/.gitlab-ci.yml new file mode 100644 index 0000000000000..a251795317829 --- /dev/null +++ b/vendor/gems/devise-pbkdf2-encryptable/.gitlab-ci.yml @@ -0,0 +1,26 @@ +workflow: + rules: + - if: $CI_MERGE_REQUEST_ID + +.rspec: + cache: + key: devise-pbkdf2-encryptable + paths: + - vendor/gems/devise-pbkdf2-encryptable/vendor/ruby + before_script: + - cd vendor/gems/devise-pbkdf2-encryptable + - ruby -v # Print out ruby version for debugging + - gem install bundler --no-document # Bundler is not installed with the image + - bundle config set --local path 'vendor' # Install dependencies into ./vendor/ruby + - bundle config set with 'development' + - bundle install -j $(nproc) + script: + - bundle exec rspec + +rspec-2.7: + image: "ruby:2.7" + extends: .rspec + +rspec-3.0: + image: "ruby:3.0" + extends: .rspec diff --git a/vendor/gems/devise-pbkdf2-encryptable/Gemfile b/vendor/gems/devise-pbkdf2-encryptable/Gemfile new file mode 100644 index 0000000000000..be173b205f701 --- /dev/null +++ b/vendor/gems/devise-pbkdf2-encryptable/Gemfile @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gemspec diff --git a/vendor/gems/devise-pbkdf2-encryptable/Gemfile.lock b/vendor/gems/devise-pbkdf2-encryptable/Gemfile.lock new file mode 100644 index 0000000000000..3be824244ad85 --- /dev/null +++ b/vendor/gems/devise-pbkdf2-encryptable/Gemfile.lock @@ -0,0 +1,104 @@ +PATH + remote: . + specs: + devise-pbkdf2-encryptable (0.0.0) + devise (~> 4.0) + +GEM + remote: https://rubygems.org/ + specs: + actionpack (6.1.6) + actionview (= 6.1.6) + activesupport (= 6.1.6) + rack (~> 2.0, >= 2.0.9) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actionview (6.1.6) + activesupport (= 6.1.6) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + activemodel (6.1.6) + activesupport (= 6.1.6) + activesupport (6.1.6) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + zeitwerk (~> 2.3) + bcrypt (3.1.18) + builder (3.2.4) + concurrent-ruby (1.1.10) + crass (1.0.6) + devise (4.8.1) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 4.1.0) + responders + warden (~> 1.2.3) + diff-lcs (1.5.0) + erubi (1.10.0) + i18n (1.10.0) + concurrent-ruby (~> 1.0) + loofah (2.18.0) + crass (~> 1.0.2) + nokogiri (>= 1.5.9) + method_source (1.0.0) + minitest (5.16.0) + nokogiri (1.13.6-arm64-darwin) + racc (~> 1.4) + nokogiri (1.13.6-x86_64-linux) + racc (~> 1.4) + orm_adapter (0.5.0) + racc (1.6.0) + rack (2.2.3.1) + rack-test (1.1.0) + rack (>= 1.0, < 3) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) + rails-html-sanitizer (1.4.3) + loofah (~> 2.3) + railties (6.1.6) + actionpack (= 6.1.6) + activesupport (= 6.1.6) + method_source + rake (>= 12.2) + thor (~> 1.0) + rake (13.0.6) + responders (3.0.1) + actionpack (>= 5.0) + railties (>= 5.0) + rspec (3.10.0) + rspec-core (~> 3.10.0) + rspec-expectations (~> 3.10.0) + rspec-mocks (~> 3.10.0) + rspec-core (3.10.2) + rspec-support (~> 3.10.0) + rspec-expectations (3.10.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.10.0) + rspec-mocks (3.10.3) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.10.0) + rspec-support (3.10.3) + thor (1.2.1) + tzinfo (2.0.4) + concurrent-ruby (~> 1.0) + warden (1.2.9) + rack (>= 2.0.9) + zeitwerk (2.6.0) + +PLATFORMS + arm64-darwin-21 + x86_64-linux + +DEPENDENCIES + activemodel (~> 6.1, < 8) + devise-pbkdf2-encryptable! + rspec (~> 3.10.0) + +BUNDLED WITH + 2.3.16 diff --git a/vendor/gems/devise-pbkdf2-encryptable/LICENSE b/vendor/gems/devise-pbkdf2-encryptable/LICENSE new file mode 100644 index 0000000000000..d5a25c59027f0 --- /dev/null +++ b/vendor/gems/devise-pbkdf2-encryptable/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2012-2015 Plataformatec (opensource@plataformatec.com.br) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/vendor/gems/devise-pbkdf2-encryptable/devise-pbkdf2-encryptable.gemspec b/vendor/gems/devise-pbkdf2-encryptable/devise-pbkdf2-encryptable.gemspec new file mode 100644 index 0000000000000..e507633c0bfff --- /dev/null +++ b/vendor/gems/devise-pbkdf2-encryptable/devise-pbkdf2-encryptable.gemspec @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +lib = File.expand_path('lib', __dir__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) + +Gem::Specification.new do |spec| + spec.name = 'devise-pbkdf2-encryptable' + spec.authors = ['Drew Blessing'] + spec.email = ['drew@gitlab.com'] + + spec.summary = 'Extension that allows Devise to use PBKDF2 password hashing' + spec.homepage = 'https://gitlab.com/gitlab-org/gitlab/-/tree/master/vendor/gems/devise-pbkdf2-encryptable' + spec.metadata = { 'source_code_uri' => 'https://gitlab.com/gitlab-org/gitlab/-/tree/master/vendor/gems/devise-pbkdf2-encryptable' } + spec.license = 'Apache-2.0' + + spec.files = Dir['lib/**/*.rb'] + spec.require_paths = ['lib'] + + spec.version = '0.0.0' + + spec.add_runtime_dependency 'devise', '~> 4.0' + + spec.add_development_dependency 'activemodel', '~> 6.1', '< 8' + spec.add_development_dependency 'rspec', '~> 3.10.0' +end diff --git a/vendor/gems/devise-pbkdf2-encryptable/lib/devise-pbkdf2-encryptable.rb b/vendor/gems/devise-pbkdf2-encryptable/lib/devise-pbkdf2-encryptable.rb new file mode 100644 index 0000000000000..4db9b82052f20 --- /dev/null +++ b/vendor/gems/devise-pbkdf2-encryptable/lib/devise-pbkdf2-encryptable.rb @@ -0,0 +1 @@ +require "devise/pbkdf2_encryptable/encryptable" diff --git a/vendor/gems/devise-pbkdf2-encryptable/lib/devise/pbkdf2_encryptable/encryptable.rb b/vendor/gems/devise-pbkdf2-encryptable/lib/devise/pbkdf2_encryptable/encryptable.rb new file mode 100644 index 0000000000000..027032f46c121 --- /dev/null +++ b/vendor/gems/devise-pbkdf2-encryptable/lib/devise/pbkdf2_encryptable/encryptable.rb @@ -0,0 +1,16 @@ +module Devise + # Used to define the password encryption algorithm. + mattr_accessor :encryptor + @@encryptor = nil + + module Pbkdf2Encryptable + module Encryptors + InvalidHash = Class.new(StandardError) + + autoload :Base, 'devise/pbkdf2_encryptable/encryptors/base' + autoload :Pbkdf2Sha512, 'devise/pbkdf2_encryptable/encryptors/pbkdf2_sha512' + end + end +end + +Devise.add_module(:pbkdf2_encryptable, :model => 'devise/pbkdf2_encryptable/model') diff --git a/vendor/gems/devise-pbkdf2-encryptable/lib/devise/pbkdf2_encryptable/encryptors/base.rb b/vendor/gems/devise-pbkdf2-encryptable/lib/devise/pbkdf2_encryptable/encryptors/base.rb new file mode 100644 index 0000000000000..970be7f51a6c6 --- /dev/null +++ b/vendor/gems/devise-pbkdf2-encryptable/lib/devise/pbkdf2_encryptable/encryptors/base.rb @@ -0,0 +1,52 @@ +module Devise + module Pbkdf2Encryptable + module Encryptors + class Base + def self.split_digest(hash) + split_digest = hash.split('$') + _, strategy, stretches, salt, checksum = split_digest + + unless split_digest.length == 5 && strategy.start_with?('pbkdf2-') + raise InvalidHash.new('invalid PBKDF2 hash') + end + + { strategy: strategy, stretches: stretches.to_i, + salt: passlib_decode64(salt), checksum: passlib_decode64(checksum) } + end + + # Passlib-style Base64 encoding: + # - Replaces '+' with '.' + # - Strips trailing newline and '==' + private_class_method def self.passlib_encode64(value) + Base64.strict_encode64([value].pack('H*')).tr('+', '.').delete('=') + end + + private_class_method def self.passlib_decode64(value) + enc = value.tr('.', '+') + Base64.decode64(enc).unpack1('H*') + end + + # Passlib-style hash: $pbkdf2-sha512$rounds$salt$checksum + # where salt and checksum are "adapted" Base64 encoded + private_class_method def self.format_hash(strategy, stretches, salt, checksum) + encoded_salt = passlib_encode64(salt) + encoded_checksum = passlib_encode64(checksum) + + "$#{strategy}$#{stretches}$#{encoded_salt}$#{encoded_checksum}" + end + + private_class_method def self.pbkdf2_checksum(hash, password, stretches, salt) + raise 'Stretches must be greater than zero' unless stretches.to_i > 0 + + OpenSSL::KDF.pbkdf2_hmac( + password.to_s, + salt: [salt].pack("H*"), + iterations: stretches, + hash: hash, + length: hash.digest_length + ).unpack1('H*') + end + end + end + end +end diff --git a/vendor/gems/devise-pbkdf2-encryptable/lib/devise/pbkdf2_encryptable/encryptors/pbkdf2_sha512.rb b/vendor/gems/devise-pbkdf2-encryptable/lib/devise/pbkdf2_encryptable/encryptors/pbkdf2_sha512.rb new file mode 100644 index 0000000000000..3fedc724c06e8 --- /dev/null +++ b/vendor/gems/devise-pbkdf2-encryptable/lib/devise/pbkdf2_encryptable/encryptors/pbkdf2_sha512.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Devise + module Pbkdf2Encryptable + module Encryptors + class Pbkdf2Sha512 < Base + STRATEGY = 'pbkdf2-sha512' + STRETCHES = 20_000 + + def self.compare(encrypted_password, password) + split_digest = self.split_digest(encrypted_password) + value_to_test = self.sha512_checksum(password, split_digest[:stretches], split_digest[:salt]) + + Devise.secure_compare(split_digest[:checksum], value_to_test) + end + + def self.digest(password, stretches, salt) + checksum = sha512_checksum(password, stretches, salt) + + format_hash(STRATEGY, stretches, salt, checksum) + end + + def self.split_digest(hash) + split_digest = super + + unless split_digest[:strategy] == STRATEGY + raise InvalidHash.new('invalid PBKDF2 SHA512 hash') + end + + split_digest + end + + private_class_method def self.sha512_checksum(password, stretches, salt) + hash = OpenSSL::Digest.new('SHA512') + + pbkdf2_checksum(hash, password, stretches, salt) + end + end + end + end +end diff --git a/vendor/gems/devise-pbkdf2-encryptable/lib/devise/pbkdf2_encryptable/model.rb b/vendor/gems/devise-pbkdf2-encryptable/lib/devise/pbkdf2_encryptable/model.rb new file mode 100644 index 0000000000000..06430b26db963 --- /dev/null +++ b/vendor/gems/devise-pbkdf2-encryptable/lib/devise/pbkdf2_encryptable/model.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'devise/strategies/database_authenticatable' + +# Based on `devise-encryptable` Encryptable model +# https://github.com/heartcombo/devise-encryptable/blob/main/lib/devise/encryptable/model.rb +module Devise + module Models + module Pbkdf2Encryptable + extend ActiveSupport::Concern + + def valid_password?(password) + encryptor_class.compare(encrypted_password, password) + end + + def password_strategy + split_encrypted_password[:strategy]&.tr('-', '_')&.to_sym + end + + def password_salt + split_encrypted_password[:salt] + end + + # Used by warden and other modules where there is a + # need for a random token based on the user password. + alias_method :authenticatable_salt, :password_salt + + def password_stretches + split_encrypted_password[:stretches] + end + + def password_checksum + split_encrypted_password[:checksum] + end + + protected + + # Used by Devise DatabaseAuthenticatable when setting a password + def password_digest(password) + remove_instance_variable('@split_encrypted_password') if defined?(@split_encrypted_password) + + encryptor_class.digest(password, encryptor_class::STRETCHES, Devise.friendly_token[0, 16]) + end + + def encryptor_class + self.class.encryptor_class + end + + private + + def split_encrypted_password + return {} unless encrypted_password.present? + return @split_encrypted_password if defined?(@split_encrypted_password) + + @split_encrypted_password = encryptor_class.split_digest(encrypted_password) + end + + module ClassMethods + Devise::Models.config(self, :encryptor) + + # Returns the class for the configured encryptor. + def encryptor_class + @encryptor_class ||= case encryptor + when :bcrypt + raise "In order to use bcrypt as encryptor, simply remove :pbkdf2_encryptable from your devise model" + when nil + raise "You need to specify an :encryptor in Devise configuration in order to use :pbkdf2_encryptable" + else + Devise::Pbkdf2Encryptable::Encryptors.const_get(encryptor.to_s.classify) + end + rescue NameError + raise "Configured encryptor '#{encryptor.to_sym}' could not be found for pbkdf2_encryptable" + end + end + end + end +end diff --git a/vendor/gems/devise-pbkdf2-encryptable/spec/lib/pbkdf2_encryptable/encryptors/pbkdf2_sha512_spec.rb b/vendor/gems/devise-pbkdf2-encryptable/spec/lib/pbkdf2_encryptable/encryptors/pbkdf2_sha512_spec.rb new file mode 100644 index 0000000000000..3a9b692db6e7b --- /dev/null +++ b/vendor/gems/devise-pbkdf2-encryptable/spec/lib/pbkdf2_encryptable/encryptors/pbkdf2_sha512_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512 do + let(:bcrypt_hash) { '$2a$12$ftnD4XSrhVdlEaEgCn/lxO0Pt3QplwgblmhCug3nSeRhh5a9UDBWK' } + let(:pbkdf2_sha512_hash) { '$pbkdf2-sha512$20000$boHGAw0hEyI$DBA67J7zNZebyzLtLk2X9wRDbmj1LNKVGnZLYyz6PGrIDGIl45fl/BPH0y1TPZnV90A20i.fD9C3G9Bp8jzzOA' } + + describe '.compare' do + subject(:compare) { described_class.compare(encrypted_password, password) } + + context 'with a PBKDF2+SHA512 encrypted password' do + let(:encrypted_password) { pbkdf2_sha512_hash } + + context 'with a matching password' do + let(:password) { 'password' } + + it { is_expected.to eq(true) } + end + + context 'with an incorrect password' do + let(:password) { 'other_password' } + + it { is_expected.to eq(false) } + end + end + + context 'with a non PBKDF2+SHA512 encrypted password' do + let(:encrypted_password) { bcrypt_hash } + let(:password) { 'password' } + + it 'raises an invalid hash error ' do + expect { compare }.to raise_error Devise::Pbkdf2Encryptable::Encryptors::InvalidHash, 'invalid PBKDF2 hash' + end + end + end + + describe '.digest' do + it 'returns a properly formatted and correct hash' do + expect(described_class.digest('password', 20000, '6e81c6030d211322')).to eq(pbkdf2_sha512_hash) + end + + it 'raises an error if stretches is not greater than 0' do + expect { described_class.digest('password', 0, '6e81c6030d211322') } + .to raise_error('Stretches must be greater than zero') + end + end + + describe '.split_digest' do + subject(:split_digest) { described_class.split_digest(digest) } + + context 'with a PBKDF2+SHA512 digest' do + let(:digest) { pbkdf2_sha512_hash } + + it { is_expected.to eq({ + strategy: 'pbkdf2-sha512', + stretches: 20000, + salt: '6e81c6030d211322', + checksum: '0c103aec9ef335979bcb32ed2e4d97f704436e68f52cd2951a764b632cfa3c6ac80c6225e397e5fc13c7d32d533d99d5f74036d22f9f0fd0b71bd069f23cf338' + }) + } + end + + context 'with a PBKDF2+SHA256 digest' do + let(:digest) { '$pbkdf2-sha256$6400$.6UI/S.nXIk8jcbdHx3Fhg$98jZicV16ODfEsEZeYPGHU3kbrUrvUEXOPimVSQDD44' } + + it 'raises invalid hash error' do + expect { split_digest }.to raise_error Devise::Pbkdf2Encryptable::Encryptors::InvalidHash, 'invalid PBKDF2 SHA512 hash' + end + end + + context 'with a BCrypt digest' do + let(:digest) { bcrypt_hash } + + it 'raises invalid hash error' do + expect { split_digest }.to raise_error Devise::Pbkdf2Encryptable::Encryptors::InvalidHash, 'invalid PBKDF2 hash' + end + end + end +end diff --git a/vendor/gems/devise-pbkdf2-encryptable/spec/lib/pbkdf2_encryptable/model_spec.rb b/vendor/gems/devise-pbkdf2-encryptable/spec/lib/pbkdf2_encryptable/model_spec.rb new file mode 100644 index 0000000000000..ebb94c1d68c9b --- /dev/null +++ b/vendor/gems/devise-pbkdf2-encryptable/spec/lib/pbkdf2_encryptable/model_spec.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'active_model' + +RSpec.describe Devise::Models::Pbkdf2Encryptable do + let(:unconfigured_model) do + Class.new do + include ActiveModel::Model + extend ActiveModel::Callbacks + include ActiveModel::Validations::Callbacks + extend Devise::Models + + define_model_callbacks :update, only: :after + + devise :database_authenticatable, :pbkdf2_encryptable + + attr_accessor :encrypted_password + + def initialize(encrypted_password: '') + self.encrypted_password = encrypted_password + end + end + end + + let(:bcrypt_configured_model) do + Class.new(unconfigured_model) do + devise :database_authenticatable, :pbkdf2_encryptable, encryptor: :bcrypt + end + end + + let(:unknown_configured_model) do + Class.new(unconfigured_model) do + devise :database_authenticatable, :pbkdf2_encryptable, encryptor: :sha512 + end + end + + let(:configured_model) do + Class.new(unconfigured_model) do + devise :database_authenticatable, :pbkdf2_encryptable, encryptor: :pbkdf2_sha512 + end + end + + let(:user) { configured_model.new } + let(:pbkdf2_sha512_hash) { '$pbkdf2-sha512$20000$boHGAw0hEyI$DBA67J7zNZebyzLtLk2X9wRDbmj1LNKVGnZLYyz6PGrIDGIl45fl/BPH0y1TPZnV90A20i.fD9C3G9Bp8jzzOA' } + + describe '#valid_password?' do + let(:user) { configured_model.new(encrypted_password: pbkdf2_sha512_hash) } + + it 'validates a correct password' do + expect(user.valid_password?('password')).to eq(true) + end + + it 'does not validate an incorrect password' do + expect(user.valid_password?('other_password')).to eq(false) + end + end + + describe '#password=' do + it 'sets the correct encrypted_password value', :aggregate_failures do + expect(user.encrypted_password).to be_empty + + user.password = 'password' + + expect(user.encrypted_password).to start_with('$pbkdf2-sha512$') + end + + it 'clears split digest memoization' do + user.encrypted_password = '$pbkdf2-sha512$1000$boHGAw0hEyI$DBA67J7zNZebyzLtLk2X9wRDbmj1LNKVGnZLYyz6PGrIDGIl45fl/BPH0y1TPZnV90A20i.fD9C3G9Bp8jzzOA' + + expect(user.password_stretches).to eq(1_000) + + user.password = 'other_password' + + expect(user.password_stretches).to eq(20_000) + end + end + + describe 'password_* methods' do + let(:user) { configured_model.new(encrypted_password: encrypted_password) } + + context 'with a PBKDF2+SHA512 encrypted password' do + let(:encrypted_password) { pbkdf2_sha512_hash } + + it 'extracts the correct split hash values', :aggregate_failures do + expect(Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512).to receive(:split_digest).once.and_call_original + + expect(user.password_strategy).to eq(:pbkdf2_sha512) + expect(user.password_salt).to eq('6e81c6030d211322') + expect(user.password_stretches).to eq(20_000) + expect(user.password_checksum).to eq('0c103aec9ef335979bcb32ed2e4d97f704436e68f52cd2951a764b632cfa3c6ac80c6225e397e5fc13c7d32d533d99d5f74036d22f9f0fd0b71bd069f23cf338') + end + end + + context 'with a BCrypt encrypted password' do + let(:encrypted_password) { '$2a$10$xLTxCKOa75IU4RQGqqOrTuZOgZdJEzfSzjG6ZSEi/C31TB/yLZYpi' } + + it 'raises errors', :aggregate_failures do + expect { user.password_strategy }.to raise_error Devise::Pbkdf2Encryptable::Encryptors::InvalidHash + expect { user.password_salt }.to raise_error Devise::Pbkdf2Encryptable::Encryptors::InvalidHash + expect { user.password_stretches }.to raise_error Devise::Pbkdf2Encryptable::Encryptors::InvalidHash + expect { user.password_checksum }.to raise_error Devise::Pbkdf2Encryptable::Encryptors::InvalidHash + end + end + end + + describe '.encryptor_class' do + it 'returns a class when configured' do + expect(configured_model.encryptor_class).to eq(Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512) + end + + it 'raises an error when unconfigured' do + expect { unconfigured_model.encryptor_class } + .to raise_error('You need to specify an :encryptor in Devise configuration in order to use :pbkdf2_encryptable') + end + + it 'raises an error when BCrypt is configured' do + expect { bcrypt_configured_model.encryptor_class } + .to raise_error('In order to use bcrypt as encryptor, simply remove :pbkdf2_encryptable from your devise model') + end + + it 'raises an error when a class cannot be found' do + expect { unknown_configured_model.encryptor_class } + .to raise_error("Configured encryptor 'sha512' could not be found for pbkdf2_encryptable") + end + end +end diff --git a/vendor/gems/devise-pbkdf2-encryptable/spec/spec_helper.rb b/vendor/gems/devise-pbkdf2-encryptable/spec/spec_helper.rb new file mode 100644 index 0000000000000..ced0cf6fdab87 --- /dev/null +++ b/vendor/gems/devise-pbkdf2-encryptable/spec/spec_helper.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +require 'devise' +require 'devise/pbkdf2_encryptable/encryptable' -- GitLab