mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
447e28347e
This change allows for configuration of the hash digest that is used in the key generator for key derivation. SHA1 is an outdated algorithm and security auditors tend to frown on its usage. By allowing this to be configured, it becomes possible to move to a more up to date hash mechanism. While I don't think this has any current relevant security implications, especially not with a proper random secret base, moving away from SHA1 makes conversations with auditors and FIPS compliance checks easier since the best answer is always that an approved algorithm is used. A rotation can be built using this change with an approach like the following for encrypted cookies: ```ruby Rails.application.config.active_support.key_generator_hash_digest_class = OpenSSL::Digest::SHA256 Rails.application.config.action_dispatch.cookies_rotations.tap do |cookies| salt = Rails.application.config.action_dispatch.authenticated_encrypted_cookie_salt secret_key_base = Rails.application.secrets.secret_key_base key_generator = ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000, hash_digest_class: OpenSSL::Digest::SHA1) key_len = ActiveSupport::MessageEncryptor.key_len secret = key_generator.generate_key(salt, key_len) cookies.rotate :encrypted, secret end ``` This turns the default into using SHA256 but also still accepts secrets derived using SHA1. The defaults for new apps is here changed to use SHA256. Existing apps will keep using SHA1.
58 lines
2.1 KiB
Ruby
58 lines
2.1 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "concurrent/map"
|
|
require "openssl"
|
|
|
|
module ActiveSupport
|
|
# KeyGenerator is a simple wrapper around OpenSSL's implementation of PBKDF2.
|
|
# It can be used to derive a number of keys for various purposes from a given secret.
|
|
# This lets Rails applications have a single secure secret, but avoid reusing that
|
|
# key in multiple incompatible contexts.
|
|
class KeyGenerator
|
|
class << self
|
|
def hash_digest_class=(klass)
|
|
if klass.kind_of?(Class) && klass < OpenSSL::Digest
|
|
@hash_digest_class = klass
|
|
else
|
|
raise ArgumentError, "#{klass} is expected to be an OpenSSL::Digest subclass"
|
|
end
|
|
end
|
|
|
|
def hash_digest_class
|
|
@hash_digest_class ||= OpenSSL::Digest::SHA1
|
|
end
|
|
end
|
|
|
|
def initialize(secret, options = {})
|
|
@secret = secret
|
|
# The default iterations are higher than required for our key derivation uses
|
|
# on the off chance someone uses this for password storage
|
|
@iterations = options[:iterations] || 2**16
|
|
# Also allow configuration here so people can use this to build a rotation
|
|
# scheme when switching the digest class.
|
|
@hash_digest_class = options[:hash_digest_class] || self.class.hash_digest_class
|
|
end
|
|
|
|
# Returns a derived key suitable for use. The default key_size is chosen
|
|
# to be compatible with the default settings of ActiveSupport::MessageVerifier.
|
|
# i.e. OpenSSL::Digest::SHA1#block_length
|
|
def generate_key(salt, key_size = 64)
|
|
OpenSSL::PKCS5.pbkdf2_hmac(@secret, salt, @iterations, key_size, @hash_digest_class.new)
|
|
end
|
|
end
|
|
|
|
# CachingKeyGenerator is a wrapper around KeyGenerator which allows users to avoid
|
|
# re-executing the key generation process when it's called using the same salt and
|
|
# key_size.
|
|
class CachingKeyGenerator
|
|
def initialize(key_generator)
|
|
@key_generator = key_generator
|
|
@cache_keys = Concurrent::Map.new
|
|
end
|
|
|
|
# Returns a derived key suitable for use.
|
|
def generate_key(*args)
|
|
@cache_keys[args.join("|")] ||= @key_generator.generate_key(*args)
|
|
end
|
|
end
|
|
end
|