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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/README.md +149 -23
- data/Rakefile +38 -24
- data/ext/scrypt/Rakefile +1 -1
- data/ext/scrypt/warnp.c +7 -1
- data/lib/scrypt/engine.rb +227 -0
- data/lib/scrypt/errors.rb +14 -0
- data/lib/scrypt/password.rb +127 -0
- data/lib/scrypt/scrypt_ext.rb +8 -1
- data/lib/scrypt/security_utils.rb +4 -3
- data/lib/scrypt/version.rb +3 -1
- data/lib/scrypt.rb +7 -266
- data/scrypt.gemspec +29 -24
- data/spec/fixtures/test_vectors.yml +67 -0
- data/spec/scrypt/engine_spec.rb +171 -45
- data/spec/scrypt/ffi_spec.rb +33 -0
- data/spec/scrypt/integration_spec.rb +129 -0
- data/spec/scrypt/password_spec.rb +124 -78
- data/spec/scrypt/utils_spec.rb +53 -10
- data/spec/spec_helper.rb +46 -3
- data/spec/support/shared_examples.rb +46 -0
- data/spec/support/test_helpers.rb +47 -0
- data.tar.gz.sig +0 -0
- metadata +40 -104
- metadata.gz.sig +0 -0
- data/autotest/discover.rb +0 -1
|
@@ -1,139 +1,185 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
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
|
|
13
|
-
expect
|
|
14
|
+
it 'returns a valid password' do
|
|
15
|
+
expect { SCrypt::Password.new(@password) }.not_to raise_error
|
|
14
16
|
end
|
|
15
17
|
|
|
16
|
-
it
|
|
17
|
-
expect
|
|
18
|
-
expect
|
|
19
|
-
expect
|
|
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
|
|
23
|
-
expect
|
|
24
|
-
expect
|
|
25
|
-
expect(
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
@
|
|
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
|
|
37
|
+
it 'reads the cost, salt, and hash' do
|
|
37
38
|
password = SCrypt::Password.new(@hash)
|
|
38
|
-
expect(password.cost).to eq(
|
|
39
|
-
expect(password.salt).to eq(
|
|
40
|
-
expect(password.
|
|
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
|
|
44
|
-
expect
|
|
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
|
|
49
|
-
before
|
|
50
|
-
@secret =
|
|
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
|
|
55
|
-
expect(
|
|
55
|
+
it 'compares successfully to the original secret' do
|
|
56
|
+
expect(@password == @secret).to be true
|
|
56
57
|
end
|
|
57
58
|
|
|
58
|
-
it
|
|
59
|
-
expect(
|
|
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
|
|
65
|
-
before
|
|
66
|
-
@secret =
|
|
64
|
+
describe 'non-default salt sizes' do
|
|
65
|
+
before do
|
|
66
|
+
@secret = 's3cret'
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
-
it
|
|
70
|
-
@password = SCrypt::Password.create(@secret, :
|
|
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
|
|
75
|
-
@password = SCrypt::Password.create(@secret, :
|
|
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
|
|
80
|
-
@password = SCrypt::Password.create(@secret, :
|
|
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
|
|
85
|
-
@password = SCrypt::Password.create(@secret, :
|
|
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
|
|
90
|
-
@password = SCrypt::Password.create(@secret, :
|
|
91
|
-
expect(
|
|
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
|
|
97
|
-
before
|
|
98
|
-
@secret =
|
|
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
|
|
102
|
-
@password = SCrypt::Password.create(@secret, :
|
|
103
|
-
expect(@password.digest.length).to eq(
|
|
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
|
|
107
|
-
@password = SCrypt::Password.create(@secret, :
|
|
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
|
|
112
|
-
@password = SCrypt::Password.create(@secret, :
|
|
113
|
-
expect(@password
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
130
|
-
|
|
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
|
|
135
|
-
it '
|
|
136
|
-
|
|
137
|
-
|
|
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
|
data/spec/scrypt/utils_spec.rb
CHANGED
|
@@ -1,12 +1,55 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
3
|
-
require
|
|
4
|
-
require
|
|
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
|