File: engine.rb

package info (click to toggle)
ruby-bcrypt 3.1.20-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 340 kB
  • sloc: ansic: 1,390; java: 764; ruby: 438; asm: 157; makefile: 12
file content (134 lines) | stat: -rw-r--r-- 4,633 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
module BCrypt
  # A Ruby wrapper for the bcrypt() C extension calls and the Java calls.
  class Engine
    # The default computational expense parameter.
    DEFAULT_COST    = 12
    # The minimum cost supported by the algorithm.
    MIN_COST        = 4
    # The maximum cost supported by the algorithm.
    MAX_COST = 31
    # Maximum possible size of bcrypt() secrets.
    # Older versions of the bcrypt library would truncate passwords longer
    # than 72 bytes, but newer ones do not. We truncate like the old library for
    # forward compatibility. This way users upgrading from Ubuntu 18.04 to 20.04
    # will not have their user passwords invalidated, for example.
    # A max secret length greater than 255 leads to bcrypt returning nil.
    # https://github.com/bcrypt-ruby/bcrypt-ruby/issues/225#issuecomment-875908425
    MAX_SECRET_BYTESIZE = 72
    # Maximum possible size of bcrypt() salts.
    MAX_SALT_LENGTH = 16

    if RUBY_PLATFORM != "java"
      # C-level routines which, if they don't get the right input, will crash the
      # hell out of the Ruby process.
      private_class_method :__bc_salt
      private_class_method :__bc_crypt
    end

    @cost = nil

    # Returns the cost factor that will be used if one is not specified when
    # creating a password hash.  Defaults to DEFAULT_COST if not set.
    def self.cost
      @cost || DEFAULT_COST
    end

    # Set a default cost factor that will be used if one is not specified when
    # creating a password hash.
    #
    # Example:
    #
    #   BCrypt::Engine::DEFAULT_COST            #=> 12
    #   BCrypt::Password.create('secret').cost  #=> 12
    #
    #   BCrypt::Engine.cost = 8
    #   BCrypt::Password.create('secret').cost  #=> 8
    #
    #   # cost can still be overridden as needed
    #   BCrypt::Password.create('secret', :cost => 6).cost  #=> 6
    def self.cost=(cost)
      @cost = cost
    end

    # Given a secret and a valid salt (see BCrypt::Engine.generate_salt) calculates
    # a bcrypt() password hash. Secrets longer than 72 bytes are truncated.
    def self.hash_secret(secret, salt, _ = nil)
      unless _.nil?
        warn "[DEPRECATION] Passing the third argument to " \
             "`BCrypt::Engine.hash_secret` is deprecated. " \
             "Please do not pass the third argument which " \
             "is currently not used."
      end

      if valid_secret?(secret)
        if valid_salt?(salt)
          if RUBY_PLATFORM == "java"
            Java.bcrypt_jruby.BCrypt.hashpw(secret.to_s.to_java_bytes, salt.to_s)
          else
            secret = secret.to_s
            secret = secret.byteslice(0, MAX_SECRET_BYTESIZE) if secret && secret.bytesize > MAX_SECRET_BYTESIZE
            __bc_crypt(secret, salt)
          end
        else
          raise Errors::InvalidSalt.new("invalid salt")
        end
      else
        raise Errors::InvalidSecret.new("invalid secret")
      end
    end

    # Generates a random salt with a given computational cost.
    def self.generate_salt(cost = self.cost)
      cost = cost.to_i
      if cost > 0
        if cost < MIN_COST
          cost = MIN_COST
        end
        if RUBY_PLATFORM == "java"
          Java.bcrypt_jruby.BCrypt.gensalt(cost)
        else
          __bc_salt("$2a$", cost, OpenSSL::Random.random_bytes(MAX_SALT_LENGTH))
        end
      else
        raise Errors::InvalidCost.new("cost must be numeric and > 0")
      end
    end

    # Returns true if +salt+ is a valid bcrypt() salt, false if not.
    def self.valid_salt?(salt)
      !!(salt =~ /\A\$[0-9a-z]{2,}\$[0-9]{2,}\$[A-Za-z0-9\.\/]{22,}\z/)
    end

    # Returns true if +secret+ is a valid bcrypt() secret, false if not.
    def self.valid_secret?(secret)
      secret.respond_to?(:to_s)
    end

    # Returns the cost factor which will result in computation times less than +upper_time_limit_in_ms+.
    #
    # Example:
    #
    #   BCrypt::Engine.calibrate(200)  #=> 10
    #   BCrypt::Engine.calibrate(1000) #=> 12
    #
    #   # should take less than 200ms
    #   BCrypt::Password.create("woo", :cost => 10)
    #
    #   # should take less than 1000ms
    #   BCrypt::Password.create("woo", :cost => 12)
    def self.calibrate(upper_time_limit_in_ms)
      (BCrypt::Engine::MIN_COST..BCrypt::Engine::MAX_COST-1).each do |i|
        start_time = Time.now
        Password.create("testing testing", :cost => i+1)
        end_time = Time.now - start_time
        return i if end_time * 1_000 > upper_time_limit_in_ms
      end
    end

    # Autodetects the cost from the salt string.
    def self.autodetect_cost(salt)
      salt[4..5].to_i
    end
  end

end