| | 1 | # A class for creating random secret keys. This class will do its best to create a |
|---|
| | 2 | # random secret key that's as secure as possible, using whatever methods are |
|---|
| | 3 | # available on the current platform. For example: |
|---|
| | 4 | # |
|---|
| | 5 | # generator = Rails::SecretKeyGenerator("some unique identifier, such as the application name") |
|---|
| | 6 | # generator.generate_secret # => "f3f1be90053fa851... (some long string)" |
|---|
| | 7 | |
|---|
| | 8 | module Rails |
|---|
| | 9 | class SecretKeyGenerator |
|---|
| | 10 | GENERATORS = [ :secure_random, :win32_api, :urandom, :openssl, :prng ].freeze |
|---|
| | 11 | |
|---|
| | 12 | def initialize(identifier) |
|---|
| | 13 | @identifier = identifier |
|---|
| | 14 | end |
|---|
| | 15 | |
|---|
| | 16 | # Generate a random secret key with the best possible method available on |
|---|
| | 17 | # the current platform. |
|---|
| | 18 | def generate_secret |
|---|
| | 19 | generator = GENERATORS.find do |g| |
|---|
| | 20 | self.class.send("supports_#{g}?") |
|---|
| | 21 | end |
|---|
| | 22 | send("generate_secret_with_#{generator}") |
|---|
| | 23 | end |
|---|
| | 24 | |
|---|
| | 25 | # Generate a random secret key by using the Win32 API. Raises LoadError |
|---|
| | 26 | # if the current platform cannot make use of the Win32 API. Raises |
|---|
| | 27 | # SystemCallError if some other error occured. |
|---|
| | 28 | def generate_secret_with_win32_api |
|---|
| | 29 | # Following code is based on David Garamond's GUID library for Ruby. |
|---|
| | 30 | require 'Win32API' |
|---|
| | 31 | |
|---|
| | 32 | crypt_acquire_context = Win32API.new("advapi32", "CryptAcquireContext", |
|---|
| | 33 | 'PPPII', 'L') |
|---|
| | 34 | crypt_gen_random = Win32API.new("advapi32", "CryptGenRandom", |
|---|
| | 35 | 'LIP', 'L') |
|---|
| | 36 | crypt_release_context = Win32API.new("advapi32", "CryptReleaseContext", |
|---|
| | 37 | 'LI', 'L') |
|---|
| | 38 | prov_rsa_full = 1 |
|---|
| | 39 | crypt_verifycontext = 0xF0000000 |
|---|
| | 40 | |
|---|
| | 41 | hProvStr = " " * 4 |
|---|
| | 42 | if crypt_acquire_context.call(hProvStr, nil, nil, prov_rsa_full, |
|---|
| | 43 | crypt_verifycontext) == 0 |
|---|
| | 44 | raise SystemCallError, "CryptAcquireContext failed: #{lastWin32ErrorMessage}" |
|---|
| | 45 | end |
|---|
| | 46 | hProv, = hProvStr.unpack('L') |
|---|
| | 47 | bytes = " " * 64 |
|---|
| | 48 | if crypt_gen_random.call(hProv, bytes.size, bytes) == 0 |
|---|
| | 49 | raise SystemCallError, "CryptGenRandom failed: #{lastWin32ErrorMessage}" |
|---|
| | 50 | end |
|---|
| | 51 | if crypt_release_context.call(hProv, 0) == 0 |
|---|
| | 52 | raise SystemCallError, "CryptReleaseContext failed: #{lastWin32ErrorMessage}" |
|---|
| | 53 | end |
|---|
| | 54 | bytes.unpack("H*")[0] |
|---|
| | 55 | end |
|---|
| | 56 | |
|---|
| | 57 | # Generate a random secret key with Ruby 1.9's SecureRandom module. |
|---|
| | 58 | # Raises LoadError if the current Ruby version does not support |
|---|
| | 59 | # SecureRandom. |
|---|
| | 60 | def generate_secret_with_secure_random |
|---|
| | 61 | require 'securerandom' |
|---|
| | 62 | return SecureRandom.hex(64) |
|---|
| | 63 | end |
|---|
| | 64 | |
|---|
| | 65 | # Generate a random secret key with OpenSSL. If OpenSSL is not |
|---|
| | 66 | # already loaded, then this method will attempt to load it. |
|---|
| | 67 | # LoadError will be raised if that fails. |
|---|
| | 68 | def generate_secret_with_openssl |
|---|
| | 69 | require 'openssl' |
|---|
| | 70 | if !File.exist?("/dev/urandom") |
|---|
| | 71 | # OpenSSL transparently seeds the random number generator with |
|---|
| | 72 | # data from /dev/urandom. On platforms where that is not |
|---|
| | 73 | # available, such as Windows, we have to provide OpenSSL with |
|---|
| | 74 | # our own seed. Unfortunately there's no way to provide a |
|---|
| | 75 | # secure seed without OS support, so we'll have to do with |
|---|
| | 76 | # rand() and Time.now.usec(). |
|---|
| | 77 | OpenSSL::Random.seed(rand(0).to_s + Time.now.usec.to_s) |
|---|
| | 78 | end |
|---|
| | 79 | data = OpenSSL::BN.rand(2048, -1, false).to_s |
|---|
| | 80 | return OpenSSL::Digest::SHA512.new(data).hexdigest |
|---|
| | 81 | end |
|---|
| | 82 | |
|---|
| | 83 | # Generate a random secret key with /dev/urandom. |
|---|
| | 84 | # Raises SystemCallError on failure. |
|---|
| | 85 | def generate_secret_with_urandom |
|---|
| | 86 | return File.read("/dev/urandom", 64).unpack("H*")[0] |
|---|
| | 87 | end |
|---|
| | 88 | |
|---|
| | 89 | # Generate a random secret key with Ruby's pseudo random number generator, |
|---|
| | 90 | # as well as some environment information. |
|---|
| | 91 | # |
|---|
| | 92 | # This is the least cryptographically secure way to generate a secret key, |
|---|
| | 93 | # and should be avoided whenever possible. |
|---|
| | 94 | def generate_secret_with_prng |
|---|
| | 95 | require 'digest/sha2' |
|---|
| | 96 | sha = Digest::SHA2.new(512) |
|---|
| | 97 | now = Time.now |
|---|
| | 98 | sha << now.to_s |
|---|
| | 99 | sha << String(now.usec) |
|---|
| | 100 | sha << String(rand(0)) |
|---|
| | 101 | sha << String($$) |
|---|
| | 102 | sha << @identifier |
|---|
| | 103 | return sha.hexdigest |
|---|
| | 104 | end |
|---|
| | 105 | |
|---|
| | 106 | private |
|---|
| | 107 | def lastWin32ErrorMessage |
|---|
| | 108 | # Following code is based on David Garamond's GUID library for Ruby. |
|---|
| | 109 | get_last_error = Win32API.new("kernel32", "GetLastError", '', 'L') |
|---|
| | 110 | format_message = Win32API.new("kernel32", "FormatMessageA", |
|---|
| | 111 | 'LPLLPLPPPPPPPP', 'L') |
|---|
| | 112 | format_message_ignore_inserts = 0x00000200 |
|---|
| | 113 | format_message_from_system = 0x00001000 |
|---|
| | 114 | |
|---|
| | 115 | code = get_last_error.call |
|---|
| | 116 | msg = "\0" * 1024 |
|---|
| | 117 | len = format_message.call(format_message_ignore_inserts + |
|---|
| | 118 | format_message_from_system, 0, |
|---|
| | 119 | code, 0, msg, 1024, nil, nil, |
|---|
| | 120 | nil, nil, nil, nil, nil, nil) |
|---|
| | 121 | msg[0, len].tr("\r", '').chomp |
|---|
| | 122 | end |
|---|
| | 123 | |
|---|
| | 124 | def self.supports_secure_random? |
|---|
| | 125 | begin |
|---|
| | 126 | require 'securerandom' |
|---|
| | 127 | true |
|---|
| | 128 | rescue LoadError |
|---|
| | 129 | false |
|---|
| | 130 | end |
|---|
| | 131 | end |
|---|
| | 132 | |
|---|
| | 133 | def self.supports_win32_api? |
|---|
| | 134 | return false unless RUBY_PLATFORM =~ /(:?mswin|mingw)/ |
|---|
| | 135 | begin |
|---|
| | 136 | require 'Win32API' |
|---|
| | 137 | true |
|---|
| | 138 | rescue LoadError |
|---|
| | 139 | false |
|---|
| | 140 | end |
|---|
| | 141 | end |
|---|
| | 142 | |
|---|
| | 143 | def self.supports_urandom? |
|---|
| | 144 | File.exists?('/dev/urandom') |
|---|
| | 145 | end |
|---|
| | 146 | |
|---|
| | 147 | def self.supports_openssl? |
|---|
| | 148 | begin |
|---|
| | 149 | require 'openssl' |
|---|
| | 150 | true |
|---|
| | 151 | rescue LoadError |
|---|
| | 152 | false |
|---|
| | 153 | end |
|---|
| | 154 | end |
|---|
| | 155 | |
|---|
| | 156 | def self.supports_prng? |
|---|
| | 157 | true |
|---|
| | 158 | end |
|---|
| | 159 | end |
|---|
| | 160 | end |
|---|
| | 161 | |