Module: EncryptedUserPassword

Extended by:
ActiveSupport::Concern
Included in:
User
Defined in:
app/models/concerns/encrypted_user_password.rb

Overview

Support for both BCrypt and PBKDF2+SHA512 user passwords Meant to be used exclusively with User model but extracted to a concern for isolation and clarity.

Constant Summary collapse

BCRYPT_PREFIX =
'$2a$'
PBKDF2_SHA512_PREFIX =
'$pbkdf2-sha512$'
PBKDF2_SALT_LENGTH =
64
BCRYPT_STRATEGY =
:bcrypt
PBKDF2_SHA512_STRATEGY =
:pbkdf2_sha512

Instance Method Summary collapse

Instance Method Details

#authenticatable_saltObject

Use Devise DatabaseAuthenticatable#authenticatable_salt unless encrypted password is PBKDF2+SHA512.



18
19
20
21
22
# File 'app/models/concerns/encrypted_user_password.rb', line 18

def authenticatable_salt
  return super unless pbkdf2_password?

  Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512.split_digest(encrypted_password)[:salt]
end

#migrated_password?Boolean

Returns:

  • (Boolean)


54
55
56
57
58
59
60
61
62
63
64
# File 'app/models/concerns/encrypted_user_password.rb', line 54

def migrated_password?
  return false if password_strategy != encryptor

  if BCRYPT_STRATEGY == password_strategy
    return true if bcrypt_password_matches_current_stretches?
  elsif PBKDF2_SHA512_STRATEGY == password_strategy
    return true if pbkdf2_password_matches_salt_length?
  end

  false
end

#password=(new_password) ⇒ Object



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'app/models/concerns/encrypted_user_password.rb', line 35

def password=(new_password)
  @password = new_password # rubocop:disable Gitlab/ModuleWithInstanceVariables
  return unless new_password.present?

  return unless new_password.to_s.length >= ApplicationSetting::DEFAULT_MINIMUM_PASSWORD_LENGTH

  # Use SafeRequestStore to cache the password hash during registration
  # This prevents redundant bcrypt operations when the same password is set on multiple
  # User objects during registration. Our analysis showed that two separate User objects
  # are created during registration (one by BuildService and another by Devise), and
  # both trigger expensive password hashing operations. By caching within the request,
  # we reduce registration time by ~31% while maintaining security.
  hash_key = "password_hash:#{Digest::SHA256.hexdigest(new_password.to_s)}"

  self.encrypted_password = Gitlab::SafeRequestStore.fetch(hash_key) do
    hash_this_password(new_password)
  end
end

#valid_password?(password) ⇒ Boolean

Called by Devise during database authentication. Also migrates the user password to the configured encryption type (BCrypt or PBKDF2+SHA512), if needed.

Returns:

  • (Boolean)


27
28
29
30
31
32
33
# File 'app/models/concerns/encrypted_user_password.rb', line 27

def valid_password?(password)
  # On Ubuntu 22.04 FIPS, attempting to hash a password < 8 bytes results in PKCS5_PBKDF2_HMAC: invalid key length
  return false unless password.length >= ApplicationSetting::DEFAULT_MINIMUM_PASSWORD_LENGTH
  return false unless password_matches?(password)

  migrate_password!(password)
end