File: active_support/secure_random.rb

Overview
Module Structure
Code

Overview

Module Structure

  module: <Toplevel Module>
  module: ActiveSupport#6
has properties
constant: SecureRandom #9
  module: SecureRandom#49
has properties
module method: random_bytes / 1 #59
module method: hex / 1 #137
module method: base64 / 1 #151
module method: random_number / 1 #164
module method: lastWin32ErrorMessage #186

Code

   1  begin
   2    require 'securerandom'
   3  rescue LoadError
   4  end
   5 
   6  module ActiveSupport
   7    if defined?(::SecureRandom)
   8      # Use Ruby's SecureRandom library if available.
   9      SecureRandom = ::SecureRandom # :nodoc:
  10    else
  11      # = Secure random number generator interface.
  12      #
  13      # This library is an interface for secure random number generator which is
  14      # suitable for generating session key in HTTP cookies, etc.
  15      #
  16      # It supports following secure random number generators.
  17      #
  18      # * openssl
  19      # * /dev/urandom
  20      # * Win32
  21      #
  22      # *Note*: This module is based on the SecureRandom library from Ruby 1.9,
  23      # revision 18786, August 23 2008. It's 100% interface-compatible with Ruby 1.9's
  24      # SecureRandom library.
  25      #
  26      # == Example
  27      #
  28      #  # random hexadecimal string.
  29      #  p SecureRandom.hex(10) #=> "52750b30ffbc7de3b362"
  30      #  p SecureRandom.hex(10) #=> "92b15d6c8dc4beb5f559"
  31      #  p SecureRandom.hex(11) #=> "6aca1b5c58e4863e6b81b8"
  32      #  p SecureRandom.hex(12) #=> "94b2fff3e7fd9b9c391a2306"
  33      #  p SecureRandom.hex(13) #=> "39b290146bea6ce975c37cfc23"
  34      #  ...
  35      #
  36      #  # random base64 string.
  37      #  p SecureRandom.base64(10) #=> "EcmTPZwWRAozdA=="
  38      #  p SecureRandom.base64(10) #=> "9b0nsevdwNuM/w=="
  39      #  p SecureRandom.base64(10) #=> "KO1nIU+p9DKxGg=="
  40      #  p SecureRandom.base64(11) #=> "l7XEiFja+8EKEtY="
  41      #  p SecureRandom.base64(12) #=> "7kJSM/MzBJI+75j8"
  42      #  p SecureRandom.base64(13) #=> "vKLJ0tXBHqQOuIcSIg=="
  43      #  ...
  44      #
  45      #  # random binary string.
  46      #  p SecureRandom.random_bytes(10) #=> "\016\t{\370g\310pbr\301"
  47      #  p SecureRandom.random_bytes(10) #=> "\323U\030TO\234\357\020\a\337"
  48      #  ...
  49      module SecureRandom
  50        # SecureRandom.random_bytes generates a random binary string.
  51        #
  52        # The argument n specifies the length of the result string.
  53        #
  54        # If n is not specified, 16 is assumed.
  55        # It may be larger in future.
  56        #
  57        # If secure random number generator is not available,
  58        # NotImplementedError is raised.
  59        def self.random_bytes(n=nil)
  60          n ||= 16
  61 
  62          unless defined? OpenSSL
  63            begin
  64              require 'openssl'
  65            rescue LoadError
  66            end
  67          end
  68 
  69          if defined? OpenSSL::Random
  70            return OpenSSL::Random.random_bytes(n)
  71          end
  72 
  73          if !defined?(@has_urandom) || @has_urandom
  74            flags = File::RDONLY
  75            flags |= File::NONBLOCK if defined? File::NONBLOCK
  76            flags |= File::NOCTTY if defined? File::NOCTTY
  77            flags |= File::NOFOLLOW if defined? File::NOFOLLOW
  78            begin
  79              File.open("/dev/urandom", flags) {|f|
  80                unless f.stat.chardev?
  81                  raise Errno::ENOENT
  82                end
  83                @has_urandom = true
  84                ret = f.readpartial(n)
  85                if ret.length != n
  86                  raise NotImplementedError, "Unexpected partial read from random device"
  87                end
  88                return ret
  89              }
  90            rescue Errno::ENOENT
  91              @has_urandom = false
  92            end
  93          end
  94 
  95          if !defined?(@has_win32)
  96            begin
  97              require 'Win32API'
  98 
  99              crypt_acquire_context = Win32API.new("advapi32", "CryptAcquireContext", 'PPPII', 'L')
 100              @crypt_gen_random = Win32API.new("advapi32", "CryptGenRandom", 'LIP', 'L')
 101 
 102              hProvStr = " " * 4
 103              prov_rsa_full = 1
 104              crypt_verifycontext = 0xF0000000
 105 
 106              if crypt_acquire_context.call(hProvStr, nil, nil, prov_rsa_full, crypt_verifycontext) == 0
 107                raise SystemCallError, "CryptAcquireContext failed: #{lastWin32ErrorMessage}"
 108              end
 109              @hProv, = hProvStr.unpack('L')
 110 
 111              @has_win32 = true
 112            rescue LoadError
 113              @has_win32 = false
 114            end
 115          end
 116          if @has_win32
 117            bytes = " " * n
 118            if @crypt_gen_random.call(@hProv, bytes.size, bytes) == 0
 119              raise SystemCallError, "CryptGenRandom failed: #{lastWin32ErrorMessage}"
 120            end
 121            return bytes
 122          end
 123 
 124          raise NotImplementedError, "No random device"
 125        end
 126 
 127        # SecureRandom.hex generates a random hex string.
 128        #
 129        # The argument n specifies the length of the random length.
 130        # The length of the result string is twice of n.
 131        #
 132        # If n is not specified, 16 is assumed.
 133        # It may be larger in future.
 134        #
 135        # If secure random number generator is not available,
 136        # NotImplementedError is raised.
 137        def self.hex(n=nil)
 138          random_bytes(n).unpack("H*")[0]
 139        end
 140 
 141        # SecureRandom.base64 generates a random base64 string.
 142        #
 143        # The argument n specifies the length of the random length.
 144        # The length of the result string is about 4/3 of n.
 145        #
 146        # If n is not specified, 16 is assumed.
 147        # It may be larger in future.
 148        #
 149        # If secure random number generator is not available,
 150        # NotImplementedError is raised.
 151        def self.base64(n=nil)
 152          [random_bytes(n)].pack("m*").delete("\n")
 153        end
 154 
 155        # SecureRandom.random_number generates a random number.
 156        #
 157        # If an positive integer is given as n,
 158        # SecureRandom.random_number returns an integer:
 159        # 0 <= SecureRandom.random_number(n) < n.
 160        #
 161        # If 0 is given or an argument is not given,
 162        # SecureRandom.random_number returns an float:
 163        # 0.0 <= SecureRandom.random_number() < 1.0.
 164        def self.random_number(n=0)
 165          if 0 < n
 166            hex = n.to_s(16)
 167            hex = '0' + hex if (hex.length & 1) == 1
 168            bin = [hex].pack("H*")
 169            mask = bin[0]
 170            mask |= mask >> 1
 171            mask |= mask >> 2
 172            mask |= mask >> 4
 173            begin
 174              rnd = SecureRandom.random_bytes(bin.length)
 175              rnd[0] = rnd[0] & mask
 176            end until rnd < bin
 177            rnd.unpack("H*")[0].hex
 178          else
 179            # assumption: Float::MANT_DIG <= 64
 180            i64 = SecureRandom.random_bytes(8).unpack("Q")[0]
 181            Math.ldexp(i64 >> (64-Float::MANT_DIG), -Float::MANT_DIG)
 182          end
 183        end
 184 
 185        # Following code is based on David Garamond's GUID library for Ruby.
 186        def self.lastWin32ErrorMessage # :nodoc:
 187          get_last_error = Win32API.new("kernel32", "GetLastError", '', 'L')
 188          format_message = Win32API.new("kernel32", "FormatMessageA", 'LPLLPLPPPPPPPP', 'L')
 189          format_message_ignore_inserts = 0x00000200
 190          format_message_from_system    = 0x00001000
 191 
 192          code = get_last_error.call
 193          msg = "\0" * 1024
 194          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)
 195          msg[0, len].tr("\r", '').chomp
 196        end
 197      end
 198    end
 199  end