scrypt 3.0.7 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/README.md +149 -23
- data/Rakefile +36 -23
- data/ext/scrypt/Rakefile +1 -1
- data/ext/scrypt/warnp.c +7 -1
- data/lib/scrypt/engine.rb +95 -43
- data/lib/scrypt/password.rb +48 -18
- data/lib/scrypt/scrypt_ext.rb +5 -1
- data/lib/scrypt/version.rb +1 -1
- data/scrypt.gemspec +7 -20
- data/spec/fixtures/test_vectors.yml +67 -0
- data/spec/scrypt/engine_spec.rb +159 -30
- data/spec/scrypt/ffi_spec.rb +33 -0
- data/spec/scrypt/integration_spec.rb +129 -0
- data/spec/scrypt/password_spec.rb +104 -56
- data/spec/scrypt/utils_spec.rb +48 -7
- data/spec/spec_helper.rb +40 -0
- data/spec/support/shared_examples.rb +46 -0
- data/spec/support/test_helpers.rb +47 -0
- data.tar.gz.sig +0 -0
- metadata +39 -168
- metadata.gz.sig +0 -0
- data/autotest/discover.rb +0 -1
data/lib/scrypt/password.rb
CHANGED
@@ -23,6 +23,13 @@ module SCrypt
|
|
23
23
|
# @db_password == "a paltry guess" #=> false
|
24
24
|
#
|
25
25
|
class Password < String
|
26
|
+
# Key length constraints
|
27
|
+
MIN_KEY_LENGTH = 16
|
28
|
+
MAX_KEY_LENGTH = 512
|
29
|
+
# Salt size constraints
|
30
|
+
MIN_SALT_SIZE = 8
|
31
|
+
MAX_SALT_SIZE = 32
|
32
|
+
|
26
33
|
# The hash portion of the stored password hash.
|
27
34
|
attr_reader :digest
|
28
35
|
# The salt of the store password hash
|
@@ -32,14 +39,23 @@ module SCrypt
|
|
32
39
|
|
33
40
|
class << self
|
34
41
|
# Hashes a secret, returning a SCrypt::Password instance.
|
35
|
-
# Takes five options (optional), which will determine the salt/key's length and
|
36
|
-
#
|
37
|
-
# <tt>:
|
42
|
+
# Takes five options (optional), which will determine the salt/key's length and
|
43
|
+
# the cost limits of the computation.
|
44
|
+
# <tt>:key_len</tt> specifies the length in bytes of the key you want to generate.
|
45
|
+
# The default is 32 bytes (256 bits). Minimum is 16 bytes (128 bits). Maximum is 512 bytes (4096 bits).
|
46
|
+
# <tt>:salt_size</tt> specifies the size in bytes of the random salt you want to generate.
|
47
|
+
# The default and minimum is 8 bytes (64 bits). Maximum is 32 bytes (256 bits).
|
38
48
|
# <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.
|
40
|
-
#
|
41
|
-
#
|
42
|
-
#
|
49
|
+
# <tt>:max_mem</tt> specifies the maximum number of bytes the computation should take.
|
50
|
+
# A value of 0 specifies no upper limit. The minimum is always 1 MB.
|
51
|
+
# <tt>:max_memfrac</tt> specifies the maximum memory in a fraction of available resources to use.
|
52
|
+
# Any value equal to 0 or greater than 0.5 will result in 0.5 being used.
|
53
|
+
# The scrypt key derivation function is designed to be far more secure against hardware
|
54
|
+
# brute-force attacks than alternative functions such as PBKDF2 or bcrypt.
|
55
|
+
# The designers of scrypt estimate that on modern (2009) hardware, if 5 seconds are spent
|
56
|
+
# computing a derived key, the cost of a hardware brute-force attack against scrypt is roughly
|
57
|
+
# 4000 times greater than the cost of a similar attack against bcrypt (to find the same password),
|
58
|
+
# and 20000 times greater than a similar attack against PBKDF2.
|
43
59
|
# Default options will result in calculation time of approx. 200 ms with 1 MB memory use.
|
44
60
|
#
|
45
61
|
# Example:
|
@@ -48,19 +64,32 @@ module SCrypt
|
|
48
64
|
def create(secret, options = {})
|
49
65
|
options = SCrypt::Engine::DEFAULTS.merge(options)
|
50
66
|
|
51
|
-
|
52
|
-
options[:
|
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
|
67
|
+
options[:key_len] = clamp_key_length(options[:key_len])
|
68
|
+
options[:salt_size] = clamp_salt_size(options[:salt_size])
|
58
69
|
|
59
70
|
salt = SCrypt::Engine.generate_salt(options)
|
60
71
|
hash = SCrypt::Engine.hash_secret(secret, salt, options[:key_len])
|
61
72
|
|
62
73
|
Password.new(hash)
|
63
74
|
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
# Clamps key length to valid range
|
79
|
+
def clamp_key_length(key_len)
|
80
|
+
return MIN_KEY_LENGTH if key_len < MIN_KEY_LENGTH
|
81
|
+
return MAX_KEY_LENGTH if key_len > MAX_KEY_LENGTH
|
82
|
+
|
83
|
+
key_len
|
84
|
+
end
|
85
|
+
|
86
|
+
# Clamps salt size to valid range
|
87
|
+
def clamp_salt_size(salt_size)
|
88
|
+
return MIN_SALT_SIZE if salt_size < MIN_SALT_SIZE
|
89
|
+
return MAX_SALT_SIZE if salt_size > MAX_SALT_SIZE
|
90
|
+
|
91
|
+
salt_size
|
92
|
+
end
|
64
93
|
end
|
65
94
|
|
66
95
|
# Initializes a SCrypt::Password instance with the data from a stored hash.
|
@@ -82,16 +111,17 @@ module SCrypt
|
|
82
111
|
|
83
112
|
# Returns true if +h+ is a valid hash.
|
84
113
|
def valid_hash?(h)
|
85
|
-
|
114
|
+
!SCrypt::Engine::HASH_PATTERN.match(h).nil?
|
86
115
|
end
|
87
116
|
|
88
117
|
# call-seq:
|
89
118
|
# split_hash(raw_hash) -> cost, salt, hash
|
90
119
|
#
|
91
120
|
# Splits +h+ into cost, salt, and hash and returns them in that order.
|
92
|
-
def split_hash(
|
93
|
-
|
94
|
-
[
|
121
|
+
def split_hash(hash_string)
|
122
|
+
cpu_cost, version, memory_cost, salt, hash = hash_string.split('$')
|
123
|
+
cost_string = "#{[cpu_cost, version, memory_cost].join('$')}$"
|
124
|
+
[cost_string, salt, hash]
|
95
125
|
end
|
96
126
|
end
|
97
127
|
end
|
data/lib/scrypt/scrypt_ext.rb
CHANGED
@@ -7,6 +7,10 @@ module SCrypt
|
|
7
7
|
module Ext
|
8
8
|
extend FFI::Library
|
9
9
|
|
10
|
-
|
10
|
+
begin
|
11
|
+
ffi_lib FFI::Compiler::Loader.find('scrypt_ext')
|
12
|
+
rescue LoadError => e
|
13
|
+
raise LoadError, "Failed to load scrypt extension library: #{e.message}"
|
14
|
+
end
|
11
15
|
end
|
12
16
|
end
|
data/lib/scrypt/version.rb
CHANGED
data/scrypt.gemspec
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
$:.push File.expand_path('lib', __dir__)
|
6
4
|
require 'scrypt/version'
|
7
5
|
|
8
6
|
Gem::Specification.new do |s|
|
@@ -16,10 +14,11 @@ Gem::Specification.new do |s|
|
|
16
14
|
'steve@advancedcontrol.com.au',
|
17
15
|
'rene.vanpaassen@gmail.com',
|
18
16
|
'io+scrypt@jsg.io']
|
19
|
-
s.cert_chain = ['certs/
|
17
|
+
s.cert_chain = ['certs/pbhogan.pem']
|
20
18
|
s.license = 'BSD-3-Clause'
|
21
19
|
|
22
20
|
s.signing_key = File.expand_path('~/.ssh/gem-private_key.pem') if $0 =~ /gem\z/
|
21
|
+
s.metadata['rubygems_mfa_required'] = 'true'
|
23
22
|
|
24
23
|
s.homepage = 'https://github.com/pbhogan/scrypt'
|
25
24
|
s.summary = 'scrypt password hashing algorithm.'
|
@@ -30,26 +29,14 @@ Gem::Specification.new do |s|
|
|
30
29
|
alternative functions such as PBKDF2 or bcrypt.
|
31
30
|
DESC
|
32
31
|
|
33
|
-
s.
|
34
|
-
s.add_development_dependency 'awesome_print', '>= 1', '< 2'
|
35
|
-
s.add_development_dependency 'rake', '>= 9', '< 13'
|
36
|
-
s.add_development_dependency 'rdoc', '>= 4', '< 5'
|
37
|
-
s.add_development_dependency 'rspec', '>= 3', '< 4'
|
38
|
-
|
39
|
-
if RUBY_VERSION >= '2.5'
|
40
|
-
s.add_development_dependency 'rubocop', '>= 0.76.0', '< 1.0.0'
|
41
|
-
s.add_development_dependency 'rubocop-gitlab-security', '>= 0.1.1', '< 0.2'
|
42
|
-
s.add_development_dependency 'rubocop-performance', '>= 1.5.0', '< 1.6.0'
|
43
|
-
end
|
32
|
+
s.required_ruby_version = '>= 2.3.0'
|
44
33
|
|
45
|
-
s.
|
34
|
+
s.add_dependency 'ffi-compiler', '>= 1.0', '< 2.0'
|
35
|
+
s.add_dependency 'rake', '~> 13'
|
46
36
|
|
47
37
|
s.extensions = ['ext/scrypt/Rakefile']
|
48
38
|
|
49
|
-
s.files = %w[Rakefile scrypt.gemspec README.md COPYING] + Dir.glob('{lib,spec
|
39
|
+
s.files = %w[Rakefile scrypt.gemspec README.md COPYING] + Dir.glob('{lib,spec}/**/*')
|
50
40
|
s.files += Dir.glob('ext/scrypt/*')
|
51
|
-
s.test_files = Dir.glob('spec/**/*')
|
52
41
|
s.require_paths = ['lib']
|
53
42
|
end
|
54
|
-
|
55
|
-
# rubocop:enable
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# SCrypt Test Vectors
|
2
|
+
# These are the official test vectors from the scrypt specification
|
3
|
+
# Used to verify our implementation matches the reference
|
4
|
+
|
5
|
+
scrypt_vectors:
|
6
|
+
- description: "Empty string test"
|
7
|
+
password: ""
|
8
|
+
salt: ""
|
9
|
+
n: 16
|
10
|
+
r: 1
|
11
|
+
p: 1
|
12
|
+
key_len: 64
|
13
|
+
expected: "77d6576238657b203b19ca42c18a0497f16b4844e3074ae8dfdffa3fede21442fcd0069ded0948f8326a753a0fc81f17e8d3e0fb2e0d3628cf35e20c38d18906"
|
14
|
+
|
15
|
+
- description: "Standard test vector"
|
16
|
+
password: "password"
|
17
|
+
salt: "NaCl"
|
18
|
+
n: 1024
|
19
|
+
r: 8
|
20
|
+
p: 16
|
21
|
+
key_len: 64
|
22
|
+
expected: "fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b3731622eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640"
|
23
|
+
|
24
|
+
- description: "High memory test vector"
|
25
|
+
password: "pleaseletmein"
|
26
|
+
salt: "SodiumChloride"
|
27
|
+
n: 16384
|
28
|
+
r: 8
|
29
|
+
p: 1
|
30
|
+
key_len: 64
|
31
|
+
expected: "7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2d5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887"
|
32
|
+
|
33
|
+
- description: "Very high memory test (disabled on memory-constrained systems)"
|
34
|
+
password: "pleaseletmein"
|
35
|
+
salt: "SodiumChloride"
|
36
|
+
n: 1048576
|
37
|
+
r: 8
|
38
|
+
p: 1
|
39
|
+
key_len: 64
|
40
|
+
expected: "2101cb9b6a511aaeaddbbe09cf70f881ec568d574a2ffd4dabe5ee9820adaa478e56fd8f4ba5d09ffa1c6d927c40f4c337304049e8a952fbcbf45c6fa77a41a4"
|
41
|
+
skip_reason: "Memory limited systems (like Raspberry Pi) may fail this test"
|
42
|
+
|
43
|
+
hash_secret_vectors:
|
44
|
+
- description: "Empty string via hash_secret"
|
45
|
+
password: ""
|
46
|
+
salt: "10$1$1$0000000000000000"
|
47
|
+
key_len: 64
|
48
|
+
expected_pattern: "77d6576238657b203b19ca42c18a0497f16b4844e3074ae8dfdffa3fede21442fcd0069ded0948f8326a753a0fc81f17e8d3e0fb2e0d3628cf35e20c38d18906"
|
49
|
+
|
50
|
+
- description: "Standard test via hash_secret"
|
51
|
+
password: "password"
|
52
|
+
salt: "400$8$10$000000004e61436c"
|
53
|
+
key_len: 64
|
54
|
+
expected_pattern: "fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b3731622eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640"
|
55
|
+
|
56
|
+
- description: "High memory test via hash_secret"
|
57
|
+
password: "pleaseletmein"
|
58
|
+
salt: "4000$8$1$536f6469756d43686c6f72696465"
|
59
|
+
key_len: 64
|
60
|
+
expected_pattern: "7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2d5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887"
|
61
|
+
|
62
|
+
- description: "Very high memory test via hash_secret (disabled)"
|
63
|
+
password: "pleaseletmein"
|
64
|
+
salt: "100000$8$1$536f6469756d43686c6f72696465"
|
65
|
+
key_len: 64
|
66
|
+
expected_pattern: "2101cb9b6a511aaeaddbbe09cf70f881ec568d574a2ffd4dabe5ee9820adaa478e56fd8f4ba5d09ffa1c6d927c40f4c337304049e8a952fbcbf45c6fa77a41a4"
|
67
|
+
skip_reason: "Memory limited systems may fail this test"
|
data/spec/scrypt/engine_spec.rb
CHANGED
@@ -3,34 +3,45 @@
|
|
3
3
|
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
|
4
4
|
|
5
5
|
describe 'The SCrypt engine' do
|
6
|
-
it '
|
6
|
+
it 'calculates a valid cost factor' do
|
7
7
|
first = SCrypt::Engine.calibrate(max_time: 0.2)
|
8
8
|
expect(SCrypt::Engine.valid_cost?(first)).to equal(true)
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
12
12
|
describe 'Generating SCrypt salts' do
|
13
|
-
it '
|
13
|
+
it 'produces strings' do
|
14
14
|
expect(SCrypt::Engine.generate_salt).to be_an_instance_of(String)
|
15
15
|
end
|
16
16
|
|
17
|
-
it '
|
17
|
+
it 'produces random data' do
|
18
18
|
expect(SCrypt::Engine.generate_salt).not_to equal(SCrypt::Engine.generate_salt)
|
19
19
|
end
|
20
20
|
|
21
|
-
it '
|
21
|
+
it 'uses the saved cost factor' do
|
22
22
|
# Verify cost is different before saving
|
23
23
|
cost = SCrypt::Engine.calibrate(max_time: 0.01)
|
24
|
-
expect(SCrypt::Engine.generate_salt
|
24
|
+
expect(SCrypt::Engine.generate_salt).not_to start_with(cost)
|
25
25
|
|
26
26
|
cost = SCrypt::Engine.calibrate!(max_time: 0.01)
|
27
|
-
expect(SCrypt::Engine.generate_salt
|
27
|
+
expect(SCrypt::Engine.generate_salt).to start_with(cost)
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'resets calibrated cost when setting new calibration' do
|
31
|
+
# Set initial calibration
|
32
|
+
first_cost = SCrypt::Engine.calibrate!(max_time: 0.01)
|
33
|
+
expect(SCrypt::Engine.calibrated_cost).to eq(first_cost)
|
34
|
+
|
35
|
+
# Set different calibration
|
36
|
+
second_cost = SCrypt::Engine.calibrate!(max_time: 0.02)
|
37
|
+
expect(SCrypt::Engine.calibrated_cost).to eq(second_cost)
|
38
|
+
expect(SCrypt::Engine.calibrated_cost).not_to eq(first_cost)
|
28
39
|
end
|
29
40
|
end
|
30
41
|
|
31
42
|
describe 'Autodetecting of salt cost' do
|
32
|
-
it '
|
33
|
-
expect(SCrypt::Engine.autodetect_cost('2a$08$c3$
|
43
|
+
it 'works' do
|
44
|
+
expect(SCrypt::Engine.autodetect_cost('2a$08$c3$some_salt')).to eq('2a$08$c3$')
|
34
45
|
end
|
35
46
|
end
|
36
47
|
|
@@ -39,43 +50,161 @@ describe 'Generating SCrypt hashes' do
|
|
39
50
|
undef to_s
|
40
51
|
end
|
41
52
|
|
42
|
-
before
|
53
|
+
before do
|
43
54
|
@salt = SCrypt::Engine.generate_salt
|
44
55
|
@password = 'woo'
|
45
56
|
end
|
46
57
|
|
47
|
-
it '
|
58
|
+
it 'produces a string' do
|
48
59
|
expect(SCrypt::Engine.hash_secret(@password, @salt)).to be_an_instance_of(String)
|
49
60
|
end
|
50
61
|
|
51
|
-
it '
|
52
|
-
expect
|
62
|
+
it 'raises an InvalidSalt error if the salt is invalid' do
|
63
|
+
expect { SCrypt::Engine.hash_secret(@password, 'nino') }.to raise_error(SCrypt::Errors::InvalidSalt)
|
53
64
|
end
|
54
65
|
|
55
|
-
it '
|
56
|
-
expect
|
57
|
-
expect
|
58
|
-
expect
|
66
|
+
it 'raises an InvalidSecret error if the secret is invalid' do
|
67
|
+
expect { SCrypt::Engine.hash_secret(MyInvalidSecret.new, @salt) }.to raise_error(SCrypt::Errors::InvalidSecret)
|
68
|
+
expect { SCrypt::Engine.hash_secret(nil, @salt) }.not_to raise_error
|
69
|
+
expect { SCrypt::Engine.hash_secret(false, @salt) }.not_to raise_error
|
59
70
|
end
|
60
71
|
|
61
|
-
it '
|
72
|
+
it 'calls #to_s on the secret and use the return value as the actual secret data' do
|
62
73
|
expect(SCrypt::Engine.hash_secret(false, @salt)).to eq(SCrypt::Engine.hash_secret('false', @salt))
|
63
74
|
end
|
64
75
|
end
|
65
76
|
|
66
77
|
describe 'SCrypt test vectors' do
|
67
|
-
it '
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
78
|
+
it 'matches results of SCrypt function' do
|
79
|
+
TEST_VECTORS['scrypt_vectors'].each do |vector|
|
80
|
+
next if vector['skip_reason'] # Skip memory-intensive tests
|
81
|
+
|
82
|
+
result = SCrypt::Engine.scrypt(
|
83
|
+
vector['password'],
|
84
|
+
vector['salt'],
|
85
|
+
vector['n'],
|
86
|
+
vector['r'],
|
87
|
+
vector['p'],
|
88
|
+
vector['key_len']
|
89
|
+
).unpack('H*').first
|
90
|
+
|
91
|
+
expect(result).to eq(vector['expected']), "Failed for: #{vector['description']}"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'matches equivalent results sent through hash_secret() function' do
|
96
|
+
TEST_VECTORS['hash_secret_vectors'].each do |vector|
|
97
|
+
next if vector['skip_reason'] # Skip memory-intensive tests
|
98
|
+
|
99
|
+
result = SCrypt::Engine.hash_secret(
|
100
|
+
vector['password'],
|
101
|
+
vector['salt'],
|
102
|
+
vector['key_len']
|
103
|
+
)
|
104
|
+
|
105
|
+
# hash_secret returns: salt + '$' + hash_digest
|
106
|
+
# So we expect: "salt$expected_pattern"
|
107
|
+
expected_full_hash = "#{vector['salt']}$#{vector['expected_pattern']}"
|
108
|
+
expect(result).to eq(expected_full_hash), "Failed for: #{vector['description']}"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
describe 'Input validation' do
|
114
|
+
describe '#calibrate' do
|
115
|
+
it 'raises ArgumentError for negative max_mem' do
|
116
|
+
expect do
|
117
|
+
SCrypt::Engine.send(:__sc_calibrate, -1, 0.5, 0.2)
|
118
|
+
end.to raise_error(ArgumentError, 'max_mem must be non-negative')
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'raises ArgumentError for invalid max_memfrac' do
|
122
|
+
expect do
|
123
|
+
SCrypt::Engine.send(:__sc_calibrate, 1024, -0.1,
|
124
|
+
0.2)
|
125
|
+
end.to raise_error(ArgumentError, 'max_memfrac must be between 0 and 1')
|
126
|
+
expect do
|
127
|
+
SCrypt::Engine.send(:__sc_calibrate, 1024, 1.1,
|
128
|
+
0.2)
|
129
|
+
end.to raise_error(ArgumentError, 'max_memfrac must be between 0 and 1')
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'raises ArgumentError for non-positive max_time' do
|
133
|
+
expect do
|
134
|
+
SCrypt::Engine.send(:__sc_calibrate, 1024, 0.5, 0)
|
135
|
+
end.to raise_error(ArgumentError, 'max_time must be positive')
|
136
|
+
|
137
|
+
expect do
|
138
|
+
SCrypt::Engine.send(:__sc_calibrate, 1024, 0.5, -0.1)
|
139
|
+
end.to raise_error(ArgumentError, 'max_time must be positive')
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
describe '#scrypt' do
|
144
|
+
it 'raises ArgumentError for nil secret' do
|
145
|
+
expect do
|
146
|
+
SCrypt::Engine.send(:__sc_crypt, nil, 'salt', 16, 1, 1, 32)
|
147
|
+
end.to raise_error(ArgumentError, 'secret cannot be nil')
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'raises ArgumentError for nil salt' do
|
151
|
+
expect do
|
152
|
+
SCrypt::Engine.send(:__sc_crypt, 'secret', nil, 16, 1, 1, 32)
|
153
|
+
end.to raise_error(ArgumentError, 'salt cannot be nil')
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'raises ArgumentError for non-positive parameters' do
|
157
|
+
expect do
|
158
|
+
SCrypt::Engine.send(:__sc_crypt, 'secret', 'salt', 0, 1, 1, 32)
|
159
|
+
end.to raise_error(ArgumentError, 'cpu_cost must be positive')
|
160
|
+
|
161
|
+
expect do
|
162
|
+
SCrypt::Engine.send(:__sc_crypt, 'secret', 'salt', 16, 0, 1, 32)
|
163
|
+
end.to raise_error(ArgumentError, 'memory_cost must be positive')
|
164
|
+
|
165
|
+
expect do
|
166
|
+
SCrypt::Engine.send(:__sc_crypt, 'secret', 'salt', 16, 1, 0, 32)
|
167
|
+
end.to raise_error(ArgumentError, 'parallelization must be positive')
|
168
|
+
|
169
|
+
expect do
|
170
|
+
SCrypt::Engine.send(:__sc_crypt, 'secret', 'salt', 16, 1, 1,
|
171
|
+
0)
|
172
|
+
end.to raise_error(ArgumentError, 'key_len must be positive')
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
describe 'Memory usage calculation' do
|
178
|
+
it 'calculates memory usage correctly' do
|
179
|
+
cost = '400$8$1$'
|
180
|
+
memory = SCrypt::Engine.memory_use(cost)
|
181
|
+
n = 0x400
|
182
|
+
r = 8
|
183
|
+
p = 1
|
184
|
+
expected = (128 * r * p) + (256 * r) + (128 * r * n)
|
185
|
+
expect(memory).to eq(expected)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
describe 'Calibrated cost management' do
|
190
|
+
after do
|
191
|
+
# Reset calibrated cost after each test
|
192
|
+
SCrypt::Engine.calibrated_cost = nil
|
193
|
+
end
|
194
|
+
|
195
|
+
it 'initializes have no calibrated cost' do
|
196
|
+
SCrypt::Engine.calibrated_cost = nil
|
197
|
+
expect(SCrypt::Engine.calibrated_cost).to be_nil
|
198
|
+
end
|
199
|
+
|
200
|
+
it 'stores and retrieve calibrated cost' do
|
201
|
+
cost = SCrypt::Engine.calibrate!(max_time: 0.01)
|
202
|
+
expect(SCrypt::Engine.calibrated_cost).to eq(cost)
|
203
|
+
end
|
204
|
+
|
205
|
+
it 'uses calibrated cost in generate_salt when available' do
|
206
|
+
cost = SCrypt::Engine.calibrate!(max_time: 0.01)
|
207
|
+
salt = SCrypt::Engine.generate_salt
|
208
|
+
expect(salt).to start_with(cost)
|
80
209
|
end
|
81
210
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
|
4
|
+
|
5
|
+
describe 'SCrypt FFI Library Loading' do
|
6
|
+
describe 'Extension loading' do
|
7
|
+
it 'loads the scrypt extension successfully' do
|
8
|
+
# This test verifies that the FFI library loads without error
|
9
|
+
# If we get here, the library loaded successfully during require
|
10
|
+
expect(SCrypt::Ext).to be_a(Module)
|
11
|
+
expect(SCrypt::Ext).to respond_to(:sc_calibrate)
|
12
|
+
expect(SCrypt::Ext).to respond_to(:crypto_scrypt)
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'has proper FFI function signatures' do
|
16
|
+
# Verify that the FFI functions are properly bound
|
17
|
+
expect(SCrypt::Ext.method(:sc_calibrate)).to be_a(Method)
|
18
|
+
expect(SCrypt::Ext.method(:crypto_scrypt)).to be_a(Method)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe 'FFI function behavior' do
|
23
|
+
it 'handles basic calibration calls' do
|
24
|
+
# Test that the FFI functions are callable
|
25
|
+
expect { SCrypt::Engine.calibrate(max_time: 0.01) }.not_to raise_error
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'handles basic scrypt calls' do
|
29
|
+
salt = SCrypt::Engine.generate_salt(max_time: 0.01)
|
30
|
+
expect { SCrypt::Engine.hash_secret('test', salt) }.not_to raise_error
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
|
4
|
+
|
5
|
+
describe 'SCrypt Integration Tests' do
|
6
|
+
describe 'Full password lifecycle' do
|
7
|
+
let(:secret) { 'my_super_secret_password' }
|
8
|
+
let(:options) { { max_time: 0.1, max_mem: 8 * 1024 * 1024 } }
|
9
|
+
|
10
|
+
it 'create,s store, and verify passwords correctly' do
|
11
|
+
# Create password
|
12
|
+
password = SCrypt::Password.create(secret, options)
|
13
|
+
expect(password).to be_a(SCrypt::Password)
|
14
|
+
expect(password.to_s).to match(/^[0-9a-z]+\$[0-9a-z]+\$[0-9a-z]+\$[A-Za-z0-9]+\$[A-Za-z0-9]+$/)
|
15
|
+
|
16
|
+
# Verify password
|
17
|
+
expect(password == secret).to be(true)
|
18
|
+
expect(password == 'wrong_password').to be(false)
|
19
|
+
|
20
|
+
# Re-instantiate from stored hash
|
21
|
+
stored_hash = password.to_s
|
22
|
+
recovered_password = SCrypt::Password.new(stored_hash)
|
23
|
+
|
24
|
+
expect(recovered_password == secret).to be(true)
|
25
|
+
expect(recovered_password == 'wrong_password').to be(false)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'handles calibration workflow correctly' do
|
29
|
+
# Calibrate for fast testing
|
30
|
+
cost = SCrypt::Engine.calibrate!(max_time: 0.05)
|
31
|
+
expect(cost).to match(/^[0-9a-z]+\$[0-9a-z]+\$[0-9a-z]+\$$/)
|
32
|
+
|
33
|
+
# Generate salt using calibrated cost
|
34
|
+
salt = SCrypt::Engine.generate_salt
|
35
|
+
expect(salt).to start_with(cost)
|
36
|
+
|
37
|
+
# Hash secret with calibrated parameters
|
38
|
+
hash = SCrypt::Engine.hash_secret(secret, salt)
|
39
|
+
expect(hash).to be_a(String)
|
40
|
+
expect(hash).to include(salt)
|
41
|
+
|
42
|
+
# Verify the hash
|
43
|
+
password = SCrypt::Password.new(hash)
|
44
|
+
expect(password == secret).to be(true)
|
45
|
+
|
46
|
+
# Reset calibration
|
47
|
+
SCrypt::Engine.calibrated_cost = nil
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe 'Cross-compatibility tests' do
|
52
|
+
it 'is compatible between Engine and Password classes' do
|
53
|
+
# Create using Password class
|
54
|
+
password1 = SCrypt::Password.create('test_secret', max_time: 0.05)
|
55
|
+
|
56
|
+
# Extract components and recreate using Engine
|
57
|
+
cost = password1.cost
|
58
|
+
salt_with_cost = cost + password1.salt
|
59
|
+
hash2 = SCrypt::Engine.hash_secret('test_secret', salt_with_cost, password1.digest.length / 2)
|
60
|
+
|
61
|
+
# Both should verify the same secret
|
62
|
+
password2 = SCrypt::Password.new(hash2)
|
63
|
+
expect(password1 == 'test_secret').to be(true)
|
64
|
+
expect(password2 == 'test_secret').to be(true)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe 'Edge cases and error conditions' do
|
69
|
+
it 'handles various secret types' do
|
70
|
+
# String secret
|
71
|
+
password1 = SCrypt::Password.create('string_secret', max_time: 0.05)
|
72
|
+
expect(password1 == 'string_secret').to be(true)
|
73
|
+
|
74
|
+
# Symbol secret
|
75
|
+
password2 = SCrypt::Password.create(:symbol_secret, max_time: 0.05)
|
76
|
+
expect(password2 == 'symbol_secret').to be(true)
|
77
|
+
|
78
|
+
# Numeric secret
|
79
|
+
password3 = SCrypt::Password.create(12_345, max_time: 0.05)
|
80
|
+
expect(password3 == '12345').to be(true)
|
81
|
+
|
82
|
+
# Boolean secret
|
83
|
+
password4 = SCrypt::Password.create(false, max_time: 0.05)
|
84
|
+
expect(password4 == 'false').to be(true)
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'handles empty and nil secrets safely' do
|
88
|
+
# Empty string
|
89
|
+
password1 = SCrypt::Password.create('', max_time: 0.05)
|
90
|
+
expect(password1 == '').to be(true)
|
91
|
+
|
92
|
+
# Nil (converts to empty string)
|
93
|
+
password2 = SCrypt::Password.create(nil, max_time: 0.05)
|
94
|
+
expect(password2 == '').to be(true)
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'validates input parameters' do
|
98
|
+
# Invalid hash format
|
99
|
+
expect { SCrypt::Password.new('invalid_hash') }.to raise_error(SCrypt::Errors::InvalidHash)
|
100
|
+
|
101
|
+
# Invalid salt
|
102
|
+
expect { SCrypt::Engine.hash_secret('secret', 'invalid_salt') }.to raise_error(SCrypt::Errors::InvalidSalt)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe 'Performance and memory tests' do
|
107
|
+
it 'respects memory and time constraints' do
|
108
|
+
start_time = Time.now
|
109
|
+
|
110
|
+
# Use very low constraints for fast testing
|
111
|
+
password = SCrypt::Password.create('test', max_time: 0.01, max_mem: 1024 * 1024)
|
112
|
+
|
113
|
+
elapsed_time = Time.now - start_time
|
114
|
+
|
115
|
+
# Should complete reasonably quickly (allowing some overhead)
|
116
|
+
expect(elapsed_time).to be < 1.0
|
117
|
+
expect(password == 'test').to be(true)
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'calculates memory usage correctly' do
|
121
|
+
cost = SCrypt::Engine.calibrate(max_time: 0.05)
|
122
|
+
memory_usage = SCrypt::Engine.memory_use(cost)
|
123
|
+
|
124
|
+
# Memory usage should be a reasonable number
|
125
|
+
expect(memory_usage).to be > 0
|
126
|
+
expect(memory_usage).to be < 100 * 1024 * 1024 # Less than 100MB
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|