Class: SCrypt::Engine

Inherits:
Object
  • Object
show all
Defined in:
lib/scrypt.rb

Defined Under Namespace

Classes: Calibration

Constant Summary

DEFAULTS =
{
  :key_len     => 32,
  :salt_size   => 8,
  :max_mem     => 1024 * 1024,
  :max_memfrac => 0.5,
  :max_time    => 0.2
}

Class Method Summary collapse

Class Method Details

.autodetect_cost(salt) ⇒ Object

Autodetects the cost from the salt string.



121
122
123
# File 'lib/scrypt.rb', line 121

def self.autodetect_cost(salt)
  salt[/^[0-9a-z]+\$[0-9a-z]+\$[0-9a-z]+\$/]
end

.calibrate(options = {}) ⇒ Object

Returns the cost value which will result in computation limits less than the given options.

Options: :max_time specifies the maximum number of seconds the computation should take. :max_mem specifies the maximum number of bytes the computation should take. A value of 0 specifies no upper limit. The minimum is always 1 MB. :max_memfrac specifies the maximum memory in a fraction of available resources to use. Any value equal to 0 or greater than 0.5 will result in 0.5 being used.

Example:

# should take less than 200ms
SCrypt::Engine.calibrate(:max_time => 0.2)


109
110
111
112
# File 'lib/scrypt.rb', line 109

def self.calibrate(options = {})
  options = DEFAULTS.merge(options)
  "%x$%x$%x$" % __sc_calibrate(options[:max_mem], options[:max_memfrac], options[:max_time])
end

.generate_salt(options = {}) ⇒ Object

Generates a random salt with a given computational cost.



71
72
73
74
75
76
77
78
79
80
# File 'lib/scrypt.rb', line 71

def self.generate_salt(options = {})
  options = DEFAULTS.merge(options)
  cost = calibrate(options)
  salt = OpenSSL::Random.random_bytes(options[:salt_size]).unpack('H*').first.rjust(16,'0')
  if salt.length == 40
    #If salt is 40 characters, the regexp will think that it is an old-style hash, so add a '0'.
    salt = '0' + salt
  end
  cost + salt
end

.hash_secret(secret, salt, key_len = DEFAULTS[:key_len]) ⇒ Object

Given a secret and a valid salt (see SCrypt::Engine.generate_salt) calculates an scrypt password hash.



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/scrypt.rb', line 49

def self.hash_secret(secret, salt, key_len = DEFAULTS[:key_len])
  if valid_secret?(secret)
    if valid_salt?(salt)
      cost = autodetect_cost(salt)
      salt_only = salt[/\$([A-Za-z0-9]{16,64})$/, 1]
      if salt_only.length == 40
        # Old-style hash with 40-character salt
        salt + "$" + Digest::SHA1.hexdigest(scrypt(secret.to_s, salt, cost, 256))
      else
        # New-style hash
        salt_only = [salt_only.sub(/^(00)+/, '')].pack('H*')
        salt + "$" + scrypt(secret.to_s, salt_only, cost, key_len).unpack('H*').first.rjust(key_len * 2, '0')
      end
    else
      raise Errors::InvalidSalt.new("invalid salt")
    end
  else
    raise Errors::InvalidSecret.new("invalid secret")
  end
end

.memory_use(cost) ⇒ Object

Computes the memory use of the given cost



115
116
117
118
# File 'lib/scrypt.rb', line 115

def self.memory_use(cost)
  n, r, p = cost.scanf("%x$%x$%x$")
  (128 * r * p) + (256 * r) + (128 * r * n);
end

.scrypt(secret, salt, *args) ⇒ Object



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/scrypt.rb', line 32

def self.scrypt(secret, salt, *args)
  if args.length == 2
    # args is [cost_string, key_len]
    n, r, p = args[0].split('$').map{ |x| x.to_i(16) }
    key_len = args[1]
    __sc_crypt(secret, salt, n, r, p, key_len)
  elsif args.length == 4
    # args is [n, r, p, key_len]
    n, r, p = args[0, 3]
    key_len = args[3]
    __sc_crypt(secret, salt, n, r, p, key_len)
  else
    raise ArgumentError.new("invalid number of arguments (4 or 6)")
  end
end

.valid_cost?(cost) ⇒ Boolean

Returns true if cost is a valid cost, false if not.

Returns:

  • (Boolean)


83
84
85
# File 'lib/scrypt.rb', line 83

def self.valid_cost?(cost)
  cost.match(/^[0-9a-z]+\$[0-9a-z]+\$[0-9a-z]+\$$/) != nil
end

.valid_salt?(salt) ⇒ Boolean

Returns true if salt is a valid salt, false if not.

Returns:

  • (Boolean)


88
89
90
# File 'lib/scrypt.rb', line 88

def self.valid_salt?(salt)
  salt.match(/^[0-9a-z]+\$[0-9a-z]+\$[0-9a-z]+\$[A-Za-z0-9]{16,64}$/) != nil
end

.valid_secret?(secret) ⇒ Boolean

Returns true if secret is a valid secret, false if not.

Returns:

  • (Boolean)


93
94
95
# File 'lib/scrypt.rb', line 93

def self.valid_secret?(secret)
  secret.respond_to?(:to_s)
end