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.
@@ -3,135 +3,183 @@
3
3
  require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
4
4
 
5
5
  describe 'Creating a hashed password' do
6
- before :each do
6
+ before do
7
7
  @password = SCrypt::Password.create('s3cr3t', max_time: 0.25)
8
8
  end
9
9
 
10
- it 'should return a SCrypt::Password' do
10
+ it 'returns a SCrypt::Password' do
11
11
  expect(@password).to be_an_instance_of(SCrypt::Password)
12
12
  end
13
13
 
14
- it 'should return a valid password' do
15
- expect(-> { SCrypt::Password.new(@password) }).to_not raise_error
14
+ it 'returns a valid password' do
15
+ expect { SCrypt::Password.new(@password) }.not_to raise_error
16
16
  end
17
17
 
18
- it 'should behave normally if the secret is not a string' do
19
- expect(-> { SCrypt::Password.create(nil) }).to_not raise_error
20
- expect(-> { SCrypt::Password.create(woo: 'yeah') }).to_not raise_error
21
- expect(-> { SCrypt::Password.create(false) }).to_not raise_error
18
+ it 'behaves normally if the secret is not a string' do
19
+ expect { SCrypt::Password.create(nil) }.not_to raise_error
20
+ expect { SCrypt::Password.create(false) }.not_to raise_error
21
+ expect { SCrypt::Password.create(42) }.not_to raise_error
22
22
  end
23
23
 
24
- it 'should tolerate empty string secrets' do
25
- expect(-> { SCrypt::Password.create("\n".chop) }).to_not raise_error
26
- expect(-> { SCrypt::Password.create('') }).to_not raise_error
27
- expect(-> { SCrypt::Password.create('') }).to_not raise_error
24
+ it 'tolerates empty string secrets' do
25
+ expect { SCrypt::Password.create('') }.not_to raise_error
26
+ expect { SCrypt::Password.create('', max_time: 0.01) }.not_to raise_error
27
+ expect(SCrypt::Password.create('')).to be_an_instance_of(SCrypt::Password)
28
28
  end
29
29
  end
30
30
 
31
31
  describe 'Reading a hashed password' do
32
- before :each do
32
+ before do
33
33
  @secret = 'my secret'
34
34
  @hash = '400$8$d$173a8189751c095a29b933789560b73bf17b2e01$9bf66d74bd6f3ebcf99da3b379b689b89db1cb07'
35
35
  end
36
36
 
37
- it 'should read the cost, salt, and hash' do
37
+ it 'reads the cost, salt, and hash' do
38
38
  password = SCrypt::Password.new(@hash)
39
39
  expect(password.cost).to eq('400$8$d$')
40
40
  expect(password.salt).to eq('173a8189751c095a29b933789560b73bf17b2e01')
41
- expect(password.to_s).to eq(@hash)
41
+ expect(password.digest).to eq('9bf66d74bd6f3ebcf99da3b379b689b89db1cb07')
42
42
  end
43
43
 
44
- it 'should raise an InvalidHashError when given an invalid hash' do
45
- expect(-> { SCrypt::Password.new('not a valid hash') }).to raise_error(SCrypt::Errors::InvalidHash)
44
+ it 'raises an InvalidHashError when given an invalid hash' do
45
+ expect { SCrypt::Password.new('invalid') }.to raise_error(SCrypt::Errors::InvalidHash)
46
46
  end
47
47
  end
48
48
 
49
49
  describe 'Comparing a hashed password with a secret' do
50
- before :each do
50
+ before do
51
51
  @secret = 's3cr3t'
52
- @password = SCrypt::Password.create(@secret)
52
+ @password = SCrypt::Password.create(@secret, max_time: 0.01)
53
53
  end
54
54
 
55
- it 'should compare successfully to the original secret' do
56
- expect((@password == @secret)).to be(true)
55
+ it 'compares successfully to the original secret' do
56
+ expect(@password == @secret).to be true
57
57
  end
58
58
 
59
- it 'should compare unsuccessfully to anything besides original secret' do
60
- expect((@password == '@secret')).to be(false)
59
+ it 'compares unsuccessfully to anything besides original secret' do
60
+ expect(@password == 'different').to be false
61
61
  end
62
62
  end
63
63
 
64
64
  describe 'non-default salt sizes' do
65
- before :each do
65
+ before do
66
66
  @secret = 's3cret'
