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