Class: ApiKeys::Services::Digestor

Inherits:
Object
  • Object
show all
Defined in:
lib/api_keys/services/digestor.rb

Overview

Handles hashing (digesting) and verifying tokens based on configured strategy.

Constant Summary collapse

BCRYPT_MAX_SECRET_BYTESIZE =
if BCrypt::Engine.const_defined?(:MAX_SECRET_BYTESIZE)
  BCrypt::Engine::MAX_SECRET_BYTESIZE
else
  72
end
BCRYPT_MAX_SAFE_COST =

Costs above this value can turn a malformed/imported database row into a multi-second (or worse) CPU denial of service during authentication. The default bcrypt cost is comfortably below this ceiling.

16
MAX_TOKEN_BYTESIZE =
512

Class Method Summary collapse

Class Method Details

.digest(token:, strategy: ApiKeys.configuration.hash_strategy) ⇒ Hash

Creates a digest of the given token using the configured strategy.

Parameters:

  • token (String)

    The plaintext token.

  • strategy (Symbol) (defaults to: ApiKeys.configuration.hash_strategy)

    The hashing strategy (:bcrypt or :sha256).

Returns:

  • (Hash)

    A hash containing the digest and the algorithm used. e.g., { digest: "...", algorithm: "bcrypt" }



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/api_keys/services/digestor.rb', line 27

def self.digest(token:, strategy: ApiKeys.configuration.hash_strategy)
  validate_token!(token)

  case strategy
  when :bcrypt
    if token.bytesize > BCRYPT_MAX_SECRET_BYTESIZE
      raise ArgumentError,
        "BCrypt tokens must not exceed #{BCRYPT_MAX_SECRET_BYTESIZE} bytes because BCrypt truncates longer inputs."
    end

    unless safe_bcrypt_cost?(BCrypt::Engine.cost)
      raise ArgumentError,
        "BCrypt cost must be between #{BCrypt::Engine::MIN_COST} and #{BCRYPT_MAX_SAFE_COST}."
    end

    # BCrypt handles salt generation internally
    digest = BCrypt::Password.create(token, cost: BCrypt::Engine.cost)
    { digest: digest.to_s, algorithm: "bcrypt" }
  when :sha256
    # Note: Simple SHA256 without salt/pepper. Consider enhancing if needed.
    # BCrypt is generally preferred for password/token hashing.
    digest = Digest::SHA256.hexdigest(token)
    { digest: digest, algorithm: "sha256" }
  else
    raise ArgumentError, "Unsupported hash strategy: #{strategy}. Use :bcrypt or :sha256."
  end
end

.match?(token:, stored_digest:, strategy: ApiKeys.configuration.hash_strategy, comparison_proc: ApiKeys.configuration.secure_compare_proc) ⇒ Boolean

Securely compares a plaintext token against a stored digest. Uses the configured secure comparison proc and hash strategy.

Parameters:

  • token (String)

    The plaintext token provided by the user/client.

  • stored_digest (String)

    The hashed digest stored in the database.

  • strategy (Symbol) (defaults to: ApiKeys.configuration.hash_strategy)

    The hashing strategy used to create the stored_digest.

  • comparison_proc (Proc) (defaults to: ApiKeys.configuration.secure_compare_proc)

    The secure comparison function.

Returns:

  • (Boolean)

    True if the token matches the digest, false otherwise.



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/api_keys/services/digestor.rb', line 63

def self.match?(token:, stored_digest:, strategy: ApiKeys.configuration.hash_strategy, comparison_proc: ApiKeys.configuration.secure_compare_proc)
  return false unless valid_match_inputs?(token, stored_digest)

  case strategy
  when :bcrypt
    return false if token.bytesize > BCRYPT_MAX_SECRET_BYTESIZE

    bcrypt_object = validated_bcrypt_password(stored_digest)
    return false unless bcrypt_object

    # BCrypt's `==` operator is designed for secure comparison.
    bcrypt_object == token
  when :sha256
    # Directly compare the SHA256 hash of the input token with the stored digest
    return false unless stored_digest.match?(/\A\h{64}\z/)

    # A custom comparator is security-sensitive. Accept only the literal
    # boolean true so truthy sentinel/error values can never authenticate.
    comparison_proc.call(stored_digest, Digest::SHA256.hexdigest(token)) == true
  else
    # Strategy mismatch or unsupported strategy should fail comparison safely
    if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
      Rails.logger.error "[ApiKeys] Digestor comparison failed: Unsupported hash strategy '#{strategy}' for digest check."
    end
    false
  end
rescue StandardError => error
  # A malformed digest or application-supplied comparison proc must never
  # turn an authentication failure into an exception or an availability issue.
  if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
    Rails.logger.error "[ApiKeys] Digestor comparison error (#{error.class})."
  end
  false
end

.valid_bcrypt_digest?(stored_digest) ⇒ Boolean

Returns whether a stored bcrypt digest is structurally valid and has a bounded cost that is safe to evaluate on an authentication request.

Returns:

  • (Boolean)


100
101
102
# File 'lib/api_keys/services/digestor.rb', line 100

def self.valid_bcrypt_digest?(stored_digest)
  !validated_bcrypt_password(stored_digest).nil?
end