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,139 +1,185 @@
1
- require File.expand_path(File.join(File.dirname(__FILE__), "..", "spec_helper"))
1
+ # frozen_string_literal: true
2
2
 
3
- describe "Creating a hashed password" do
4
- before :each do
5
- @password = SCrypt::Password.create("s3cr3t", :max_time => 0.25)
3
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
4
+
5
+ describe 'Creating a hashed password' do
6
+ before do
7
+ @password = SCrypt::Password.create('s3cr3t', max_time: 0.25)
6
8
  end
7
9
 
8
- it "should return a SCrypt::Password" do
10
+ it 'returns a SCrypt::Password' do
9
11
  expect(@password).to be_an_instance_of(SCrypt::Password)
10
12
  end
11
13
 
12
- it "should return a valid password" do
13
- expect(lambda { 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
14
16
  end
15
17
 
16
- it "should behave normally if the secret is not a string" do
17
- expect(lambda { SCrypt::Password.create(nil) }).to_not raise_error
18
- expect(lambda { SCrypt::Password.create({:woo => "yeah"}) }).to_not raise_error
19
- expect(lambda { 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
20
22
  end
21
23
 
22
- it "should tolerate empty string secrets" do
23
- expect(lambda { SCrypt::Password.create( "\n".chop ) }).to_not raise_error
24
- expect(lambda { SCrypt::Password.create( "" ) }).to_not raise_error
25
- expect(lambda { SCrypt::Password.create( String.new ) }).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)
26
28
  end
27
29
  end
28
30
 
29
-
30
- describe "Reading a hashed password" do
31
- before :each do
32
- @secret = "my secret"
33
- @hash = "400$8$d$173a8189751c095a29b933789560b73bf17b2e01$9bf66d74bd6f3ebcf99da3b379b689b89db1cb07"
31
+ describe 'Reading a hashed password' do
32
+ before do
33
+ @secret = 'my secret'
34
+ @hash = '400$8$d$173a8189751c095a29b933789560b73bf17b2e01$9bf66d74bd6f3ebcf99da3b379b689b89db1cb07'
34
35
  end
35
36
 
36
- it "should read the cost, salt, and hash" do
37
+ it 'reads the cost, salt, and hash' do
37
38
  password = SCrypt::Password.new(@hash)
38
- expect(password.cost).to eq("400$8$d$")
39
- expect(password.salt).to eq("173a8189751c095a29b933789560b73bf17b2e01")
40
- expect(password.to_s).to eq(@hash)
39
+ expect(password.cost).to eq('400$8$d$')
40
+ expect(password.salt).to eq('173a8189751c095a29b933789560b73bf17b2e01')
41
+ expect(password.digest).to eq('9bf66d74bd6f3ebcf99da3b379b689b89db1cb07')
41
42
  end
42
43
 
43
- it "should raise an InvalidHashError when given an invalid hash" do
44
- expect(lambda { 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)
45
46
  end
46
47
  end
47
48
 
48
- describe "Comparing a hashed password with a secret" do
49
- before :each do
50
- @secret = "s3cr3t"
51
- @password = SCrypt::Password.create(@secret)
49
+ describe 'Comparing a hashed password with a secret' do
50
+ before do
51
+ @secret = 's3cr3t'
52
+ @password = SCrypt::Password.create(@secret, max_time: 0.01)
52
53
  end
53
54
 
54
- it "should compare successfully to the original secret" do
55
- expect((@password == @secret)).to be(true)
55
+ it 'compares successfully to the original secret' do
56
+ expect(@password == @secret).to be true
56
57
  end
57
58
 
58
- it "should compare unsuccessfully to anything besides original secret" do
59
- expect((@password == "@secret")).to be(false)
59
+ it 'compares unsuccessfully to anything besides original secret' do
60
+ expect(@password == 'different').to be false
60
61
  end
61
-
62
62
  end
63
63
 
64
- describe "non-default salt sizes" do
65
- before :each do
66
- @secret = "s3cret"
64
+ describe 'non-default salt sizes' do
65
+ before do
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
85
- @password = SCrypt::Password.create(@secret, :salt_size => 20)
84
+ it 'pads a 20-byte salt to not look like a 20-byte SHA1' do
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
-
94
93
  end
95
94
 
96
- describe "non-default key lengths" do
97
- before :each do
98
- @secret = "s3cret"
95
+ describe 'non-default key lengths' do
96
+ before do
97
+ @secret = 's3cret'
98
+ end
99
+
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)
99
103
  end
100
104
 
101
- it "should enforce a minimum keylength of 16 bytes" do
102
- @password = SCrypt::Password.create(@secret, :key_len => 15)
103
- expect(@password.digest.length).to eq(16 * 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)
104
108
  end
105
109
 
106
- it "should allow a keylength of 512 bytes" do
107
- @password = SCrypt::Password.create(@secret, :key_len => 512)
108
- 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)
109
113
  end
110
114
 
111
- it "should enforce a maximum keylength of 512 bytes" do
112
- @password = SCrypt::Password.create(@secret, :key_len => 513)
113
- expect(@password.digest.length).to eq(512 * 2)
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
114
118
  end
119
+ end
115
120
 
116
- it "should properly compare a non-standard hash" do
117
- @password = SCrypt::Password.create(@secret, :key_len => 512)
118
- expect((SCrypt::Password.new(@password.to_s) == @secret)).to be(true)
121
+ describe 'Old-style hashes' do
122
+ before do
123
+ @secret = 'my secret'
124
+ @hash = '400$8$d$173a8189751c095a29b933789560b73bf17b2e01$9bf66d74bd6f3ebcf99da3b379b689b89db1cb07'
119
125
  end
120
126
 
127
+ it 'compares successfully' do
128
+ expect(SCrypt::Password.new(@hash) == @secret).to be true
129
+ end
130
+ end
131
+
132
+ describe 'Respecting standard ruby behaviors' do
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
121
137
  end
122
138
 
123
- describe "Old-style hashes" do
124
- before :each do
125
- @secret = "my secret"
126
- @hash = "400$8$d$173a8189751c095a29b933789560b73bf17b2e01$9bf66d74bd6f3ebcf99da3b379b689b89db1cb07"
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)
127
155
  end
128
156
 
129
- it "should compare successfully" do
130
- expect((SCrypt::Password.new(@hash) == @secret)).to be(true)
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
131
162
  end
132
163
  end
133
164
 
134
- describe "Respecting standard ruby behaviors" do
135
- it 'should hash as an integer' do
136
- password = SCrypt::Password.create('')
137
- expect(password.hash).to be_kind_of(Integer)
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)
138
184
  end
