#!/usr/bin/env ruby

class SecretGenerator
    SECRET_MIN_LENGTH = 30
    
    def initialize(app_name = "foobar")
      @app_name = app_name
    end
    
    # Generate a random secret key with the best possible method available on the
    # current platform. 
    def generate_secret(method = nil)
      if method.nil?
        begin
          secret = generate_secret_with_secure_random
        rescue LoadError
          # SecureRandom not available. Try other methods.
          begin
            if RUBY_PLATFORM =~ /(:?mswin|mingw)/
              begin
                secret = generate_secret_with_win32_api
              rescue LoadError
                secret = generate_secret_with_openssl
              end
            else
              begin
                secret = generate_secret_with_urandom
              rescue SystemCallError
                secret = generate_secret_with_openssl
              end
            end
          rescue LoadError
            # OpenSSL not available. Try last resort.
            secret = generate_secret_with_prng
          end
        end
      else
        secret = self.send("generate_secret_with_#{method}".to_sym)
      end
      secret
    end
    
  protected
    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
    
    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 generate_secret_with_secure_random
      require 'securerandom'
      return SecureRandom.hex(64)
    end
    
    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
    
    def generate_secret_with_urandom
      return File.read("/dev/urandom", 64).unpack("H*")[0]
    end
    
    def generate_secret_with_prng
      # This is the least cryptographically secure way to generate a secret key.
      # See the following website for details:
      # http://epsilondelta.net/2006/05/17/examining-rubys-http-session-id-generation/
      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 << @app_name
      return sha.hexdigest
    end
end

generator = SecretGenerator.new
puts "Best for platform:\n  " << generator.generate_secret
puts "/dev/urandom:\n  "  << generator.generate_secret(:urandom)
puts "OpenSSL:\n  "       << generator.generate_secret(:openssl)
puts "Pseudo random:\n  " << generator.generate_secret(:prng)
begin
  puts "Win32 API:\n  " << generator.generate_secret(:win32_api)
rescue LoadError
end
begin
  puts "SecureRandom:\n  " << generator.generate_secret(:secure_random)
rescue LoadError
end