From f1d6a0e4d25ad67fead3ed215495adcf08332c80 Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Wed, 28 Nov 2007 19:36:59 +0000 Subject: Introduce SecretKeyGenerator for more secure session secrets than CGI::Session's pseudo-random id generator. Consider extracting to Active Support later. Closes #10286. git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@8229 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- railties/CHANGELOG | 2 + .../generators/applications/app/app_generator.rb | 6 +- .../lib/rails_generator/secret_key_generator.rb | 160 +++++++++++++++++++++ railties/test/secret_key_generation_test.rb | 35 +++++ 4 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 railties/lib/rails_generator/secret_key_generator.rb create mode 100644 railties/test/secret_key_generation_test.rb diff --git a/railties/CHANGELOG b/railties/CHANGELOG index 5b3bb07022..e081bff410 100644 --- a/railties/CHANGELOG +++ b/railties/CHANGELOG @@ -1,5 +1,7 @@ *SVN* +* Introduce SecretKeyGenerator for more secure session secrets than CGI::Session's pseudo-random id generator. Consider extracting to Active Support later. #10286 [Hongli Lai] + * RAILS_GEM_VERSION may be set to any valid gem version specifier. #10057 [Chad Woolley, Chu Yeow] * Load config/preinitializer.rb, if present, before loading the environment. #9943 [Chad Woolley] diff --git a/railties/lib/rails_generator/generators/applications/app/app_generator.rb b/railties/lib/rails_generator/generators/applications/app/app_generator.rb index 55c8bf3239..02d9b0fb88 100644 --- a/railties/lib/rails_generator/generators/applications/app/app_generator.rb +++ b/railties/lib/rails_generator/generators/applications/app/app_generator.rb @@ -1,5 +1,6 @@ require 'rbconfig' require 'digest/md5' +require 'rails_generator/secret_key_generator' class AppGenerator < Rails::Generator::Base DEFAULT_SHEBANG = File.join(Config::CONFIG['bindir'], @@ -33,6 +34,9 @@ class AppGenerator < Rails::Generator::Base md5 << String($$) md5 << @app_name + # Do our best to generate a secure secret key for CookieStore + secret = Rails::SecretKeyGenerator.new(@app_name).generate_secret + record do |m| # Root directory and all subdirectories. m.directory '' @@ -61,7 +65,7 @@ class AppGenerator < Rails::Generator::Base # Environments m.file "environments/boot.rb", "config/boot.rb" - m.template "environments/environment.rb", "config/environment.rb", :assigns => { :freeze => options[:freeze], :app_name => @app_name, :app_secret => md5.hexdigest } + m.template "environments/environment.rb", "config/environment.rb", :assigns => { :freeze => options[:freeze], :app_name => @app_name, :app_secret => secret } m.file "environments/production.rb", "config/environments/production.rb" m.file "environments/development.rb", "config/environments/development.rb" m.file "environments/test.rb", "config/environments/test.rb" diff --git a/railties/lib/rails_generator/secret_key_generator.rb b/railties/lib/rails_generator/secret_key_generator.rb new file mode 100644 index 0000000000..f488f96f32 --- /dev/null +++ b/railties/lib/rails_generator/secret_key_generator.rb @@ -0,0 +1,160 @@ +# A class for creating random secret keys. This class will do its best to create a +# random secret key that's as secure as possible, using whatever methods are +# available on the current platform. For example: +# +# generator = Rails::SecretKeyGenerator("some unique identifier, such as the application name") +# generator.generate_secret # => "f3f1be90053fa851... (some long string)" + +module Rails + 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 occured. + 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 + return OpenSSL::Digest::SHA512.new(data).hexdigest + 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.exists?('/dev/urandom') + end + + def self.supports_openssl? + begin + require 'openssl' + true + rescue LoadError + false + end + end + + def self.supports_prng? + true + end + end +end diff --git a/railties/test/secret_key_generation_test.rb b/railties/test/secret_key_generation_test.rb new file mode 100644 index 0000000000..093436889c --- /dev/null +++ b/railties/test/secret_key_generation_test.rb @@ -0,0 +1,35 @@ +require 'test/unit' + +# Must set before requiring generator libs. +if defined?(RAILS_ROOT) + RAILS_ROOT.replace "#{File.dirname(__FILE__)}/fixtures" +else + RAILS_ROOT = "#{File.dirname(__FILE__)}/fixtures" +end + +$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib" + +require 'rails_generator' +require 'rails_generator/secret_key_generator' +require 'rails_generator/generators/applications/app/app_generator' + +class SecretKeyGenerationTest < Test::Unit::TestCase + SECRET_KEY_MIN_LENGTH = 128 + APP_NAME = "foo" + + def setup + @generator = Rails::SecretKeyGenerator.new(APP_NAME) + end + + 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 -- cgit v1.2.3