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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/README.md +149 -23
- data/Rakefile +35 -22
- data/ext/scrypt/warnp.c +7 -1
- data/lib/scrypt/engine.rb +95 -43
- data/lib/scrypt/password.rb +48 -18
- data/lib/scrypt/scrypt_ext.rb +5 -1
- data/lib/scrypt/version.rb +1 -1
- data/scrypt.gemspec +4 -18
- data/spec/fixtures/test_vectors.yml +67 -0
- data/spec/scrypt/engine_spec.rb +157 -28
- data/spec/scrypt/ffi_spec.rb +33 -0
- data/spec/scrypt/integration_spec.rb +129 -0
- data/spec/scrypt/password_spec.rb +104 -56
- data/spec/scrypt/utils_spec.rb +48 -7
- data/spec/spec_helper.rb +40 -0
- data/spec/support/shared_examples.rb +46 -0
- data/spec/support/test_helpers.rb +47 -0
- data.tar.gz.sig +0 -0
- metadata +35 -183
- metadata.gz.sig +0 -0
- data/autotest/discover.rb +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dc64f3f7f338072fc62985fc8aeb406dd88659018f4d52adbe5f6c76e16bcf83
|
4
|
+
data.tar.gz: 832a0fb1e6137422a9de7497f6d17fcb7feab3d1b0c76cdea57a86bfeca0b08a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
2
|
-
======
|
1
|
+
# scrypt
|
3
2
|
|
4
|
-
|
3
|
+
A Ruby library providing a secure password hashing solution using the scrypt key derivation function.
|
5
4
|
|
6
|
-
|
7
|
-
|
5
|
+
[](https://badge.fury.io/rb/scrypt) [](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
|

|
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
|
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
|
-
|
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
|
-
##
|
47
|
+
## Basic Usage
|
22
48
|
|
23
|
-
|
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
|
-
#
|
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
|
-
#
|
58
|
+
# Compare passwords
|
33
59
|
password == "my grand secret" # => true
|
34
60
|
password == "a paltry guess" # => false
|
35
61
|
```
|
36
62
|
|
37
|
-
|
63
|
+
### Configuration Options
|
38
64
|
|
39
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
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 '
|
20
|
+
require './lib/scrypt/version'
|
21
21
|
|
22
|
-
task default: [
|
22
|
+
task default: %i[clean compile_ffi spec]
|
23
23
|
|
24
|
-
desc '
|
25
|
-
|
26
|
-
|
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 '
|
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
|
-
|
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
|
38
|
-
namespace
|
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?
|
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: ['
|
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
|
-
|
75
|
+
desc 'Generate RDoc documentation'
|
76
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
65
77
|
rdoc.rdoc_dir = 'doc/rdoc'
|
66
|
-
rdoc.options << '--
|
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
|
-
|
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 =
|
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
|
-
[
|
10
|
+
%i[size_t double double pointer],
|
12
11
|
:int,
|
13
12
|
blocking: true
|
14
13
|
|
15
14
|
attach_function :crypto_scrypt,
|
16
|
-
[
|
15
|
+
%i[pointer size_t pointer size_t uint64 uint32 uint32 pointer size_t],
|
17
16
|
:int,
|
18
|
-
blocking: true #
|
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
|
-
#
|
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
|
-
|
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
|
-
|
54
|
+
case args.length
|
55
|
+
when 2
|
43
56
|
# args is [cost_string, key_len]
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
51
|
-
|
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
|
75
|
+
salt_only = extract_salt_from_string(salt)
|
66
76
|
|
67
|
-
if salt_only
|
68
|
-
|
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
|
-
|
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(
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
114
|
-
#
|
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
|
-
|
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
|
-
|
135
|
-
(128 *
|
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
|
-
|
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 #{
|
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,
|
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
|
-
|
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
|