67
67
  end
68
68
 
69
- it 'should enforce a minimum salt of 8 bytes' do
70
- @password = SCrypt::Password.create(@secret, salt_size: 7)
71
- expect(@password.salt.length).to eq(8 * 2)
69
+ it 'enforces a minimum salt of 8 bytes' do
70
+ @password = SCrypt::Password.create(@secret, salt_size: 4, max_time: 0.01)
71
+ expect(@password.salt.length).to eq(16) # 8 bytes * 2 (hex encoding)
72
72
  end
73
73
 
74
- it 'should allow a salt of 32 bytes' do
75
- @password = SCrypt::Password.create(@secret, salt_size: 32)
76
- expect(@password.salt.length).to eq(32 * 2)
74
+ it 'allows a salt of 32 bytes' do
75
+ @password = SCrypt::Password.create(@secret, salt_size: 32, max_time: 0.01)
76
+ expect(@password.salt.length).to eq(64) # 32 bytes * 2 (hex encoding)
77
77
  end
78
78
 
79
- it 'should enforce a maximum salt of 32 bytes' do
80
- @password = SCrypt::Password.create(@secret, salt_size: 33)
81
- expect(@password.salt.length).to eq(32 * 2)
79
+ it 'enforces a maximum salt of 32 bytes' do
80
+ @password = SCrypt::Password.create(@secret, salt_size: 64, max_time: 0.01)
81
+ expect(@password.salt.length).to eq(64) # 32 bytes * 2 (hex encoding)
82
82
  end
83
83
 
84
- it 'should pad a 20-byte salt to not look like a 20-byte SHA1' do
84
+ it 'pads a 20-byte salt to not look like a 20-byte SHA1' do
85
85
  @password = SCrypt::Password.create(@secret, salt_size: 20)
86
86
  expect(@password.salt.length).to eq(41)
87
87
  end
88
88
 
89
- it 'should properly compare a non-standard salt hash' do
90
- @password = SCrypt::Password.create(@secret, salt_size: 20)
91
- expect((SCrypt::Password.new(@password.to_s) == @secret)).to be(true)
89
+ it 'properly compares a non-standard salt hash' do
90
+ @password = SCrypt::Password.create(@secret, salt_size: 16, max_time: 0.01)
91
+ expect(@password == @secret).to be true
92
92
  end
93
93
  end
94
94
 
95
95
  describe 'non-default key lengths' do
96
- before :each do
96
+ before do
97
97
  @secret = 's3cret'
98
98
  end
99
99
 
100
- it 'should enforce a minimum keylength of 16 bytes' do
101
- @password = SCrypt::Password.create(@secret, key_len: 15)
102
- expect(@password.digest.length).to eq(16 * 2)
100
+ it 'enforces a minimum keylength of 16 bytes' do
101
+ @password = SCrypt::Password.create(@secret, key_len: 8, max_time: 0.01)
102
+ expect(@password.digest.length).to eq(32) # 16 bytes * 2 (hex encoding)
103
103
  end
104
104
 
105
- it 'should allow a keylength of 512 bytes' do
106
- @password = SCrypt::Password.create(@secret, key_len: 512)
107
- expect(@password.digest.length).to eq(512 * 2)
105
+ it 'allows a keylength of 512 bytes' do
106
+ @password = SCrypt::Password.create(@secret, key_len: 512, max_time: 0.01)
107
+ expect(@password.digest.length).to eq(1024) # 512 bytes * 2 (hex encoding)
108
108
  end
109
109
 
110
- it 'should enforce a maximum keylength of 512 bytes' do
111
- @password = SCrypt::Password.create(@secret, key_len: 513)
112
- expect(@password.digest.length).to eq(512 * 2)
110
+ it 'enforces a maximum keylength of 512 bytes' do
111
+ @password = SCrypt::Password.create(@secret, key_len: 1024, max_time: 0.01)
112
+ expect(@password.digest.length).to eq(1024) # 512 bytes * 2 (hex encoding)
113
113
  end
114
114
 
115
- it 'should properly compare a non-standard hash' do
116
- @password = SCrypt::Password.create(@secret, key_len: 512)
117
- expect((SCrypt::Password.new(@password.to_s) == @secret)).to be(true)
115
+ it 'properly compares a non-standard hash' do
116
+ @password = SCrypt::Password.create(@secret, key_len: 64, max_time: 0.01)
117
+ expect(@password == @secret).to be true
118
118
  end
119
119
  end
120
120
 
121
121
  describe 'Old-style hashes' do
122
- before :each do
122
+ before do
123
123
  @secret = 'my secret'
124
124
  @hash = '400$8$d$173a8189751c095a29b933789560b73bf17b2e01$9bf66d74bd6f3ebcf99da3b379b689b89db1cb07'
125
125
  end
126
126
 
127
- it 'should compare successfully' do
128
- expect((SCrypt::Password.new(@hash) == @secret)).to be(true)
127
+ it 'compares successfully' do
128
+ expect(SCrypt::Password.new(@hash) == @secret).to be true
129
129
  end
130
130
  end
131
131
 
132
132
  describe 'Respecting standard ruby behaviors' do
133
- it 'should hash as an integer' do
134
- password = SCrypt::Password.create('')
135
- expect(password.hash).to be_kind_of(Integer)
133
+ it 'hashes as an integer' do
134
+ password = SCrypt::Password.create('secret', max_time: 0.01)
135
+ expect(password.hash).to be_an(Integer)
136
+ end
137
+ end
138
+
139
+ describe 'Password validation and parsing' do
140
+ it 'correctly parses hash components' do
141
+ password = SCrypt::Password.new('400$8$d$173a8189751c095a29b933789560b73bf17b2e01$9bf66d74bd6f3ebcf99da3b379b689b89db1cb07')
142
+
143
+ expect(password.cost).to eq('400$8$d$')
144
+ expect(password.salt).to eq('173a8189751c095a29b933789560b73bf17b2e01')
145
+ expect(password.digest).to eq('9bf66d74bd6f3ebcf99da3b379b689b89db1cb07')
146
+ end
147
+
148
+ it 'validates hash format strictly' do
149
+ valid_hash = '400$8$d$173a8189751c095a29b933789560b73bf17b2e01$9bf66d74bd6f3ebcf99da3b379b689b89db1cb07'
150
+
151
+ expect { SCrypt::Password.new(valid_hash) }.not_to raise_error
152
+ expect { SCrypt::Password.new('invalid') }.to raise_error(SCrypt::Errors::InvalidHash)
153
+ expect { SCrypt::Password.new('') }.to raise_error(SCrypt::Errors::InvalidHash)
154
+ expect { SCrypt::Password.new('400$8$d$') }.to raise_error(SCrypt::Errors::InvalidHash)
155
+ end
156
+
157
+ it 'handles alias method correctly' do
158
+ password = SCrypt::Password.create('secret', max_time: 0.01)
159
+
160
+ expect(password.is_password?('secret')).to be true
161
+ expect(password.is_password?('wrong')).to be false
162
+ end
163
+ end
164
+
165
+ describe 'Parameter boundary testing' do
166
+ it 'enforces minimum and maximum key lengths correctly' do
167
+ # Test minimum key length (should be clamped to 16)
168
+ password_min = SCrypt::Password.create('secret', key_len: 8, max_time: 0.01)
169
+ expect(password_min.digest.length).to eq(32) # 16 bytes * 2 (hex encoding)
170
+
171
+ # Test maximum key length (should be clamped to 512)
172
+ password_max = SCrypt::Password.create('secret', key_len: 1024, max_time: 0.01)
173
+ expect(password_max.digest.length).to eq(1024) # 512 bytes * 2 (hex encoding)
174
+ end
175
+
176
+ it 'enforces minimum and maximum salt sizes correctly' do
177
+ # Test minimum salt size (should be clamped to 8)
178
+ password_min = SCrypt::Password.create('secret', salt_size: 4, max_time: 0.01)
179
+ expect(password_min.salt.length).to eq(16) # 8 bytes * 2 (hex encoding)
180
+
181
+ # Test maximum salt size (should be clamped to 32)
182
+ password_max = SCrypt::Password.create('secret', salt_size: 64, max_time: 0.01)
183
+ expect(password_max.salt.length).to eq(64) # 32 bytes * 2 (hex encoding)
136
184
  end
137
185
  end
@@ -3,12 +3,53 @@
3
3
  require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
4
4
 
5
5
  describe 'Security Utils' do
