Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Deprecate Rails::SecretKeyGenerator in favor of ActiveSupport::Secure…

…Random.

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]
  • Loading branch information...
commit b3411ff59eb1e1c31f98f58f117a2ffaaf0c3ff5 1 parent 9dbde4f
@FooBarWidget FooBarWidget authored NZKoz committed
View
2  activesupport/lib/active_support.rb
@@ -55,6 +55,8 @@
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
View
197 activesupport/lib/active_support/secure_random.rb
@@ -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
View
15 activesupport/test/secure_random_test.rb
@@ -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
View
3  railties/lib/rails_generator/generators/applications/app/app_generator.rb
@@ -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 @@ def manifest
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.
View
151 railties/lib/rails_generator/secret_key_generator.rb
@@ -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}")
- 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
+ ActiveSupport::SecureRandom.hex(64)
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
View
7 railties/lib/tasks/misc.rake
@@ -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
View
8 railties/test/secret_key_generation_test.rb
@@ -33,12 +33,4 @@ def setup
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
Please sign in to comment.
Something went wrong with that request. Please try again.