mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Deprecate Rails::SecretKeyGenerator in favor of ActiveSupport::SecureRandom.
SecureRandom has a few minor security enhancements and can be used as a drop-in replacement Signed-off-by: Michael Koziarski <michael@koziarski.com> [#913 state:committed]
This commit is contained in:
parent
9dbde4f5cb
commit
b3411ff59e
7 changed files with 222 additions and 161 deletions
|
@ -55,6 +55,8 @@ require 'active_support/base64'
|
|||
|
||||
require 'active_support/time_with_zone'
|
||||
|
||||
require 'active_support/secure_random'
|
||||
|
||||
I18n.populate do
|
||||
I18n.load_translations File.dirname(__FILE__) + '/active_support/locale/en-US.yml'
|
||||
end
|
||||
|
|
197
activesupport/lib/active_support/secure_random.rb
Normal file
197
activesupport/lib/active_support/secure_random.rb
Normal file
|
@ -0,0 +1,197 @@
|
|||
begin
|
||||
require 'openssl'
|
||||
rescue LoadError
|
||||
end
|
||||
|
||||
begin
|
||||
require 'securerandom'
|
||||
rescue LoadError
|
||||
end
|
||||
|
||||
module ActiveSupport
|
||||
if defined?(::SecureRandom)
|
||||
# Use Ruby 1.9's SecureRandom library whenever possible.
|
||||
SecureRandom = ::SecureRandom # :nodoc:
|
||||
else
|
||||
# = Secure random number generator interface.
|
||||
#
|
||||
# This library is an interface for secure random number generator which is
|
||||
# suitable for generating session key in HTTP cookies, etc.
|
||||
#
|
||||
# It supports following secure random number generators.
|
||||
#
|
||||
# * openssl
|
||||
# * /dev/urandom
|
||||
# * Win32
|
||||
#
|
||||
# *Note*: This module is based on the SecureRandom library from Ruby 1.9,
|
||||
# revision 18786, August 23 2008. It's 100% interface-compatible with Ruby 1.9's
|
||||
# SecureRandom library.
|
||||
#
|
||||
# == Example
|
||||
#
|
||||
# # random hexadecimal string.
|
||||
# p SecureRandom.hex(10) #=> "52750b30ffbc7de3b362"
|
||||
# p SecureRandom.hex(10) #=> "92b15d6c8dc4beb5f559"
|
||||
# p SecureRandom.hex(11) #=> "6aca1b5c58e4863e6b81b8"
|
||||
# p SecureRandom.hex(12) #=> "94b2fff3e7fd9b9c391a2306"
|
||||
# p SecureRandom.hex(13) #=> "39b290146bea6ce975c37cfc23"
|
||||
# ...
|
||||
#
|
||||
# # random base64 string.
|
||||
# p SecureRandom.base64(10) #=> "EcmTPZwWRAozdA=="
|
||||
# p SecureRandom.base64(10) #=> "9b0nsevdwNuM/w=="
|
||||
# p SecureRandom.base64(10) #=> "KO1nIU+p9DKxGg=="
|
||||
# p SecureRandom.base64(11) #=> "l7XEiFja+8EKEtY="
|
||||
# p SecureRandom.base64(12) #=> "7kJSM/MzBJI+75j8"
|
||||
# p SecureRandom.base64(13) #=> "vKLJ0tXBHqQOuIcSIg=="
|
||||
# ...
|
||||
#
|
||||
# # random binary string.
|
||||
# p SecureRandom.random_bytes(10) #=> "\016\t{\370g\310pbr\301"
|
||||
# p SecureRandom.random_bytes(10) #=> "\323U\030TO\234\357\020\a\337"
|
||||
# ...
|
||||
module SecureRandom
|
||||
# SecureRandom.random_bytes generates a random binary string.
|
||||
#
|
||||
# The argument n specifies the length of the result string.
|
||||
#
|
||||
# If n is not specified, 16 is assumed.
|
||||
# It may be larger in future.
|
||||
#
|
||||
# If secure random number generator is not available,
|
||||
# NotImplementedError is raised.
|
||||
def self.random_bytes(n=nil)
|
||||
n ||= 16
|
||||
|
||||
if defined? OpenSSL::Random
|
||||
return OpenSSL::Random.random_bytes(n)
|
||||
end
|
||||
|
||||
if !defined?(@has_urandom) || @has_urandom
|
||||
flags = File::RDONLY
|
||||
flags |= File::NONBLOCK if defined? File::NONBLOCK
|
||||
flags |= File::NOCTTY if defined? File::NOCTTY
|
||||
flags |= File::NOFOLLOW if defined? File::NOFOLLOW
|
||||
begin
|
||||
File.open("/dev/urandom", flags) {|f|
|
||||
unless f.stat.chardev?
|
||||
raise Errno::ENOENT
|
||||
end
|
||||
@has_urandom = true
|
||||
ret = f.readpartial(n)
|
||||
if ret.length != n
|
||||
raise NotImplementedError, "Unexpected partial read from random device"
|
||||
end
|
||||
return ret
|
||||
}
|
||||
rescue Errno::ENOENT
|
||||
@has_urandom = false
|
||||
end
|
||||
end
|
||||
|
||||
if !defined?(@has_win32)
|
||||
begin
|
||||
require 'Win32API'
|
||||
|
||||
crypt_acquire_context = Win32API.new("advapi32", "CryptAcquireContext", 'PPPII', 'L')
|
||||
@crypt_gen_random = Win32API.new("advapi32", "CryptGenRandom", 'LIP', 'L')
|
||||
|
||||
hProvStr = " " * 4
|
||||
prov_rsa_full = 1
|
||||
crypt_verifycontext = 0xF0000000
|
||||
|
||||
if crypt_acquire_context.call(hProvStr, nil, nil, prov_rsa_full, crypt_verifycontext) == 0
|
||||
raise SystemCallError, "CryptAcquireContext failed: #{lastWin32ErrorMessage}"
|
||||
end
|
||||
@hProv, = hProvStr.unpack('L')
|
||||
|
||||
@has_win32 = true
|
||||
rescue LoadError
|
||||
@has_win32 = false
|
||||
end
|
||||
end
|
||||
if @has_win32
|
||||
bytes = " " * n
|
||||
if @crypt_gen_random.call(@hProv, bytes.size, bytes) == 0
|
||||
raise SystemCallError, "CryptGenRandom failed: #{lastWin32ErrorMessage}"
|
||||
end
|
||||
return bytes
|
||||
end
|
||||
|
||||
raise NotImplementedError, "No random device"
|
||||
end
|
||||
|
||||
# SecureRandom.hex generates a random hex string.
|
||||
#
|
||||
# The argument n specifies the length of the random length.
|
||||
# The length of the result string is twice of n.
|
||||
#
|
||||
# If n is not specified, 16 is assumed.
|
||||
# It may be larger in future.
|
||||
#
|
||||
# If secure random number generator is not available,
|
||||
# NotImplementedError is raised.
|
||||
def self.hex(n=nil)
|
||||
random_bytes(n).unpack("H*")[0]
|
||||
end
|
||||
|
||||
# SecureRandom.base64 generates a random base64 string.
|
||||
#
|
||||
# The argument n specifies the length of the random length.
|
||||
# The length of the result string is about 4/3 of n.
|
||||
#
|
||||
# If n is not specified, 16 is assumed.
|
||||
# It may be larger in future.
|
||||
#
|
||||
# If secure random number generator is not available,
|
||||
# NotImplementedError is raised.
|
||||
def self.base64(n=nil)
|
||||
[random_bytes(n)].pack("m*").delete("\n")
|
||||
end
|
||||
|
||||
# SecureRandom.random_number generates a random number.
|
||||
#
|
||||
# If an positive integer is given as n,
|
||||
# SecureRandom.random_number returns an integer:
|
||||
# 0 <= SecureRandom.random_number(n) < n.
|
||||
#
|
||||
# If 0 is given or an argument is not given,
|
||||
# SecureRandom.random_number returns an float:
|
||||
# 0.0 <= SecureRandom.random_number() < 1.0.
|
||||
def self.random_number(n=0)
|
||||
if 0 < n
|
||||
hex = n.to_s(16)
|
||||
hex = '0' + hex if (hex.length & 1) == 1
|
||||
bin = [hex].pack("H*")
|
||||
mask = bin[0].ord
|
||||
mask |= mask >> 1
|
||||
mask |= mask >> 2
|
||||
mask |= mask >> 4
|
||||
begin
|
||||
rnd = SecureRandom.random_bytes(bin.length)
|
||||
rnd[0] = (rnd[0].ord & mask).chr
|
||||
end until rnd < bin
|
||||
rnd.unpack("H*")[0].hex
|
||||
else
|
||||
# assumption: Float::MANT_DIG <= 64
|
||||
i64 = SecureRandom.random_bytes(8).unpack("Q")[0]
|
||||
Math.ldexp(i64 >> (64-Float::MANT_DIG), -Float::MANT_DIG)
|
||||
end
|
||||
end
|
||||
|
||||
# Following code is based on David Garamond's GUID library for Ruby.
|
||||
def self.lastWin32ErrorMessage # :nodoc:
|
||||
get_last_error = Win32API.new("kernel32", "GetLastError", '', 'L')
|
||||
format_message = Win32API.new("kernel32", "FormatMessageA", 'LPLLPLPPPPPPPP', 'L')
|
||||
format_message_ignore_inserts = 0x00000200
|
||||
format_message_from_system = 0x00001000
|
||||
|
||||
code = get_last_error.call
|
||||
msg = "\0" * 1024
|
||||
len = format_message.call(format_message_ignore_inserts + format_message_from_system, 0, code, 0, msg, 1024, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
msg[0, len].tr("\r", '').chomp
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
15
activesupport/test/secure_random_test.rb
Normal file
15
activesupport/test/secure_random_test.rb
Normal file
|
@ -0,0 +1,15 @@
|
|||
require 'abstract_unit'
|
||||
|
||||
class SecureRandomTest < Test::Unit::TestCase
|
||||
def test_random_bytes
|
||||
b1 = ActiveSupport::SecureRandom.random_bytes(64)
|
||||
b2 = ActiveSupport::SecureRandom.random_bytes(64)
|
||||
assert_not_equal b1, b2
|
||||
end
|
||||
|
||||
def test_hex
|
||||
b1 = ActiveSupport::SecureRandom.hex(64)
|
||||
b2 = ActiveSupport::SecureRandom.hex(64)
|
||||
assert_not_equal b1, b2
|
||||
end
|
||||
end
|
|
@ -1,6 +1,5 @@
|
|||
require 'rbconfig'
|
||||
require 'digest/md5'
|
||||
require 'rails_generator/secret_key_generator'
|
||||
|
||||
class AppGenerator < Rails::Generator::Base
|
||||
DEFAULT_SHEBANG = File.join(Config::CONFIG['bindir'],
|
||||
|
@ -36,7 +35,7 @@ class AppGenerator < Rails::Generator::Base
|
|||
md5 << @app_name
|
||||
|
||||
# Do our best to generate a secure secret key for CookieStore
|
||||
secret = Rails::SecretKeyGenerator.new(@app_name).generate_secret
|
||||
secret = ActiveSupport::SecureRandom.hex(64)
|
||||
|
||||
record do |m|
|
||||
# Root directory and all subdirectories.
|
||||
|
|
|
@ -5,160 +5,17 @@ module Rails
|
|||
#
|
||||
# generator = Rails::SecretKeyGenerator("some unique identifier, such as the application name")
|
||||
# generator.generate_secret # => "f3f1be90053fa851... (some long string)"
|
||||
#
|
||||
# This class is *deprecated* in Rails 2.2 in favor of ActiveSupport::SecureRandom.
|
||||
# It is currently a wrapper around ActiveSupport::SecureRandom.
|
||||
class SecretKeyGenerator
|
||||
GENERATORS = [ :secure_random, :win32_api, :urandom, :openssl, :prng ].freeze
|
||||
|
||||
def initialize(identifier)
|
||||
@identifier = identifier
|
||||
end
|
||||
|
||||
# Generate a random secret key with the best possible method available on
|
||||
# the current platform.
|
||||
def generate_secret
|
||||
generator = GENERATORS.find do |g|
|
||||
self.class.send("supports_#{g}?")
|
||||
end
|
||||
send("generate_secret_with_#{generator}")
|
||||
ActiveSupport::SecureRandom.hex(64)
|
||||
end
|
||||
|
||||
# Generate a random secret key by using the Win32 API. Raises LoadError
|
||||
# if the current platform cannot make use of the Win32 API. Raises
|
||||
# SystemCallError if some other error occurred.
|
||||
def generate_secret_with_win32_api
|
||||
# Following code is based on David Garamond's GUID library for Ruby.
|
||||
require 'Win32API'
|
||||
|
||||
crypt_acquire_context = Win32API.new("advapi32", "CryptAcquireContext",
|
||||
'PPPII', 'L')
|
||||
crypt_gen_random = Win32API.new("advapi32", "CryptGenRandom",
|
||||
'LIP', 'L')
|
||||
crypt_release_context = Win32API.new("advapi32", "CryptReleaseContext",
|
||||
'LI', 'L')
|
||||
prov_rsa_full = 1
|
||||
crypt_verifycontext = 0xF0000000
|
||||
|
||||
hProvStr = " " * 4
|
||||
if crypt_acquire_context.call(hProvStr, nil, nil, prov_rsa_full,
|
||||
crypt_verifycontext) == 0
|
||||
raise SystemCallError, "CryptAcquireContext failed: #{lastWin32ErrorMessage}"
|
||||
end
|
||||
hProv, = hProvStr.unpack('L')
|
||||
bytes = " " * 64
|
||||
if crypt_gen_random.call(hProv, bytes.size, bytes) == 0
|
||||
raise SystemCallError, "CryptGenRandom failed: #{lastWin32ErrorMessage}"
|
||||
end
|
||||
if crypt_release_context.call(hProv, 0) == 0
|
||||
raise SystemCallError, "CryptReleaseContext failed: #{lastWin32ErrorMessage}"
|
||||
end
|
||||
bytes.unpack("H*")[0]
|
||||
end
|
||||
|
||||
# Generate a random secret key with Ruby 1.9's SecureRandom module.
|
||||
# Raises LoadError if the current Ruby version does not support
|
||||
# SecureRandom.
|
||||
def generate_secret_with_secure_random
|
||||
require 'securerandom'
|
||||
return SecureRandom.hex(64)
|
||||
end
|
||||
|
||||
# Generate a random secret key with OpenSSL. If OpenSSL is not
|
||||
# already loaded, then this method will attempt to load it.
|
||||
# LoadError will be raised if that fails.
|
||||
def generate_secret_with_openssl
|
||||
require 'openssl'
|
||||
if !File.exist?("/dev/urandom")
|
||||
# OpenSSL transparently seeds the random number generator with
|
||||
# data from /dev/urandom. On platforms where that is not
|
||||
# available, such as Windows, we have to provide OpenSSL with
|
||||
# our own seed. Unfortunately there's no way to provide a
|
||||
# secure seed without OS support, so we'll have to do with
|
||||
# rand() and Time.now.usec().
|
||||
OpenSSL::Random.seed(rand(0).to_s + Time.now.usec.to_s)
|
||||
end
|
||||
data = OpenSSL::BN.rand(2048, -1, false).to_s
|
||||
|
||||
if OpenSSL::OPENSSL_VERSION_NUMBER > 0x00908000
|
||||
OpenSSL::Digest::SHA512.new(data).hexdigest
|
||||
else
|
||||
generate_secret_with_prng
|
||||
end
|
||||
end
|
||||
|
||||
# Generate a random secret key with /dev/urandom.
|
||||
# Raises SystemCallError on failure.
|
||||
def generate_secret_with_urandom
|
||||
return File.read("/dev/urandom", 64).unpack("H*")[0]
|
||||
end
|
||||
|
||||
# Generate a random secret key with Ruby's pseudo random number generator,
|
||||
# as well as some environment information.
|
||||
#
|
||||
# This is the least cryptographically secure way to generate a secret key,
|
||||
# and should be avoided whenever possible.
|
||||
def generate_secret_with_prng
|
||||
require 'digest/sha2'
|
||||
sha = Digest::SHA2.new(512)
|
||||
now = Time.now
|
||||
sha << now.to_s
|
||||
sha << String(now.usec)
|
||||
sha << String(rand(0))
|
||||
sha << String($$)
|
||||
sha << @identifier
|
||||
return sha.hexdigest
|
||||
end
|
||||
|
||||
private
|
||||
def lastWin32ErrorMessage
|
||||
# Following code is based on David Garamond's GUID library for Ruby.
|
||||
get_last_error = Win32API.new("kernel32", "GetLastError", '', 'L')
|
||||
format_message = Win32API.new("kernel32", "FormatMessageA",
|
||||
'LPLLPLPPPPPPPP', 'L')
|
||||
format_message_ignore_inserts = 0x00000200
|
||||
format_message_from_system = 0x00001000
|
||||
|
||||
code = get_last_error.call
|
||||
msg = "\0" * 1024
|
||||
len = format_message.call(format_message_ignore_inserts +
|
||||
format_message_from_system, 0,
|
||||
code, 0, msg, 1024, nil, nil,
|
||||
nil, nil, nil, nil, nil, nil)
|
||||
msg[0, len].tr("\r", '').chomp
|
||||
end
|
||||
|
||||
def self.supports_secure_random?
|
||||
begin
|
||||
require 'securerandom'
|
||||
true
|
||||
rescue LoadError
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def self.supports_win32_api?
|
||||
return false unless RUBY_PLATFORM =~ /(:?mswin|mingw)/
|
||||
begin
|
||||
require 'Win32API'
|
||||
true
|
||||
rescue LoadError
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def self.supports_urandom?
|
||||
File.exist?('/dev/urandom')
|
||||
end
|
||||
|
||||
def self.supports_openssl?
|
||||
begin
|
||||
require 'openssl'
|
||||
true
|
||||
rescue LoadError
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def self.supports_prng?
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,10 +3,9 @@ task :environment do
|
|||
require(File.join(RAILS_ROOT, 'config', 'environment'))
|
||||
end
|
||||
|
||||
require 'rails_generator/secret_key_generator'
|
||||
desc 'Generate a crytographically secure secret key. This is typically used to generate a secret for cookie sessions. Pass a unique identifier to the generator using ID="some unique identifier" for greater security.'
|
||||
desc 'Generate a crytographically secure secret key. This is typically used to generate a secret for cookie sessions.'
|
||||
task :secret do
|
||||
puts Rails::SecretKeyGenerator.new(ENV['ID']).generate_secret
|
||||
puts ActiveSupport::SecureRandom.hex(64)
|
||||
end
|
||||
|
||||
require 'active_support'
|
||||
|
@ -54,4 +53,4 @@ namespace :time do
|
|||
puts "\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -33,12 +33,4 @@ class SecretKeyGenerationTest < Test::Unit::TestCase
|
|||
def test_secret_key_generation
|
||||
assert @generator.generate_secret.length >= SECRET_KEY_MIN_LENGTH
|
||||
end
|
||||
|
||||
Rails::SecretKeyGenerator::GENERATORS.each do |generator|
|
||||
if Rails::SecretKeyGenerator.send("supports_#{generator}?")
|
||||
define_method("test_secret_key_generation_with_#{generator}") do
|
||||
assert @generator.send("generate_secret_with_#{generator}").length >= SECRET_KEY_MIN_LENGTH
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue