scrypt 3.0.6 → 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.
@@ -1,84 +1,210 @@
1
- require File.expand_path(File.join(File.dirname(__FILE__), "..", "spec_helper"))
1
+ # frozen_string_literal: true
2
2
 
3
- describe "The SCrypt engine" do
4
- it "should calculate a valid cost factor" do
5
- first = SCrypt::Engine.calibrate(:max_time => 0.2)
3
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
4
+
5
+ describe 'The SCrypt engine' do
6
+ it 'calculates a valid cost factor' do
7
+ first = SCrypt::Engine.calibrate(max_time: 0.2)
6
8
  expect(SCrypt::Engine.valid_cost?(first)).to equal(true)
7
9
  end
8
10
  end
9
11
 
10
-
11
- describe "Generating SCrypt salts" do
12
- it "should produce strings" do
12
+ describe 'Generating SCrypt salts' do
13
+ it 'produces strings' do
13
14
  expect(SCrypt::Engine.generate_salt).to be_an_instance_of(String)
14
15
  end
15
16
 
16
- it "should produce random data" do
17
+ it 'produces random data' do
17
18
  expect(SCrypt::Engine.generate_salt).not_to equal(SCrypt::Engine.generate_salt)
18
19
  end
19
20
 
20
- it "should used the saved cost factor" do
21
+ it 'uses the saved cost factor' do
21
22
  # Verify cost is different before saving
22
- cost = SCrypt::Engine.calibrate(:max_time => 0.01)
23
- expect(SCrypt::Engine.generate_salt(:max_time => 30, :max_mem => 64*1024*1024)).not_to start_with(cost)
23
+ cost = SCrypt::Engine.calibrate(max_time: 0.01)
24
+ expect(SCrypt::Engine.generate_salt).not_to start_with(cost)
24
25
 
25
- cost = SCrypt::Engine.calibrate!(:max_time => 0.01)
26
- expect(SCrypt::Engine.generate_salt(:max_time => 30, :max_mem => 64*1024*1024)).to start_with(cost)
26
+ cost = SCrypt::Engine.calibrate!(max_time: 0.01)
27
+ expect(SCrypt::Engine.generate_salt).to start_with(cost)
27
28
  end
28
- end
29
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)
30
34
 
31
- describe "Autodetecting of salt cost" do
32
- it "should work" do
33
- expect(SCrypt::Engine.autodetect_cost("2a$08$c3$randomjunkgoeshere")).to eq("2a$08$c3$")
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)
34
39
  end
35
40
  end
36
41
 
42
+ describe 'Autodetecting of salt cost' do
43
+ it 'works' do
44
+ expect(SCrypt::Engine.autodetect_cost('2a$08$c3$some_salt')).to eq('2a$08$c3$')
45
+ end
46
+ end
37
47
 
38
- describe "Generating SCrypt hashes" do
39
-
48
+ describe 'Generating SCrypt hashes' do
40
49
  class MyInvalidSecret
41
50
  undef to_s
42
51
  end
43
52
 
44
- before :each do
53
+ before do
45
54
  @salt = SCrypt::Engine.generate_salt
46
- @password = "woo"
55
+ @password = 'woo'
47
56
  end
48
57
 
49
- it "should produce a string" do
58
+ it 'produces a string' do
50
59
  expect(SCrypt::Engine.hash_secret(@password, @salt)).to be_an_instance_of(String)
51
60
  end
52
61
 
53
- it "should raise an InvalidSalt error if the salt is invalid" do
54
- expect(lambda { SCrypt::Engine.hash_secret(@password, 'nino') }).to raise_error(SCrypt::Errors::InvalidSalt)
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)
55
64
  end
56
65
 
57
- it "should raise an InvalidSecret error if the secret is invalid" do
58
- expect(lambda { SCrypt::Engine.hash_secret(MyInvalidSecret.new, @salt) }).to raise_error(SCrypt::Errors::InvalidSecret)
59
- expect(lambda { SCrypt::Engine.hash_secret(nil, @salt) }).to_not raise_error
60
- expect(lambda { SCrypt::Engine.hash_secret(false, @salt) }).to_not raise_error
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
61
70
  end
62
71
 
63
- it "should call #to_s on the secret and use the return value as the actual secret data" do
64
- expect(SCrypt::Engine.hash_secret(false, @salt)).to eq(SCrypt::Engine.hash_secret("false", @salt))
72
+ it 'calls #to_s on the secret and use the return value as the actual secret data' do
73
+ expect(SCrypt::Engine.hash_secret(false, @salt)).to eq(SCrypt::Engine.hash_secret('false', @salt))
65
74
  end
66
75
  end
67
76
 
68
- describe "SCrypt test vectors" do
69
- it "should match results of SCrypt function" do
70
-
71
- expect(SCrypt::Engine.scrypt('', '', 16, 1, 1, 64).unpack('H*').first).to eq('77d6576238657b203b19ca42c18a0497f16b4844e3074ae8dfdffa3fede21442fcd0069ded0948f8326a753a0fc81f17e8d3e0fb2e0d3628cf35e20c38d18906')
72
- expect(SCrypt::Engine.scrypt('password', 'NaCl', 1024, 8, 16, 64).unpack('H*').first).to eq('fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b3731622eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640')
73
- expect(SCrypt::Engine.scrypt('pleaseletmein', 'SodiumChloride', 16384, 8, 1, 64).unpack('H*').first).to eq('7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2d5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887')
74
- # Raspberry is memory limited, and fails on this test
75
- # expect(SCrypt::Engine.scrypt('pleaseletmein', 'SodiumChloride', 1048576, 8, 1, 64).unpack('H*').first).to eq('2101cb9b6a511aaeaddbbe09cf70f881ec568d574a2ffd4dabe5ee9820adaa478e56fd8f4ba5d09ffa1c6d927c40f4c337304049e8a952fbcbf45c6fa77a41a4')
76
- end
77
-
78
- it "should match equivalent results sent through hash_secret() function" do
79
- expect(SCrypt::Engine.hash_secret('', '10$1$1$0000000000000000', 64)).to match(/\$77d6576238657b203b19ca42c18a0497f16b4844e3074ae8dfdffa3fede21442fcd0069ded0948f8326a753a0fc81f17e8d3e0fb2e0d3628cf35e20c38d18906$/)
80
- expect(SCrypt::Engine.hash_secret('password', '400$8$10$000000004e61436c', 64)).to match(/\$fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b3731622eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640$/)
81
- expect(SCrypt::Engine.hash_secret('pleaseletmein', '4000$8$1$536f6469756d43686c6f72696465', 64)).to match(/\$7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2d5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887$/)
82
- # expect(SCrypt::Engine.hash_secret('pleaseletmein', '100000$8$1$536f6469756d43686c6f72696465', 64)).to match(/\$2101cb9b6a511aaeaddbbe09cf70f881ec568d574a2ffd4dabe5ee9820adaa478e56fd8f4ba5d09ffa1c6d927c40f4c337304049e8a952fbcbf45c6fa77a41a4$/)
77
+ describe 'SCrypt test vectors' do
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)
83
209
  end
84
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