6
- it 'should perform a string comparison' do
7
- expect(SCrypt::SecurityUtils.secure_compare('a', 'a')).to equal(true)
8
- expect(SCrypt::SecurityUtils.secure_compare('a', 'b')).to equal(false)
9
- expect(SCrypt::SecurityUtils.secure_compare('aa', 'aa')).to equal(true)
10
- expect(SCrypt::SecurityUtils.secure_compare('aa', 'ab')).to equal(false)
11
- expect(SCrypt::SecurityUtils.secure_compare('aa', 'aaa')).to equal(false)
12
- expect(SCrypt::SecurityUtils.secure_compare('aaa', 'aa')).to equal(false)
6
+ describe '.secure_compare' do
7
+ it 'performs a string comparison correctly' do
8
+ expect(SCrypt::SecurityUtils.secure_compare('a', 'a')).to equal(true)
9
+ expect(SCrypt::SecurityUtils.secure_compare('a', 'b')).to equal(false)
10
+ expect(SCrypt::SecurityUtils.secure_compare('aa', 'aa')).to equal(true)
11
+ expect(SCrypt::SecurityUtils.secure_compare('aa', 'ab')).to equal(false)
12
+ end
13
+
14
+ it 'returns false for different length strings' do
15
+ expect(SCrypt::SecurityUtils.secure_compare('aa', 'aaa')).to equal(false)
16
+ expect(SCrypt::SecurityUtils.secure_compare('aaa', 'aa')).to equal(false)
17
+ expect(SCrypt::SecurityUtils.secure_compare('', 'a')).to equal(false)
18
+ expect(SCrypt::SecurityUtils.secure_compare('a', '')).to equal(false)
19
+ end
20
+
21
+ it 'handles empty strings correctly' do
22
+ expect(SCrypt::SecurityUtils.secure_compare('', '')).to equal(true)
23
+ end
24
+
25
+ it 'handles binary data correctly' do
26
+ binary1 = "\x00\x01\x02\x03"
27
+ binary2 = "\x00\x01\x02\x03"
28
+ binary3 = "\x00\x01\x02\x04"
29
+
30
+ expect(SCrypt::SecurityUtils.secure_compare(binary1, binary2)).to equal(true)
31
+ expect(SCrypt::SecurityUtils.secure_compare(binary1, binary3)).to equal(false)
32
+ end
33
+
34
+ it 'handles unicode strings correctly' do
35
+ unicode1 = 'héllo'
36
+ unicode2 = 'héllo'
37
+ unicode3 = 'hello'
38
+
39
+ expect(SCrypt::SecurityUtils.secure_compare(unicode1, unicode2)).to equal(true)
40
+ expect(SCrypt::SecurityUtils.secure_compare(unicode1, unicode3)).to equal(false)
41
+ end
42
+
43
+ it 'is resistant to timing attacks' do
44
+ # This test ensures the function takes constant time regardless of where differences occur
45
+ long_string1 = ('a' * 1000) + 'x'
46
+ long_string2 = ('a' * 1000) + 'y'
47
+ long_string3 = 'x' + ('a' * 1000)
48
+ long_string4 = 'y' + ('a' * 1000)
49
+
50
+ # All of these should return false and take similar time
51
+ expect(SCrypt::SecurityUtils.secure_compare(long_string1, long_string2)).to equal(false)
52
+ expect(SCrypt::SecurityUtils.secure_compare(long_string3, long_string4)).to equal(false)
53
+ end
13
54
  end
14
55
  end
data/spec/spec_helper.rb CHANGED
@@ -4,4 +4,44 @@ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib'))
4
4
 
5
5
  require 'rubygems'
6
6
  require 'rspec'
7
+ require 'yaml'
7
8
  require 'scrypt'
