scrypt 3.0.8 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8179f4b0bf8f79e8476cc36f386e1fa349efa0db814a7f40a7f67b521a3a80b2
4
- data.tar.gz: 99dd37077aa2003bf966336aee1f395cc655fcf61aac44ed12f049e36e66b62a
3
+ metadata.gz: dc64f3f7f338072fc62985fc8aeb406dd88659018f4d52adbe5f6c76e16bcf83
4
+ data.tar.gz: 832a0fb1e6137422a9de7497f6d17fcb7feab3d1b0c76cdea57a86bfeca0b08a
5
5
  SHA512:
6
- metadata.gz: 94b935e06c8d3e342b5de545cc18a8d4e04ed589d22c9006b90049e39970f1063939b660da88be43f293d67156ec394b238955bb68477bc3aefb0d70b74f8bdd
7
- data.tar.gz: e13414929c7bb835c456e7e4ad3db8f4dedf8092cf0e3fd7413fd423c39591b9b80575d9baeea35ced8d70cb9c82718d9b3fd6429d2c4452d7a87025013d960b
6
+ metadata.gz: 1ae13fbf6b63924096d36990f87d0244d8d6442400f7aeb27f0571bf8eed7f036fb934ec49e69e8472bc653616c4bbaf161c721d974e06ef602d4ec8d1831019
7
+ data.tar.gz: f619909e8e6e011e59b2a7102d5684910cbb1e028b51bc5a08225d35e1d04810348be17b95d3e59cf2b354f1677dd64e0952873db472846c7ea960a388649c04
checksums.yaml.gz.sig CHANGED
Binary file
data/README.md CHANGED
@@ -1,79 +1,205 @@
1
- scrypt [![Build Status](https://secure.travis-ci.org/pbhogan/scrypt.svg)](http://travis-ci.org/pbhogan/scrypt)
2
- ======
1
+ # scrypt
3
2
 
4
- 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.
3
+ A Ruby library providing a secure password hashing solution using the scrypt key derivation function.
5
4
 
6
- * http://www.tarsnap.com/scrypt.html
7
- * http://github.com/pbhogan/scrypt
5
+ [![Gem Version](https://badge.fury.io/rb/scrypt.svg)](https://badge.fury.io/rb/scrypt) [![Ruby](https://github.com/pbhogan/scrypt/actions/workflows/ruby.yml/badge.svg)](https://github.com/pbhogan/scrypt/actions/workflows/ruby.yml)
6
+
7
+ ## About scrypt
8
+
9
+ 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. It accomplishes this by being deliberately memory-intensive, making it expensive to implement in hardware.
10
+
11
+ **Key Features:**
12
+ - Memory-hard function that resists ASIC and FPGA attacks
13
+ - Configurable computational cost, memory usage, and parallelization
14
+ - Drop-in replacement for bcrypt in most applications
15
+ - Production-ready and battle-tested
16
+
17
+ **Resources:**
18
+ - [Original scrypt paper](http://www.tarsnap.com/scrypt.html)
19
+ - [GitHub repository](http://github.com/pbhogan/scrypt)
8
20
 
9
21
  ## Why you should use scrypt
10
22
 
11
23
  ![KDF comparison](https://github.com/tarcieri/scrypt/raw/modern-readme/kdf-comparison.png)
12
24
 
13
- 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.
25
+ 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 4,000 times greater than the cost of a similar attack against bcrypt (to find the same password), and 20,000 times greater than a similar attack against PBKDF2.
26
+
27
+ ## Installation
14
28
 
15
- ## How to install scrypt
29
+ Add this line to your application's Gemfile:
16
30
 
31
+ ```ruby
32
+ gem 'scrypt'
17
33
  ```
34
+
35
+ And then execute:
36
+
37
+ ```bash
38
+ bundle install
39
+ ```
40
+
41
+ Or install it yourself as:
42
+
43
+ ```bash
18
44
  gem install scrypt
19
45
  ```
20
46
 
21
- ## How to use scrypt
47
+ ## Basic Usage
22
48
 
23
- It works pretty similarly to ruby-bcrypt with a few minor differences, especially where the cost factor is concerned.
49
+ The scrypt gem works similarly to ruby-bcrypt with a few minor differences, especially regarding the cost factor configuration.
24
50
 
25
51
  ```ruby
26
52
  require "scrypt"
27
53
 
28
- # hash a user's password
54
+ # Hash a user's password
29
55
  password = SCrypt::Password.create("my grand secret")
30
56
  # => "400$8$36$78f4ae6983f76119$37ec6ce55a2b928dc56ff9a7d0cdafbd7dbde49d9282c38a40b1434e88f24cf5"
31
57
 
32
- # compare to strings
58
+ # Compare passwords
33
59
  password == "my grand secret" # => true
34
60
  password == "a paltry guess" # => false
35
61
  ```
36
62
 
37
- Password.create takes five options which will determine the key length and salt size, as well as the cost limits of the computation:
63
+ ### Configuration Options
38
64
 
39
- * `:key_len` 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).
40
- * `:salt_size` specifies the size in bytes of the random salt you want to generate. The default and maximum is 32 bytes (256 bits). Minimum is 8 bytes (64 bits).
41
- * `:max_time` specifies the maximum number of seconds the computation should take.
42
- * `:max_mem` specifies the maximum number of bytes the computation should take. A value of 0 specifies no upper limit. The minimum is always 1 MB.
43
- * `:max_memfrac` 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.
44
- * `:cost` specifies a cost string (e.g. `'400$8$19$'`) from the `calibrate` method. The `:max_*` options will be ignored if this option is given, or if `calibrate!` has been called.
65
+ `Password.create` accepts several options to customize the key length, salt size, and computational cost limits:
45
66
 
46
- Default options will result in calculation time of approx. 200 ms with 16 MB memory use.
67
+ * **`:key_len`** - Length in bytes of the generated key. Default: 32 bytes (256 bits). Range: 16-512 bytes.
68
+ * **`:salt_size`** - Size in bytes of the random salt. Default: 32 bytes (256 bits). Range: 8-32 bytes.
69
+ * **`:max_time`** - Maximum computation time in seconds. Default: 0.2 seconds.
70
+ * **`:max_mem`** - Maximum memory usage in bytes. Default: 16 MB. Set to 0 for no limit (minimum 1 MB).
71
+ * **`:max_memfrac`** - Maximum memory as a fraction of available resources. Default: 0.5. Range: 0-0.5.
72
+ * **`:cost`** - Explicit cost string from `calibrate` method (e.g., `'400$8$19$'`). When provided, `max_*` options are ignored.
47
73
 
48
- ## Other things you can do
74
+ **Note:** Default options result in approximately 200ms computation time with 16 MB memory usage.
75
+
76
+ ## Advanced Usage
77
+
78
+ ### Engine Methods
79
+
80
+ The scrypt gem provides low-level access to the scrypt algorithm through the `SCrypt::Engine` class:
49
81
 
50
82
  ```ruby
51
83
  require "scrypt"
52
84
 
85
+ # Calibrate scrypt parameters for your system
53
86
  SCrypt::Engine.calibrate
54
87
  # => "400$8$25$"
55
88
 
89
+ # Generate a salt with default parameters
56
90
  salt = SCrypt::Engine.generate_salt
57
91
  # => "400$8$26$b62e0f787a5fc373"
58
92
 
59
- SCrypt::Engine.hash_secret "my grand secret", salt
93
+ # Hash a secret with a specific salt
94
+ SCrypt::Engine.hash_secret("my grand secret", salt)
60
95
  # => "400$8$26$b62e0f787a5fc373$0399ccd4fa26642d92741b17c366b7f6bd12ccea5214987af445d2bed97bc6a2"
61
96
 
97
+ # Calibrate with custom memory limits and save for future use
62
98
  SCrypt::Engine.calibrate!(max_mem: 16 * 1024 * 1024)
63
99
  # => "4000$8$4$"
64
100
 
101
+ # Subsequent salt generation will use the calibrated parameters
65
102
  SCrypt::Engine.generate_salt
66
103
  # => "4000$8$4$c6d101522d3cb045"
67
104
  ```
68
105
 
106
+ ### Password Creation with Custom Options
107
+
108
+ ```ruby
109
+ # Create password with custom parameters
110
+ password = SCrypt::Password.create("my secret", {
111
+ key_len: 64,
112
+ salt_size: 16,
113
+ max_time: 0.5,
114
+ max_mem: 32 * 1024 * 1024
115
+ })
116
+
117
+ # Create password with pre-calibrated cost
118
+ cost = SCrypt::Engine.calibrate(max_time: 0.1)
119
+ password = SCrypt::Password.create("my secret", cost: cost)
120
+ ```
121
+
69
122
  ## Usage in Rails (and the like)
70
123
 
71
124
  ```ruby
72
- # store it safely in the user model
125
+ ## Usage in Rails (and similar frameworks)
126
+
127
+ # Store password safely in the user model
73
128
  user.update_attribute(:password, SCrypt::Password.create("my grand secret"))
74
129
 
75
- # read it back later
130
+ # Read it back later
76
131
  user.reload!
77
132
  password = SCrypt::Password.new(user.password)
78
133
  password == "my grand secret" # => true
79
134
  ```
135
+
136
+ ## Security Considerations
137
+
138
+ * **Memory Safety**: The scrypt algorithm requires significant memory, making it resistant to hardware-based attacks
139
+ * **Time-Memory Trade-off**: Higher memory requirements make it expensive to parallelize attacks
140
+ * **Parameter Selection**: Use `calibrate` to find optimal parameters for your system's performance requirements
141
+ * **Salt Generation**: Always use cryptographically secure random salts (handled automatically)
142
+
143
+ ## Performance Tuning
144
+
145
+ The scrypt parameters can be tuned based on your security and performance requirements:
146
+
147
+ ```ruby
148
+ # For high-security applications (slower)
149
+ password = SCrypt::Password.create("secret", max_time: 1.0, max_mem: 64 * 1024 * 1024)
150
+
151
+ # For faster authentication (less secure)
152
+ password = SCrypt::Password.create("secret", max_time: 0.1, max_mem: 8 * 1024 * 1024)
153
+
154
+ # Calibrate once and reuse parameters
155
+ SCrypt::Engine.calibrate!(max_time: 0.5)
156
+ # All subsequent Password.create calls will use these parameters
157
+ ```
158
+
159
+ ## Error Handling
160
+
161
+ The library raises specific exceptions for different error conditions:
162
+
163
+ ```ruby
164
+ begin
165
+ SCrypt::Password.new("invalid_hash_format")
166
+ rescue SCrypt::Errors::InvalidHash => e
167
+ puts "Invalid hash format: #{e.message}"
168
+ end
169
+
170
+ begin
171
+ SCrypt::Engine.hash_secret(nil, "salt")
172
+ rescue SCrypt::Errors::InvalidSecret => e
173
+ puts "Invalid secret: #{e.message}"
174
+ end
175
+ ```
176
+
177
+ ## Contributing
178
+
179
+ 1. Fork the repository
180
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
181
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
182
+ 4. Push to the branch (`git push origin my-new-feature`)
183
+ 5. Create a new Pull Request
184
+
185
+ ## Acknowledgments
186
+
187
+ ### Original scrypt Algorithm
188
+ - **Colin Percival** and **Tarsnap** for creating the scrypt key derivation function and providing the reference implementation
189
+ - The original scrypt paper: [Stronger Key Derivation via Sequential Memory-Hard Functions](http://www.tarsnap.com/scrypt.html)
190
+
191
+ ### Core Collaborators
192
+
193
+ - **Patrick Hogan** ([@pbhogan](https://github.com/pbhogan))
194
+ - **Stephen von Takach** ([@stakach](https://github.com/stakach))
195
+ - **Rene van Paassen** ([@repagh](https://github.com/repagh))
196
+ - **Johanns Gregorian** ([@johanns](https://github.com/johanns))
197
+
198
+ ### Special Thanks
199
+ - The Ruby community for testing and feedback
200
+ - Contributors who have submitted bug reports, feature requests, and patches
201
+ - The cryptography community for security reviews and recommendations
202
+
203
+ ## License
204
+
205
+ This project is licensed under the BSD-3-Clause License - see the [COPYING](COPYING) file for details.
data/Rakefile CHANGED
@@ -2,45 +2,57 @@
2
2
 
3
3
  require 'bundler/setup'
4
4
  require 'bundler/gem_tasks'
5
+ require 'digest/sha2'
5
6
 
7
+ require 'ffi'
8
+ require 'ffi-compiler/compile_task'
9
+
10
+ require 'fileutils'
6
11
  require 'rake'
7
12
  require 'rake/clean'
13
+ require 'rdoc/task'
8
14
 
9
15
  require 'rspec/core/rake_task'
10
16
 
11
- require 'ffi'
12
- require 'ffi-compiler/compile_task'
13
-
14
- require 'digest/sha2'
15
- require './lib/scrypt/version'
16
-
17
17
  require 'rubygems'
18
18
  require 'rubygems/package_task'
19
19
 
20
- require 'rdoc/task'
20
+ require './lib/scrypt/version'
21
21
 
22
- task default: [:clean, :compile_ffi, :spec]
22
+ task default: %i[clean compile_ffi spec]
23
23
 
24
- desc 'clean, make and run specs'
25
- task :spec do
26
- RSpec::Core::RakeTask.new
24
+ desc 'Run all specs'
25
+ RSpec::Core::RakeTask.new(:spec) do |t|
26
+ t.rspec_opts = ['--color', '--backtrace', '--format', 'documentation']
27
27
  end
28
28
 
29
- desc 'generate checksum'
29
+ desc 'Generate checksum for built gem'
30
30
  task :checksum do
31
31
  built_gem_path = "pkg/scrypt-#{SCrypt::VERSION}.gem"
32
+
33
+ unless File.exist?(built_gem_path)
34
+ puts "Gem file not found: #{built_gem_path}"
35
+ puts "Run 'rake build' first to create the gem."
36
+ exit 1
37
+ end
38
+
32
39
  checksum = Digest::SHA512.new.hexdigest(File.read(built_gem_path))
33
40
  checksum_path = "checksum/scrypt-#{SCrypt::VERSION}.gem.sha512"
34
- File.open(checksum_path, 'w') { |f| f.write(checksum) }
41
+
42
+ # Ensure checksum directory exists
43
+ FileUtils.mkdir_p(File.dirname(checksum_path))
44
+
45
+ File.write(checksum_path, checksum)
46
+ puts "Checksum written to: #{checksum_path}"
35
47
  end
36
48
 
37
- desc 'FFI compiler'
38
- namespace 'ffi-compiler' do
49
+ desc 'Compile FFI extension'
50
+ namespace :ffi_compiler do
39
51
  FFI::Compiler::CompileTask.new('ext/scrypt/scrypt_ext') do |t|
40
52
  target_cpu = RbConfig::CONFIG['target_cpu']
41
53
 
42
54
  t.cflags << '-Wall -std=c99'
43
- t.cflags << '-msse -msse2' if t.platform.arch.include? '86'
55
+ t.cflags << '-msse -msse2' if t.platform.arch.include?('86')
44
56
  t.cflags << '-D_GNU_SOURCE=1' if RbConfig::CONFIG['host_os'].downcase =~ /mingw/
45
57
  t.cflags << '-D_POSIX_C_SOURCE=200809L' if RbConfig::CONFIG['host_os'].downcase =~ /linux/
46
58
 
@@ -55,22 +67,23 @@ namespace 'ffi-compiler' do
55
67
  t.add_define 'WINDOWS_OS' if FFI::Platform.windows?
56
68
  end
57
69
  end
58
- task compile_ffi: ['ffi-compiler:default']
70
+ task compile_ffi: ['ffi_compiler:default']
59
71
 
60
72
  CLEAN.include('ext/scrypt/*{.o,.log,.so,.bundle}')
61
73
  CLEAN.include('lib/**/*{.o,.log,.so,.bundle}')
62
74
 
63
- desc 'Generate RDoc'
64
- rd = Rake::RDocTask.new do |rdoc|
75
+ desc 'Generate RDoc documentation'
76
+ RDoc::Task.new(:rdoc) do |rdoc|
65
77
  rdoc.rdoc_dir = 'doc/rdoc'
66
- rdoc.options << '--title' << 'scrypt-ruby' << '--line-numbers' << '--inline-source' << '--main' << 'README'
78
+ rdoc.options << '--force-update'
79
+ rdoc.options << '-V'
80
+
67
81
  rdoc.template = ENV['TEMPLATE'] if ENV['TEMPLATE']
68
- rdoc.rdoc_files.include('COPYING', 'lib/**/*.rb')
69
82
  end
70
83
 
71
84
  desc 'Run all specs'
72
85
  RSpec::Core::RakeTask.new do |_t|
73
- rspec_opts = ['--colour', '--backtrace']
86
+ # Task automatically runs specs based on RSpec defaults
74
87
  end
75
88
 
76
89
  def gem_spec
data/ext/scrypt/warnp.c CHANGED
@@ -36,7 +36,13 @@ warnp_setprogname(const char * progname)
36
36
  p = progname + 1;
37
37
 
38
38
  /* Copy the name string. */
39
- name = strdup(p);
39
+ name = malloc(strlen(p) + 1);
40
+ if (name == NULL) {
41
+ /* No cleanup handler needs to be registered on failure. */
42
+ return;
43
+ }
44
+ strncpy(name, p, strlen(p) + 1);
45
+ name[strlen(p)] = '\0'; /* Ensure null termination */
40
46
 
41
47
  /* If we haven't already done so, register our exit handler. */
42
48
  if (initialized == 0) {
data/lib/scrypt/engine.rb CHANGED
@@ -5,22 +5,28 @@ require 'openssl'
5
5
 
6
6
  module SCrypt
7
7
  module Ext
8
- # rubocop:disable Style/SymbolArray
9
8
  # Bind the external functions
10
9
  attach_function :sc_calibrate,
11
- [:size_t, :double, :double, :pointer],
10
+ %i[size_t double double pointer],
12
11
  :int,
13
12
  blocking: true
14
13
 
15
14
  attach_function :crypto_scrypt,
16
- [:pointer, :size_t, :pointer, :size_t, :uint64, :uint32, :uint32, :pointer, :size_t],
15
+ %i[pointer size_t pointer size_t uint64 uint32 uint32 pointer size_t],
17
16
  :int,
18
- blocking: true # todo
19
- # rubocop:enable
17
+ blocking: true # Use blocking: true for CPU-intensive operations to avoid GIL issues
20
18
  end
21
19
 
22
20
  class Engine
23
- # rubocop:disable Style/MutableConstant
21
+ # Regular expressions for validation
22
+ COST_PATTERN = /^[0-9a-z]+\$[0-9a-z]+\$[0-9a-z]+\$$/.freeze
23
+ SALT_PATTERN = /^[0-9a-z]+\$[0-9a-z]+\$[0-9a-z]+\$[A-Za-z0-9]{16,64}$/.freeze
24
+ HASH_PATTERN = /^[0-9a-z]+\$[0-9a-z]+\$[0-9a-z]+\$[A-Za-z0-9]{16,64}\$[A-Za-z0-9]{32,1024}$/.freeze
25
+
26
+ # Constants for salt handling
27
+ OLD_STYLE_SALT_LENGTH = 40
28
+ SALT_MIN_LENGTH = 16
29
+
24
30
  DEFAULTS = {
25
31
  key_len: 32,
26
32
  salt_size: 32,
@@ -28,8 +34,14 @@ module SCrypt
28
34
  max_memfrac: 0.5,
29
35
  max_time: 0.2,
30
36
  cost: nil
31
- }
32
- # rubocop:enable
37
+ }.freeze
38
+
39
+ # Class variable to store calibrated cost, separate from defaults
40
+ @calibrated_cost = nil
41
+
42
+ class << self
43
+ attr_accessor :calibrated_cost
44
+ end
33
45
 
34
46
  class Calibration < FFI::Struct
35
47
  layout :n, :uint64,
@@ -39,18 +51,16 @@ module SCrypt
39
51
 
40
52
  class << self
41
53
  def scrypt(secret, salt, *args)
42
- if args.length == 2
54
+ case args.length
55
+ when 2
43
56
  # args is [cost_string, key_len]
44
- n, r, p = args[0].split('$').map { |x| x.to_i(16) }
45
- key_len = args[1]
46
-
47
- __sc_crypt(secret, salt, n, r, p, key_len)
48
- elsif args.length == 4
57
+ cost_string, key_len = args
58
+ cpu_cost, memory_cost, parallelization = parse_cost_string(cost_string)
59
+ __sc_crypt(secret, salt, cpu_cost, memory_cost, parallelization, key_len)
60
+ when 4
49
61
  # args is [n, r, p, key_len]
50
- n, r, p = args[0, 3]
51
- key_len = args[3]
52
-
53
- __sc_crypt(secret, salt, n, r, p, key_len)
62
+ cpu_cost, memory_cost, parallelization, key_len = args
63
+ __sc_crypt(secret, salt, cpu_cost, memory_cost, parallelization, key_len)
54
64
  else
55
65
  raise ArgumentError, 'invalid number of arguments (4 or 6)'
56
66
  end
@@ -62,15 +72,12 @@ module SCrypt
62
72
  raise Errors::InvalidSalt, 'invalid salt' unless valid_salt?(salt)
63
73
 
64
74
  cost = autodetect_cost(salt)
65
- salt_only = salt[/\$([A-Za-z0-9]{16,64})$/, 1]
75
+ salt_only = extract_salt_from_string(salt)
66
76
 
67
- if salt_only.length == 40
68
- # Old-style hash with 40-character salt
69
- salt + '$' + Digest::SHA1.hexdigest(scrypt(secret.to_s, salt, cost, 256))
77
+ if old_style_hash?(salt_only)
78
+ generate_old_style_hash(secret, salt, cost)
70
79
  else
71
- # New-style hash
72
- salt_only = [salt_only.sub(/^(00)+/, '')].pack('H*')
73
- salt + '$' + scrypt(secret.to_s, salt_only, cost, key_len).unpack('H*').first.rjust(key_len * 2, '0')
80
+ generate_new_style_hash(secret, salt, salt_only, cost, key_len)
74
81
  end
75
82
  end
76
83
 
@@ -81,24 +88,21 @@ module SCrypt
81
88
  # <tt>:cost</tt> is a cost string returned by SCrypt::Engine.calibrate
82
89
  def generate_salt(options = {})
83
90
  options = DEFAULTS.merge(options)
84
- cost = options[:cost] || calibrate(options)
85
- salt = OpenSSL::Random.random_bytes(options[:salt_size]).unpack('H*').first.rjust(16, '0')
91
+ cost = options[:cost] || @calibrated_cost || calibrate(options)
92
+ salt = OpenSSL::Random.random_bytes(options[:salt_size]).unpack('H*').first.rjust(SALT_MIN_LENGTH, '0')
86
93
 
87
- if salt.length == 40
88
- # If salt is 40 characters, the regexp will think that it is an old-style hash, so add a '0'.
89
- salt = '0' + salt
90
- end
94
+ salt = avoid_old_style_collision(salt)
91
95
  cost + salt
92
96
  end
93
97
 
94
98
  # Returns true if +cost+ is a valid cost, false if not.
95
99
  def valid_cost?(cost)
96
- cost.match(/^[0-9a-z]+\$[0-9a-z]+\$[0-9a-z]+\$$/) != nil
100
+ !COST_PATTERN.match(cost).nil?
97
101
  end
98
102
 
99
103
  # Returns true if +salt+ is a valid salt, false if not.
100
104
  def valid_salt?(salt)
101
- salt.match(/^[0-9a-z]+\$[0-9a-z]+\$[0-9a-z]+\$[A-Za-z0-9]{16,64}$/) != nil
105
+ !SALT_PATTERN.match(salt).nil?
102
106
  end
103
107
 
104
108
  # Returns true if +secret+ is a valid secret, false if not.
@@ -110,8 +114,10 @@ module SCrypt
110
114
  #
111
115
  # Options:
112
116
  # <tt>:max_time</tt> specifies the maximum number of seconds the computation should take.
113
- # <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.
114
- # <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.
117
+ # <tt>:max_mem</tt> specifies the maximum number of bytes the computation should take.
118
+ # A value of 0 specifies no upper limit. The minimum is always 1 MB.
119
+ # <tt>:max_memfrac</tt> specifies the maximum memory in a fraction of available resources to use.
120
+ # Any value equal to 0 or greater than 0.5 will result in 0.5 being used.
115
121
  #
116
122
  # Example:
117
123
  #
@@ -126,13 +132,13 @@ module SCrypt
126
132
  # Calls SCrypt::Engine.calibrate and saves the cost string for future calls to
127
133
  # SCrypt::Engine.generate_salt.
128
134
  def calibrate!(options = {})
129
- DEFAULTS[:cost] = calibrate(options)
135
+ @calibrated_cost = calibrate(options)
130
136
  end
131
137
 
132
138
  # Computes the memory use of the given +cost+
133
139
  def memory_use(cost)
134
- n, r, p = cost.split('$').map { |i| i.to_i(16) }
135
- (128 * r * p) + (256 * r) + (128 * r * n)
140
+ cpu_cost, memory_cost, parallelization = parse_cost_string(cost)
141
+ (128 * memory_cost * parallelization) + (256 * memory_cost) + (128 * memory_cost * cpu_cost)
136
142
  end
137
143
 
138
144
  # Autodetects the cost from the salt string.
@@ -142,28 +148,74 @@ module SCrypt
142
148
 
143
149
  private
144
150
 
151
+ # Extracts the salt portion from a salt string
152
+ def extract_salt_from_string(salt)
153
+ salt[/\$([A-Za-z0-9]{16,64})$/, 1]
154
+ end
155
+
156
+ # Checks if this is an old-style hash based on salt length
157
+ def old_style_hash?(salt_only)
158
+ salt_only.length == OLD_STYLE_SALT_LENGTH
159
+ end
160
+
161
+ # Generates old-style hash with SHA1
162
+ def generate_old_style_hash(secret, salt, cost)
163
+ "#{salt}$#{Digest::SHA1.hexdigest(scrypt(secret.to_s, salt, cost, 256))}"
164
+ end
165
+
166
+ # Generates new-style hash
167
+ def generate_new_style_hash(secret, salt, salt_only, cost, key_len)
168
+ processed_salt = [salt_only.sub(/^(00)+/, '')].pack('H*')
169
+ hash_bytes = scrypt(secret.to_s, processed_salt, cost, key_len)
170
+ "#{salt}$#{hash_bytes.unpack('H*').first.rjust(key_len * 2, '0')}"
171
+ end
172
+
173
+ # Avoids collision with old-style hash detection
174
+ def avoid_old_style_collision(salt)
175
+ if salt.length == OLD_STYLE_SALT_LENGTH
176
+ # If salt is 40 characters, the regexp will think that it is an old-style hash, so add a '0'.
177
+ "0#{salt}"
178
+ else
179
+ salt
180
+ end
181
+ end
182
+
183
+ # Parses a cost string into its component values
184
+ def parse_cost_string(cost_string)
185
+ cost_string.split('$').map { |component| component.to_i(16) }
186
+ end
187
+
145
188
  def __sc_calibrate(max_mem, max_memfrac, max_time)
146
- result = nil
189
+ raise ArgumentError, 'max_mem must be non-negative' if max_mem.negative?
190
+ raise ArgumentError, 'max_memfrac must be between 0 and 1' unless (0..1).cover?(max_memfrac)
191
+ raise ArgumentError, 'max_time must be positive' if max_time <= 0
147
192
 
148
193
  calibration = Calibration.new
149
194
  ret_val = SCrypt::Ext.sc_calibrate(max_mem, max_memfrac, max_time, calibration)
150
195
 
151
- raise "calibration error #{result}" unless ret_val.zero?
196
+ raise "calibration error: return value #{ret_val}" unless ret_val.zero?
152
197
 
153
198
  [calibration[:n], calibration[:r], calibration[:p]]
154
199
  end
155
200
 
156
- def __sc_crypt(secret, salt, n, r, p, key_len)
201
+ def __sc_crypt(secret, salt, cpu_cost, memory_cost, parallelization, key_len)
202
+ raise ArgumentError, 'secret cannot be nil' if secret.nil?
203
+ raise ArgumentError, 'salt cannot be nil' if salt.nil?
204
+ raise ArgumentError, 'key_len must be positive' if key_len <= 0
205
+ raise ArgumentError, 'cpu_cost must be positive' if cpu_cost <= 0
206
+ raise ArgumentError, 'memory_cost must be positive' if memory_cost <= 0
207
+ raise ArgumentError, 'parallelization must be positive' if parallelization <= 0
208
+
157
209
  result = nil
158
210
 
159
211
  FFI::MemoryPointer.new(:char, key_len) do |buffer|
160
212
  ret_val = SCrypt::Ext.crypto_scrypt(
161
213
  secret, secret.bytesize, salt, salt.bytesize,
162
- n, r, p,
214
+ cpu_cost, memory_cost, parallelization,
163
215
  buffer, key_len
164
216
  )
165
217
 
166
- raise "scrypt error #{ret_val}" unless ret_val.zero?
218
+ raise "scrypt error: return value #{ret_val}" unless ret_val.zero?
167
219
 
168
220
  result = buffer.read_string(key_len)
169
221
  end