139
185
  end
@@ -1,12 +1,55 @@
1
- require File.expand_path(File.join(File.dirname(__FILE__), "..", "spec_helper"))
2
-
3
- describe "Security Utils" do
4
- it "should perform a string comparison" do
5
- expect(SCrypt::SecurityUtils.secure_compare('a', 'a')).to equal(true)
6
- expect(SCrypt::SecurityUtils.secure_compare('a', 'b')).to equal(false)
7
- expect(SCrypt::SecurityUtils.secure_compare('aa', 'aa')).to equal(true)
8
- expect(SCrypt::SecurityUtils.secure_compare('aa', 'ab')).to equal(false)
9
- expect(SCrypt::SecurityUtils.secure_compare('aa', 'aaa')).to equal(false)
10
- expect(SCrypt::SecurityUtils.secure_compare('aaa', 'aa')).to equal(false)
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
4
+
5
+ describe 'Security Utils' do
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
11
54
  end
12
55
  end
data/spec/spec_helper.rb CHANGED
@@ -1,4 +1,47 @@
1
+ # frozen_string_literal: true
2
+
1
3
  $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib'))
2
- require "rubygems"
3
- require "rspec"
4
- require "scrypt"
4
+
5
+ require 'rubygems'
6
+ require 'rspec'
7
+ require 'yaml'
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