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.
@@ -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 the cost limits of the computation.
36
- # <tt>:key_len</tt> specifies the length in bytes of the key you want to generate. The default is 32 bytes (256 bits). Minimum is 16 bytes (128 bits). Maximum is 512 bytes (4096 bits).
37
- # <tt>:salt_size</tt> specifies the size in bytes of the random salt you want to generate. The default and minimum is 8 bytes (64 bits). Maximum is 32 bytes (256 bits).
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. A value of 0 specifies no upper limit. The minimum is always 1 MB.
40
- # <tt>:max_memfrac</tt> specifies the maximum memory in a fraction of available resources to use. Any value equal to 0 or greater than 0.5 will result in 0.5 being used.
41
- # The scrypt key derivation function is designed to be far more secure against hardware brute-force attacks than alternative functions such as PBKDF2 or bcrypt.
42
- # The designers of scrypt estimate that on modern (2009) hardware, if 5 seconds are spent computing a derived key, the cost of a hardware brute-force attack against scrypt is roughly 4000 times greater than the cost of a similar attack against bcrypt (to find the same password), and 20000 times greater than a similar attack against PBKDF2.
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
- # Clamp minimum/maximum keylen
52
- options[:key_len] = 16 if options[:key_len] < 16
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
- h.match(/^[0-9a-z]+\$[0-9a-z]+\$[0-9a-z]+\$[A-Za-z0-9]{16,64}\$[A-Za-z0-9]{32,1024}$/) != nil
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(h)
93
- n, v, r, salt, hash = h.split('$')
94
- [[n, v, r].join('$') + '$', salt, hash]
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
@@ -7,6 +7,10 @@ module SCrypt
7
7
  module Ext
8
8
  extend FFI::Library
9
9
 
10
- ffi_lib FFI::Compiler::Loader.find('scrypt_ext')
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SCrypt
4
- VERSION = '3.0.7'
4
+ VERSION = '3.1.0'
5
5
  end
data/scrypt.gemspec CHANGED
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rubocop:disable Performance/EndWith, Style/SpecialGlobalVars
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/stakach.pem']
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.add_dependency 'ffi-compiler', '>= 1.0', '< 2.0'
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.rubyforge_project = 'scrypt'
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,autotest}/**/*')
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"
@@ -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 'should calculate a valid cost factor' do
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 'should produce strings' do
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 'should produce random data' do
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 'should used the saved cost factor' do
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(max_time: 30, max_mem: 64 * 1024 * 1024)).not_to start_with(cost)
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(max_time: 30, max_mem: 64 * 1024 * 1024)).to start_with(cost)
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 'should work' do
33
- expect(SCrypt::Engine.autodetect_cost('2a$08$c3$randomjunkgoeshere')).to eq('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 :each do
53
+ before do
43
54
  @salt = SCrypt::Engine.generate_salt
44
55
  @password = 'woo'
45
56
  end
46
57
 
47
- it 'should produce a string' do
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 'should raise an InvalidSalt error if the salt is invalid' do
52
- expect(-> { 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)
53
64
  end
54
65
 
55
- it 'should raise an InvalidSecret error if the secret is invalid' do
56
- expect(-> { SCrypt::Engine.hash_secret(MyInvalidSecret.new, @salt) }).to raise_error(SCrypt::Errors::InvalidSecret)
57
- expect(-> { SCrypt::Engine.hash_secret(nil, @salt) }).to_not raise_error
58
- expect(-> { 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
59
70
  end
60
71
 
61
- it 'should call #to_s on the secret and use the return value as the actual secret data' do
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 'should match results of SCrypt function' do
68
- expect(SCrypt::Engine.scrypt('', '', 16, 1, 1, 64).unpack('H*').first).to eq('77d6576238657b203b19ca42c18a0497f16b4844e3074ae8dfdffa3fede21442fcd0069ded0948f8326a753a0fc81f17e8d3e0fb2e0d3628cf35e20c38d18906')
69
- expect(SCrypt::Engine.scrypt('password', 'NaCl', 1024, 8, 16, 64).unpack('H*').first).to eq('fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b3731622eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640')
70
- expect(SCrypt::Engine.scrypt('pleaseletmein', 'SodiumChloride', 16_384, 8, 1, 64).unpack('H*').first).to eq('7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2d5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887')
71
- # Raspberry is memory limited, and fails on this test
72
- # expect(SCrypt::Engine.scrypt('pleaseletmein', 'SodiumChloride', 1048576, 8, 1, 64).unpack('H*').first).to eq('2101cb9b6a511aaeaddbbe09cf70f881ec568d574a2ffd4dabe5ee9820adaa478e56fd8f4ba5d09ffa1c6d927c40f4c337304049e8a952fbcbf45c6fa77a41a4')
73
- end
74
-
75
- it 'should match equivalent results sent through hash_secret() function' do
76
- expect(SCrypt::Engine.hash_secret('', '10$1$1$0000000000000000', 64)).to match(/\$77d6576238657b203b19ca42c18a0497f16b4844e3074ae8dfdffa3fede21442fcd0069ded0948f8326a753a0fc81f17e8d3e0fb2e0d3628cf35e20c38d18906$/)
77
- expect(SCrypt::Engine.hash_secret('password', '400$8$10$000000004e61436c', 64)).to match(/\$fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b3731622eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640$/)
78
- expect(SCrypt::Engine.hash_secret('pleaseletmein', '4000$8$1$536f6469756d43686c6f72696465', 64)).to match(/\$7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2d5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887$/)
79
- # expect(SCrypt::Engine.hash_secret('pleaseletmein', '100000$8$1$536f6469756d43686c6f72696465', 64)).to match(/\$2101cb9b6a511aaeaddbbe09cf70f881ec568d574a2ffd4dabe5ee9820adaa478e56fd8f4ba5d09ffa1c6d927c40f4c337304049e8a952fbcbf45c6fa77a41a4$/)
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