scrypt 3.0.6 → 3.0.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/Rakefile +6 -5
- data/ext/scrypt/Rakefile +1 -1
- data/lib/scrypt/engine.rb +175 -0
- data/lib/scrypt/errors.rb +14 -0
- data/lib/scrypt/password.rb +97 -0
- data/lib/scrypt/scrypt_ext.rb +3 -0
- data/lib/scrypt/security_utils.rb +4 -3
- data/lib/scrypt/version.rb +3 -1
- data/lib/scrypt.rb +7 -266
- data/scrypt.gemspec +39 -20
- data/spec/scrypt/engine_spec.rb +34 -37
- data/spec/scrypt/password_spec.rb +56 -58
- data/spec/scrypt/utils_spec.rb +5 -3
- data/spec/spec_helper.rb +6 -3
- data.tar.gz.sig +0 -0
- metadata +126 -42
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8179f4b0bf8f79e8476cc36f386e1fa349efa0db814a7f40a7f67b521a3a80b2
|
4
|
+
data.tar.gz: 99dd37077aa2003bf966336aee1f395cc655fcf61aac44ed12f049e36e66b62a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 94b935e06c8d3e342b5de545cc18a8d4e04ed589d22c9006b90049e39970f1063939b660da88be43f293d67156ec394b238955bb68477bc3aefb0d70b74f8bdd
|
7
|
+
data.tar.gz: e13414929c7bb835c456e7e4ad3db8f4dedf8092cf0e3fd7413fd423c39591b9b80575d9baeea35ced8d70cb9c82718d9b3fd6429d2c4452d7a87025013d960b
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/Rakefile
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'bundler/setup'
|
2
4
|
require 'bundler/gem_tasks'
|
3
5
|
|
@@ -17,7 +19,7 @@ require 'rubygems/package_task'
|
|
17
19
|
|
18
20
|
require 'rdoc/task'
|
19
21
|
|
20
|
-
task :
|
22
|
+
task default: [:clean, :compile_ffi, :spec]
|
21
23
|
|
22
24
|
desc 'clean, make and run specs'
|
23
25
|
task :spec do
|
@@ -29,7 +31,7 @@ task :checksum do
|
|
29
31
|
built_gem_path = "pkg/scrypt-#{SCrypt::VERSION}.gem"
|
30
32
|
checksum = Digest::SHA512.new.hexdigest(File.read(built_gem_path))
|
31
33
|
checksum_path = "checksum/scrypt-#{SCrypt::VERSION}.gem.sha512"
|
32
|
-
File.open(checksum_path, 'w'
|
34
|
+
File.open(checksum_path, 'w') { |f| f.write(checksum) }
|
33
35
|
end
|
34
36
|
|
35
37
|
desc 'FFI compiler'
|
@@ -40,7 +42,7 @@ namespace 'ffi-compiler' do
|
|
40
42
|
t.cflags << '-Wall -std=c99'
|
41
43
|
t.cflags << '-msse -msse2' if t.platform.arch.include? '86'
|
42
44
|
t.cflags << '-D_GNU_SOURCE=1' if RbConfig::CONFIG['host_os'].downcase =~ /mingw/
|
43
|
-
t.cflags << '-D_POSIX_C_SOURCE=
|
45
|
+
t.cflags << '-D_POSIX_C_SOURCE=200809L' if RbConfig::CONFIG['host_os'].downcase =~ /linux/
|
44
46
|
|
45
47
|
if 1.size == 4 && target_cpu =~ /i386|x86_32/ && t.platform.mac?
|
46
48
|
t.cflags << '-arch i386'
|
@@ -53,7 +55,7 @@ namespace 'ffi-compiler' do
|
|
53
55
|
t.add_define 'WINDOWS_OS' if FFI::Platform.windows?
|
54
56
|
end
|
55
57
|
end
|
56
|
-
task :
|
58
|
+
task compile_ffi: ['ffi-compiler:default']
|
57
59
|
|
58
60
|
CLEAN.include('ext/scrypt/*{.o,.log,.so,.bundle}')
|
59
61
|
CLEAN.include('lib/**/*{.o,.log,.so,.bundle}')
|
@@ -80,4 +82,3 @@ Gem::PackageTask.new(gem_spec) do |pkg|
|
|
80
82
|
pkg.need_tar = true
|
81
83
|
pkg.package_dir = 'pkg'
|
82
84
|
end
|
83
|
-
|
data/ext/scrypt/Rakefile
CHANGED
@@ -6,7 +6,7 @@ FFI::Compiler::CompileTask.new('scrypt_ext') do |t|
|
|
6
6
|
t.cflags << '-Wall -std=c99'
|
7
7
|
t.cflags << '-msse -msse2' if t.platform.arch.include? '86'
|
8
8
|
t.cflags << '-D_GNU_SOURCE=1' if RbConfig::CONFIG['host_os'].downcase =~ /mingw/
|
9
|
-
t.cflags << '-D_POSIX_C_SOURCE=
|
9
|
+
t.cflags << '-D_POSIX_C_SOURCE=200809L' if RbConfig::CONFIG['host_os'].downcase =~ /linux/
|
10
10
|
|
11
11
|
if 1.size == 4 && target_cpu =~ /i386|x86_32/ && t.platform.mac?
|
12
12
|
t.cflags << '-arch i386'
|
@@ -0,0 +1,175 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ffi'
|
4
|
+
require 'openssl'
|
5
|
+
|
6
|
+
module SCrypt
|
7
|
+
module Ext
|
8
|
+
# rubocop:disable Style/SymbolArray
|
9
|
+
# Bind the external functions
|
10
|
+
attach_function :sc_calibrate,
|
11
|
+
[:size_t, :double, :double, :pointer],
|
12
|
+
:int,
|
13
|
+
blocking: true
|
14
|
+
|
15
|
+
attach_function :crypto_scrypt,
|
16
|
+
[:pointer, :size_t, :pointer, :size_t, :uint64, :uint32, :uint32, :pointer, :size_t],
|
17
|
+
:int,
|
18
|
+
blocking: true # todo
|
19
|
+
# rubocop:enable
|
20
|
+
end
|
21
|
+
|
22
|
+
class Engine
|
23
|
+
# rubocop:disable Style/MutableConstant
|
24
|
+
DEFAULTS = {
|
25
|
+
key_len: 32,
|
26
|
+
salt_size: 32,
|
27
|
+
max_mem: 16 * 1024 * 1024,
|
28
|
+
max_memfrac: 0.5,
|
29
|
+
max_time: 0.2,
|
30
|
+
cost: nil
|
31
|
+
}
|
32
|
+
# rubocop:enable
|
33
|
+
|
34
|
+
class Calibration < FFI::Struct
|
35
|
+
layout :n, :uint64,
|
36
|
+
:r, :uint32,
|
37
|
+
:p, :uint32
|
38
|
+
end
|
39
|
+
|
40
|
+
class << self
|
41
|
+
def scrypt(secret, salt, *args)
|
42
|
+
if args.length == 2
|
43
|
+
# args is [cost_string, key_len]
|
44
|
+
n, r, p = args[0].split('$').map { |x| x.to_i(16) }
|
45
|
+
key_len = args[1]
|
46
|
+
|
47
|
+
__sc_crypt(secret, salt, n, r, p, key_len)
|
48
|
+
elsif args.length == 4
|
49
|
+
# args is [n, r, p, key_len]
|
50
|
+
n, r, p = args[0, 3]
|
51
|
+
key_len = args[3]
|
52
|
+
|
53
|
+
__sc_crypt(secret, salt, n, r, p, key_len)
|
54
|
+
else
|
55
|
+
raise ArgumentError, 'invalid number of arguments (4 or 6)'
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Given a secret and a valid salt (see SCrypt::Engine.generate_salt) calculates an scrypt password hash.
|
60
|
+
def hash_secret(secret, salt, key_len = DEFAULTS[:key_len])
|
61
|
+
raise Errors::InvalidSecret, 'invalid secret' unless valid_secret?(secret)
|
62
|
+
raise Errors::InvalidSalt, 'invalid salt' unless valid_salt?(salt)
|
63
|
+
|
64
|
+
cost = autodetect_cost(salt)
|
65
|
+
salt_only = salt[/\$([A-Za-z0-9]{16,64})$/, 1]
|
66
|
+
|
67
|
+
if salt_only.length == 40
|
68
|
+
# Old-style hash with 40-character salt
|
69
|
+
salt + '$' + Digest::SHA1.hexdigest(scrypt(secret.to_s, salt, cost, 256))
|
70
|
+
else
|
71
|
+
# New-style hash
|
72
|
+
salt_only = [salt_only.sub(/^(00)+/, '')].pack('H*')
|
73
|
+
salt + '$' + scrypt(secret.to_s, salt_only, cost, key_len).unpack('H*').first.rjust(key_len * 2, '0')
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Generates a random salt with a given computational cost. Uses a saved
|
78
|
+
# cost if SCrypt::Engine.calibrate! has been called.
|
79
|
+
#
|
80
|
+
# Options:
|
81
|
+
# <tt>:cost</tt> is a cost string returned by SCrypt::Engine.calibrate
|
82
|
+
def generate_salt(options = {})
|
83
|
+
options = DEFAULTS.merge(options)
|
84
|
+
cost = options[:cost] || calibrate(options)
|
85
|
+
salt = OpenSSL::Random.random_bytes(options[:salt_size]).unpack('H*').first.rjust(16, '0')
|
86
|
+
|
87
|
+
if salt.length == 40
|
88
|
+
# If salt is 40 characters, the regexp will think that it is an old-style hash, so add a '0'.
|
89
|
+
salt = '0' + salt
|
90
|
+
end
|
91
|
+
cost + salt
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns true if +cost+ is a valid cost, false if not.
|
95
|
+
def valid_cost?(cost)
|
96
|
+
cost.match(/^[0-9a-z]+\$[0-9a-z]+\$[0-9a-z]+\$$/) != nil
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns true if +salt+ is a valid salt, false if not.
|
100
|
+
def valid_salt?(salt)
|
101
|
+
salt.match(/^[0-9a-z]+\$[0-9a-z]+\$[0-9a-z]+\$[A-Za-z0-9]{16,64}$/) != nil
|
102
|
+
end
|
103
|
+
|
104
|
+
# Returns true if +secret+ is a valid secret, false if not.
|
105
|
+
def valid_secret?(secret)
|
106
|
+
secret.respond_to?(:to_s)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Returns the cost value which will result in computation limits less than the given options.
|
110
|
+
#
|
111
|
+
# Options:
|
112
|
+
# <tt>:max_time</tt> specifies the maximum number of seconds the computation should take.
|
113
|
+
# <tt>:max_mem</tt> specifies the maximum number of bytes the computation should take. A value of 0 specifies no upper limit. The minimum is always 1 MB.
|
114
|
+
# <tt>:max_memfrac</tt> 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.
|
115
|
+
#
|
116
|
+
# Example:
|
117
|
+
#
|
118
|
+
# # should take less than 200ms
|
119
|
+
# SCrypt::Engine.calibrate(:max_time => 0.2)
|
120
|
+
#
|
121
|
+
def calibrate(options = {})
|
122
|
+
options = DEFAULTS.merge(options)
|
123
|
+
'%x$%x$%x$' % __sc_calibrate(options[:max_mem], options[:max_memfrac], options[:max_time])
|
124
|
+
end
|
125
|
+
|
126
|
+
# Calls SCrypt::Engine.calibrate and saves the cost string for future calls to
|
127
|
+
# SCrypt::Engine.generate_salt.
|
128
|
+
def calibrate!(options = {})
|
129
|
+
DEFAULTS[:cost] = calibrate(options)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Computes the memory use of the given +cost+
|
133
|
+
def memory_use(cost)
|
134
|
+
n, r, p = cost.split('$').map { |i| i.to_i(16) }
|
135
|
+
(128 * r * p) + (256 * r) + (128 * r * n)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Autodetects the cost from the salt string.
|
139
|
+
def autodetect_cost(salt)
|
140
|
+
salt[/^[0-9a-z]+\$[0-9a-z]+\$[0-9a-z]+\$/]
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
def __sc_calibrate(max_mem, max_memfrac, max_time)
|
146
|
+
result = nil
|
147
|
+
|
148
|
+
calibration = Calibration.new
|
149
|
+
ret_val = SCrypt::Ext.sc_calibrate(max_mem, max_memfrac, max_time, calibration)
|
150
|
+
|
151
|
+
raise "calibration error #{result}" unless ret_val.zero?
|
152
|
+
|
153
|
+
[calibration[:n], calibration[:r], calibration[:p]]
|
154
|
+
end
|
155
|
+
|
156
|
+
def __sc_crypt(secret, salt, n, r, p, key_len)
|
157
|
+
result = nil
|
158
|
+
|
159
|
+
FFI::MemoryPointer.new(:char, key_len) do |buffer|
|
160
|
+
ret_val = SCrypt::Ext.crypto_scrypt(
|
161
|
+
secret, secret.bytesize, salt, salt.bytesize,
|
162
|
+
n, r, p,
|
163
|
+
buffer, key_len
|
164
|
+
)
|
165
|
+
|
166
|
+
raise "scrypt error #{ret_val}" unless ret_val.zero?
|
167
|
+
|
168
|
+
result = buffer.read_string(key_len)
|
169
|
+
end
|
170
|
+
|
171
|
+
result
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SCrypt
|
4
|
+
module Errors
|
5
|
+
# The salt parameter provided is invalid.
|
6
|
+
class InvalidSalt < StandardError; end
|
7
|
+
|
8
|
+
# The hash parameter provided is invalid.
|
9
|
+
class InvalidHash < StandardError; end
|
10
|
+
|
11
|
+
# The secret parameter provided is invalid.
|
12
|
+
class InvalidSecret < StandardError; end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SCrypt
|
4
|
+
# A password management class which allows you to safely store users' passwords and compare them.
|
5
|
+
#
|
6
|
+
# Example usage:
|
7
|
+
#
|
8
|
+
# include "scrypt"
|
9
|
+
#
|
10
|
+
# # hash a user's password
|
11
|
+
# @password = Password.create("my grand secret")
|
12
|
+
# @password #=> "2000$8$1$f5f2fa5fe5484a7091f1299768fbe92b5a7fbc77$6a385f22c54d92c314b71a4fd5ef33967c93d679"
|
13
|
+
#
|
14
|
+
# # store it safely
|
15
|
+
# @user.update_attribute(:password, @password)
|
16
|
+
#
|
17
|
+
# # read it back
|
18
|
+
# @user.reload!
|
19
|
+
# @db_password = Password.new(@user.password)
|
20
|
+
#
|
21
|
+
# # compare it after retrieval
|
22
|
+
# @db_password == "my grand secret" #=> true
|
23
|
+
# @db_password == "a paltry guess" #=> false
|
24
|
+
#
|
25
|
+
class Password < String
|
26
|
+
# The hash portion of the stored password hash.
|
27
|
+
attr_reader :digest
|
28
|
+
# The salt of the store password hash
|
29
|
+
attr_reader :salt
|
30
|
+
# The cost factor used to create the hash.
|
31
|
+
attr_reader :cost
|
32
|
+
|
33
|
+
class << self
|
34
|
+
# Hashes a secret, returning a SCrypt::Password instance.
|
35
|
+
# Takes five options (optional), which will determine the salt/key's length and the cost limits of the computation.
|
36
|
+
# <tt>:key_len</tt> specifies the length in bytes of the key you want to generate. The default is 32 bytes (256 bits). Minimum is 16 bytes (128 bits). Maximum is 512 bytes (4096 bits).
|
37
|
+
# <tt>:salt_size</tt> specifies the size in bytes of the random salt you want to generate. The default and minimum is 8 bytes (64 bits). Maximum is 32 bytes (256 bits).
|
38
|
+
# <tt>:max_time</tt> specifies the maximum number of seconds the computation should take.
|
39
|
+
# <tt>:max_mem</tt> specifies the maximum number of bytes the computation should take. A value of 0 specifies no upper limit. The minimum is always 1 MB.
|
40
|
+
# <tt>:max_memfrac</tt> 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.
|
41
|
+
# The scrypt key derivation function is designed to be far more secure against hardware brute-force attacks than alternative functions such as PBKDF2 or bcrypt.
|
42
|
+
# The designers of scrypt estimate that on modern (2009) hardware, if 5 seconds are spent computing a derived key, the cost of a hardware brute-force attack against scrypt is roughly 4000 times greater than the cost of a similar attack against bcrypt (to find the same password), and 20000 times greater than a similar attack against PBKDF2.
|
43
|
+
# Default options will result in calculation time of approx. 200 ms with 1 MB memory use.
|
44
|
+
#
|
45
|
+
# Example:
|
46
|
+
# @password = SCrypt::Password.create("my secret", :max_time => 0.25)
|
47
|
+
#
|
48
|
+
def create(secret, options = {})
|
49
|
+
options = SCrypt::Engine::DEFAULTS.merge(options)
|
50
|
+
|
51
|
+
# Clamp minimum/maximum keylen
|
52
|
+
options[:key_len] = 16 if options[:key_len] < 16
|
53
|
+
options[:key_len] = 512 if options[:key_len] > 512
|
54
|
+
|
55
|
+
# Clamp minimum/maximum salt_size
|
56
|
+
options[:salt_size] = 8 if options[:salt_size] < 8
|
57
|
+
options[:salt_size] = 32 if options[:salt_size] > 32
|
58
|
+
|
59
|
+
salt = SCrypt::Engine.generate_salt(options)
|
60
|
+
hash = SCrypt::Engine.hash_secret(secret, salt, options[:key_len])
|
61
|
+
|
62
|
+
Password.new(hash)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Initializes a SCrypt::Password instance with the data from a stored hash.
|
67
|
+
def initialize(raw_hash)
|
68
|
+
raise Errors::InvalidHash, 'invalid hash' unless valid_hash?(raw_hash)
|
69
|
+
|
70
|
+
replace(raw_hash)
|
71
|
+
|
72
|
+
@cost, @salt, @digest = split_hash(to_s)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Compares a potential secret against the hash. Returns true if the secret is the original secret, false otherwise.
|
76
|
+
def ==(other)
|
77
|
+
SecurityUtils.secure_compare(self, SCrypt::Engine.hash_secret(other, @cost + @salt, digest.length / 2))
|
78
|
+
end
|
79
|
+
alias is_password? ==
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
# Returns true if +h+ is a valid hash.
|
84
|
+
def valid_hash?(h)
|
85
|
+
h.match(/^[0-9a-z]+\$[0-9a-z]+\$[0-9a-z]+\$[A-Za-z0-9]{16,64}\$[A-Za-z0-9]{32,1024}$/) != nil
|
86
|
+
end
|
87
|
+
|
88
|
+
# call-seq:
|
89
|
+
# split_hash(raw_hash) -> cost, salt, hash
|
90
|
+
#
|
91
|
+
# Splits +h+ into cost, salt, and hash and returns them in that order.
|
92
|
+
def split_hash(h)
|
93
|
+
n, v, r, salt, hash = h.split('$')
|
94
|
+
[[n, v, r].join('$') + '$', salt, hash]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
data/lib/scrypt/scrypt_ext.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# NOTE:: a verbatim copy of https://github.com/rails/rails/blob/c8c660002f4b0e9606de96325f20b95248b6ff2d/activesupport/lib/active_support/security_utils.rb
|
2
4
|
# Please see the Rails license: https://github.com/rails/rails/blob/master/activesupport/MIT-LICENSE
|
3
5
|
|
@@ -9,15 +11,14 @@ module SCrypt
|
|
9
11
|
# that have already been processed by HMAC. This should not be used
|
10
12
|
# on variable length plaintext strings because it could leak length info
|
11
13
|
# via timing attacks.
|
12
|
-
def secure_compare(a, b)
|
14
|
+
def self.secure_compare(a, b)
|
13
15
|
return false unless a.bytesize == b.bytesize
|
14
16
|
|
15
17
|
l = a.unpack "C#{a.bytesize}"
|
16
18
|
|
17
19
|
res = 0
|
18
20
|
b.each_byte { |byte| res |= byte ^ l.shift }
|
19
|
-
res
|
21
|
+
res.zero?
|
20
22
|
end
|
21
|
-
module_function :secure_compare
|
22
23
|
end
|
23
24
|
end
|
data/lib/scrypt/version.rb
CHANGED
data/lib/scrypt.rb
CHANGED
@@ -1,269 +1,10 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
require "scrypt/scrypt_ext"
|
4
|
-
require "scrypt/security_utils"
|
5
|
-
require "openssl"
|
6
|
-
require "scanf"
|
7
|
-
require "ffi"
|
8
|
-
|
9
|
-
|
10
|
-
module SCrypt
|
11
|
-
|
12
|
-
module Ext
|
13
|
-
# Bind the external functions
|
14
|
-
attach_function :sc_calibrate, [:size_t, :double, :double, :pointer], :int, :blocking => true
|
15
|
-
attach_function :crypto_scrypt, [:pointer, :size_t, :pointer, :size_t, :uint64, :uint32, :uint32, :pointer, :size_t], :int, :blocking => true # todo
|
16
|
-
end
|
17
|
-
|
18
|
-
module Errors
|
19
|
-
class InvalidSalt < StandardError; end # The salt parameter provided is invalid.
|
20
|
-
class InvalidHash < StandardError; end # The hash parameter provided is invalid.
|
21
|
-
class InvalidSecret < StandardError; end # The secret parameter provided is invalid.
|
22
|
-
end
|
23
|
-
|
24
|
-
class Engine
|
25
|
-
DEFAULTS = {
|
26
|
-
:key_len => 32,
|
27
|
-
:salt_size => 32,
|
28
|
-
:max_mem => 16 * 1024 * 1024,
|
29
|
-
:max_memfrac => 0.5,
|
30
|
-
:max_time => 0.2,
|
31
|
-
:cost => nil
|
32
|
-
}
|
33
|
-
|
34
|
-
def self.scrypt(secret, salt, *args)
|
35
|
-
if args.length == 2
|
36
|
-
# args is [cost_string, key_len]
|
37
|
-
n, r, p = args[0].split('$').map{ |x| x.to_i(16) }
|
38
|
-
key_len = args[1]
|
39
|
-
__sc_crypt(secret, salt, n, r, p, key_len)
|
40
|
-
elsif args.length == 4
|
41
|
-
# args is [n, r, p, key_len]
|
42
|
-
n, r, p = args[0, 3]
|
43
|
-
key_len = args[3]
|
44
|
-
__sc_crypt(secret, salt, n, r, p, key_len)
|
45
|
-
else
|
46
|
-
raise ArgumentError.new("invalid number of arguments (4 or 6)")
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
# Given a secret and a valid salt (see SCrypt::Engine.generate_salt) calculates an scrypt password hash.
|
51
|
-
def self.hash_secret(secret, salt, key_len = DEFAULTS[:key_len])
|
52
|
-
if valid_secret?(secret)
|
53
|
-
if valid_salt?(salt)
|
54
|
-
cost = autodetect_cost(salt)
|
55
|
-
salt_only = salt[/\$([A-Za-z0-9]{16,64})$/, 1]
|
56
|
-
if salt_only.length == 40
|
57
|
-
# Old-style hash with 40-character salt
|
58
|
-
salt + "$" + Digest::SHA1.hexdigest(scrypt(secret.to_s, salt, cost, 256))
|
59
|
-
else
|
60
|
-
# New-style hash
|
61
|
-
salt_only = [salt_only.sub(/^(00)+/, '')].pack('H*')
|
62
|
-
salt + "$" + scrypt(secret.to_s, salt_only, cost, key_len).unpack('H*').first.rjust(key_len * 2, '0')
|
63
|
-
end
|
64
|
-
else
|
65
|
-
raise Errors::InvalidSalt.new("invalid salt")
|
66
|
-
end
|
67
|
-
else
|
68
|
-
raise Errors::InvalidSecret.new("invalid secret")
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
# Generates a random salt with a given computational cost. Uses a saved
|
73
|
-
# cost if SCrypt::Engine.calibrate! has been called.
|
74
|
-
#
|
75
|
-
# Options:
|
76
|
-
# <tt>:cost</tt> is a cost string returned by SCrypt::Engine.calibrate
|
77
|
-
def self.generate_salt(options = {})
|
78
|
-
options = DEFAULTS.merge(options)
|
79
|
-
cost = options[:cost] || calibrate(options)
|
80
|
-
salt = OpenSSL::Random.random_bytes(options[:salt_size]).unpack('H*').first.rjust(16,'0')
|
81
|
-
if salt.length == 40
|
82
|
-
#If salt is 40 characters, the regexp will think that it is an old-style hash, so add a '0'.
|
83
|
-
salt = '0' + salt
|
84
|
-
end
|
85
|
-
cost + salt
|
86
|
-
end
|
87
|
-
|
88
|
-
# Returns true if +cost+ is a valid cost, false if not.
|
89
|
-
def self.valid_cost?(cost)
|
90
|
-
cost.match(/^[0-9a-z]+\$[0-9a-z]+\$[0-9a-z]+\$$/) != nil
|
91
|
-
end
|
92
|
-
|
93
|
-
# Returns true if +salt+ is a valid salt, false if not.
|
94
|
-
def self.valid_salt?(salt)
|
95
|
-
salt.match(/^[0-9a-z]+\$[0-9a-z]+\$[0-9a-z]+\$[A-Za-z0-9]{16,64}$/) != nil
|
96
|
-
end
|
97
|
-
|
98
|
-
# Returns true if +secret+ is a valid secret, false if not.
|
99
|
-
def self.valid_secret?(secret)
|
100
|
-
secret.respond_to?(:to_s)
|
101
|
-
end
|
102
|
-
|
103
|
-
# Returns the cost value which will result in computation limits less than the given options.
|
104
|
-
#
|
105
|
-
# Options:
|
106
|
-
# <tt>:max_time</tt> specifies the maximum number of seconds the computation should take.
|
107
|
-
# <tt>:max_mem</tt> specifies the maximum number of bytes the computation should take. A value of 0 specifies no upper limit. The minimum is always 1 MB.
|
108
|
-
# <tt>:max_memfrac</tt> 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.
|
109
|
-
#
|
110
|
-
# Example:
|
111
|
-
#
|
112
|
-
# # should take less than 200ms
|
113
|
-
# SCrypt::Engine.calibrate(:max_time => 0.2)
|
114
|
-
#
|
115
|
-
def self.calibrate(options = {})
|
116
|
-
options = DEFAULTS.merge(options)
|
117
|
-
"%x$%x$%x$" % __sc_calibrate(options[:max_mem], options[:max_memfrac], options[:max_time])
|
118
|
-
end
|
119
|
-
|
120
|
-
# Calls SCrypt::Engine.calibrate and saves the cost string for future calls to
|
121
|
-
# SCrypt::Engine.generate_salt.
|
122
|
-
def self.calibrate!(options = {})
|
123
|
-
DEFAULTS[:cost] = calibrate(options)
|
124
|
-
end
|
125
|
-
|
126
|
-
# Computes the memory use of the given +cost+
|
127
|
-
def self.memory_use(cost)
|
128
|
-
n, r, p = cost.scanf("%x$%x$%x$")
|
129
|
-
(128 * r * p) + (256 * r) + (128 * r * n);
|
130
|
-
end
|
1
|
+
# frozen_string_literal: true
|
131
2
|
|
132
|
-
|
133
|
-
def self.autodetect_cost(salt)
|
134
|
-
salt[/^[0-9a-z]+\$[0-9a-z]+\$[0-9a-z]+\$/]
|
135
|
-
end
|
136
|
-
|
137
|
-
private
|
138
|
-
|
139
|
-
class Calibration < FFI::Struct
|
140
|
-
layout :n, :uint64,
|
141
|
-
:r, :uint32,
|
142
|
-
:p, :uint32
|
143
|
-
end
|
144
|
-
|
145
|
-
def self.__sc_calibrate(max_mem, max_memfrac, max_time)
|
146
|
-
result = nil
|
147
|
-
|
148
|
-
calibration = Calibration.new
|
149
|
-
retval = SCrypt::Ext.sc_calibrate(max_mem, max_memfrac, max_time, calibration)
|
150
|
-
|
151
|
-
if retval == 0
|
152
|
-
result = [calibration[:n], calibration[:r], calibration[:p]]
|
153
|
-
else
|
154
|
-
raise "calibration error #{result}"
|
155
|
-
end
|
156
|
-
|
157
|
-
result
|
158
|
-
end
|
159
|
-
|
160
|
-
def self.__sc_crypt(secret, salt, n, r, p, key_len)
|
161
|
-
result = nil
|
162
|
-
|
163
|
-
FFI::MemoryPointer.new(:char, key_len) do |buffer|
|
164
|
-
retval = SCrypt::Ext.crypto_scrypt(
|
165
|
-
secret, secret.bytesize, salt, salt.bytesize,
|
166
|
-
n, r, p,
|
167
|
-
buffer, key_len
|
168
|
-
)
|
169
|
-
if retval == 0
|
170
|
-
result = buffer.read_string(key_len)
|
171
|
-
else
|
172
|
-
raise "scrypt error #{retval}"
|
173
|
-
end
|
174
|
-
end
|
175
|
-
|
176
|
-
result
|
177
|
-
end
|
178
|
-
end
|
179
|
-
|
180
|
-
# A password management class which allows you to safely store users' passwords and compare them.
|
181
|
-
#
|
182
|
-
# Example usage:
|
183
|
-
#
|
184
|
-
# include "scrypt"
|
185
|
-
#
|
186
|
-
# # hash a user's password
|
187
|
-
# @password = Password.create("my grand secret")
|
188
|
-
# @password #=> "2000$8$1$f5f2fa5fe5484a7091f1299768fbe92b5a7fbc77$6a385f22c54d92c314b71a4fd5ef33967c93d679"
|
189
|
-
#
|
190
|
-
# # store it safely
|
191
|
-
# @user.update_attribute(:password, @password)
|
192
|
-
#
|
193
|
-
# # read it back
|
194
|
-
# @user.reload!
|
195
|
-
# @db_password = Password.new(@user.password)
|
196
|
-
#
|
197
|
-
# # compare it after retrieval
|
198
|
-
# @db_password == "my grand secret" #=> true
|
199
|
-
# @db_password == "a paltry guess" #=> false
|
200
|
-
#
|
201
|
-
class Password < String
|
202
|
-
# The hash portion of the stored password hash.
|
203
|
-
attr_reader :digest
|
204
|
-
# The salt of the store password hash
|
205
|
-
attr_reader :salt
|
206
|
-
# The cost factor used to create the hash.
|
207
|
-
attr_reader :cost
|
208
|
-
|
209
|
-
class << self
|
210
|
-
# Hashes a secret, returning a SCrypt::Password instance.
|
211
|
-
# Takes five options (optional), which will determine the salt/key's length and the cost limits of the computation.
|
212
|
-
# <tt>:key_len</tt> specifies the length in bytes of the key you want to generate. The default is 32 bytes (256 bits). Minimum is 16 bytes (128 bits). Maximum is 512 bytes (4096 bits).
|
213
|
-
# <tt>:salt_size</tt> specifies the size in bytes of the random salt you want to generate. The default and minimum is 8 bytes (64 bits). Maximum is 32 bytes (256 bits).
|
214
|
-
# <tt>:max_time</tt> specifies the maximum number of seconds the computation should take.
|
215
|
-
# <tt>:max_mem</tt> specifies the maximum number of bytes the computation should take. A value of 0 specifies no upper limit. The minimum is always 1 MB.
|
216
|
-
# <tt>:max_memfrac</tt> 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.
|
217
|
-
# The scrypt key derivation function is designed to be far more secure against hardware brute-force attacks than alternative functions such as PBKDF2 or bcrypt.
|
218
|
-
# The designers of scrypt estimate that on modern (2009) hardware, if 5 seconds are spent computing a derived key, the cost of a hardware brute-force attack against scrypt is roughly 4000 times greater than the cost of a similar attack against bcrypt (to find the same password), and 20000 times greater than a similar attack against PBKDF2.
|
219
|
-
# Default options will result in calculation time of approx. 200 ms with 1 MB memory use.
|
220
|
-
#
|
221
|
-
# Example:
|
222
|
-
# @password = SCrypt::Password.create("my secret", :max_time => 0.25)
|
223
|
-
#
|
224
|
-
def create(secret, options = {})
|
225
|
-
options = SCrypt::Engine::DEFAULTS.merge(options)
|
226
|
-
#Clamp minimum/maximum keylen
|
227
|
-
options[:key_len] = 16 if options[:key_len] < 16
|
228
|
-
options[:key_len] = 512 if options[:key_len] > 512
|
229
|
-
#Clamp minimum/maximum salt_size
|
230
|
-
options[:salt_size] = 8 if options[:salt_size] < 8
|
231
|
-
options[:salt_size] = 32 if options[:salt_size] > 32
|
232
|
-
salt = SCrypt::Engine.generate_salt(options)
|
233
|
-
hash = SCrypt::Engine.hash_secret(secret, salt, options[:key_len])
|
234
|
-
Password.new(hash)
|
235
|
-
end
|
236
|
-
end
|
237
|
-
|
238
|
-
# Initializes a SCrypt::Password instance with the data from a stored hash.
|
239
|
-
def initialize(raw_hash)
|
240
|
-
if valid_hash?(raw_hash)
|
241
|
-
self.replace(raw_hash)
|
242
|
-
@cost, @salt, @digest = split_hash(self.to_s)
|
243
|
-
else
|
244
|
-
raise Errors::InvalidHash.new("invalid hash")
|
245
|
-
end
|
246
|
-
end
|
247
|
-
|
248
|
-
# Compares a potential secret against the hash. Returns true if the secret is the original secret, false otherwise.
|
249
|
-
def ==(secret)
|
250
|
-
SecurityUtils.secure_compare(self, SCrypt::Engine.hash_secret(secret, @cost + @salt, self.digest.length / 2))
|
251
|
-
end
|
252
|
-
alias_method :is_password?, :==
|
3
|
+
# A wrapper for the scrypt algorithm.
|
253
4
|
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
h.match(/^[0-9a-z]+\$[0-9a-z]+\$[0-9a-z]+\$[A-Za-z0-9]{16,64}\$[A-Za-z0-9]{32,1024}$/) != nil
|
258
|
-
end
|
5
|
+
require 'scrypt/errors'
|
6
|
+
require 'scrypt/scrypt_ext'
|
7
|
+
require 'scrypt/security_utils'
|
259
8
|
|
260
|
-
|
261
|
-
|
262
|
-
#
|
263
|
-
# Splits +h+ into cost, salt, and hash and returns them in that order.
|
264
|
-
def split_hash(h)
|
265
|
-
n, v, r, salt, hash = h.split('$')
|
266
|
-
return [n, v, r].join('$') + "$", salt, hash
|
267
|
-
end
|
268
|
-
end
|
269
|
-
end
|
9
|
+
require 'scrypt/engine'
|
10
|
+
require 'scrypt/password'
|