9
+
10
+ # Load shared examples
11
+ Dir[File.expand_path('support/**/*.rb', __dir__)].each { |f| require f }
12
+
13
+ # Load test fixtures
14
+ TEST_VECTORS = YAML.load_file(File.expand_path('fixtures/test_vectors.yml', __dir__)).freeze
15
+
16
+ RSpec.configure do |config|
17
+ # Use documentation format for better output
18
+ config.default_formatter = 'doc' if config.files_to_run.one?
19
+
20
+ # Run specs in random order to surface order dependencies
21
+ config.order = :random
22
+
23
+ # Seed global randomization in this process using the `--seed` CLI option
24
+ Kernel.srand config.seed
25
+
26
+ # Allow more verbose output when running a single file
27
+ config.filter_run_when_matching :focus
28
+
29
+ # Enable expect syntax (recommended)
30
+ config.expect_with :rspec do |expectations|
31
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
32
+ # Disable deprecated should syntax
33
+ expectations.syntax = :expect
34
+ end
35
+
36
+ # Configure mocks
37
+ config.mock_with :rspec do |mocks|
38
+ mocks.verify_partial_doubles = true
39
+ end
40
+
41
+ # Enable shared context metadata behavior
42
+ config.shared_context_metadata_behavior = :apply_to_host_groups
43
+
44
+ # Configure warnings and deprecations
45
+ config.warnings = true
46
+ config.raise_errors_for_deprecations!
47
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Shared examples for SCrypt tests
4
+ RSpec.shared_examples 'a valid scrypt hash' do
5
+ it 'has the correct format' do
6
+ expect(subject).to match(/^[0-9a-z]+\$[0-9a-z]+\$[0-9a-z]+\$[A-Za-z0-9]+\$[A-Za-z0-9]+$/)
7
+ end
8
+
9
+ it 'is a string' do
10
+ expect(subject).to be_a(String)
11
+ end
12
+ end
13
+
14
+ RSpec.shared_examples 'a valid cost string' do
15
+ it 'has the correct format' do
16
+ expect(subject).to match(/^[0-9a-z]+\$[0-9a-z]+\$[0-9a-z]+\$$/)
17
+ end
18
+
19
+ it 'is valid according to Engine.valid_cost?' do
20
+ expect(SCrypt::Engine.valid_cost?(subject)).to be(true)
21
+ end
22
+ end
23
+
24
+ RSpec.shared_examples 'a valid salt string' do
25
+ it 'has the correct format' do
26
+ expect(subject).to match(/^[0-9a-z]+\$[0-9a-z]+\$[0-9a-z]+\$[A-Za-z0-9]{16,64}$/)
27
+ end
28
+
29
+ it 'is valid according to Engine.valid_salt?' do
30
+ expect(SCrypt::Engine.valid_salt?(subject)).to be(true)
31
+ end
32
+ end
33
+
34
+ RSpec.shared_examples 'proper input validation' do |method, args, error_class, error_message|
35
+ it "raises #{error_class} for invalid input" do
36
+ expect { subject.send(method, *args) }.to raise_error(error_class, error_message)
37
+ end
38
+ end
39
+
40
+ RSpec.shared_examples 'deterministic output' do |method, args|
41
+ it 'produces the same output for the same input' do
42
+ result1 = subject.send(method, *args)
43
+ result2 = subject.send(method, *args)
44
+ expect(result1).to eq(result2)
45
+ end
46
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestHelpers
4
+ # Common test data
5
+ VALID_SECRETS = [
6
+ 'simple_password',
7
+ 'complex_password_123!@#',
8
+ '',
9
+ 'unicode_tésting',
10
+ '🔒secure🔑'
11
+ ].freeze
12
+
13
+ INVALID_HASH_FORMATS = [
14
+ '',
15
+ 'invalid',
16
+ '400$8$d$invalid',
17
+ '400$8$d$173a8189751c095a29b933789560b73bf17b2e01',
18
+ '400$8$d$173a8189751c095a29b933789560b73bf17b2e01$'
19
+ ].freeze
20
+
21
+ INVALID_SALT_FORMATS = [
22
+ '',
23
+ 'invalid',
24
+ 'nino',
25
+ '400$8$d$'
26
+ ].freeze
27
+
28
+ # Helper methods
29
+ def self.generate_test_password(secret = 'test_secret', options = {})
30
+ default_options = { max_time: 0.05 }
31
+ SCrypt::Password.create(secret, default_options.merge(options))
32
+ end
33
+
34
+ def self.generate_test_salt(options = {})
35
+ default_options = { max_time: 0.05 }
36
+ SCrypt::Engine.generate_salt(default_options.merge(options))
37
+ end
38
+
39
+ def self.reset_calibration
40
+ SCrypt::Engine.calibrated_cost = nil
41
+ end
42
+ end
43
+
44
+ # Include helper methods in RSpec
45
+ RSpec.configure do |config|
46
+ config.include TestHelpers
47
+ end
data.tar.gz.sig CHANGED
Binary file