sorcery-argon2 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ffi'
4
+ require 'ffi-compiler/loader'
5
+
6
+ module Argon2
7
+ ##
8
+ # Direct external bindings. Call these methods via the Engine class to ensure
9
+ # points are dealt with.
10
+ #
11
+ module Ext
12
+ extend FFI::Library
13
+ ffi_lib FFI::Compiler::Loader.find(FFI::Platform.windows? ? 'libargon2_wrap' : 'argon2_wrap')
14
+
15
+ # int argon2i_hash_raw(const uint32_t t_cost, const uint32_t m_cost,
16
+ # const uint32_t parallelism, const void *pwd,
17
+ # const size_t pwdlen, const void *salt,
18
+ # const size_t saltlen, void *hash, const size_t hashlen);
19
+
20
+ attach_function :argon2i_hash_raw, %i[
21
+ uint uint uint pointer
22
+ size_t pointer size_t pointer size_t
23
+ ], :int, :blocking => true
24
+
25
+ # int argon2id_hash_raw(const uint32_t t_cost, const uint32_t m_cost,
26
+ # const uint32_t parallelism, const void *pwd,
27
+ # const size_t pwdlen, const void *salt,
28
+ # const size_t saltlen, void *hash, const size_t hashlen)
29
+ attach_function :argon2id_hash_raw, %i[
30
+ uint uint uint pointer
31
+ size_t pointer size_t pointer size_t
32
+ ], :int, :blocking => true
33
+
34
+ # void argon2_wrap(uint8_t *out, char *pwd, size_t pwdlen,
35
+ # uint8_t *salt, uint32_t saltlen, uint32_t t_cost,
36
+ # uint32_t m_cost, uint32_t lanes,
37
+ # uint8_t *secret, uint32_t secretlen)
38
+ attach_function :argon2_wrap, %i[
39
+ pointer pointer size_t pointer uint uint
40
+ uint uint pointer size_t
41
+ ], :int, :blocking => true
42
+
43
+ # int argon2i_verify(const char *encoded, const void *pwd,
44
+ # const size_t pwdlen);
45
+ attach_function :wrap_argon2_verify, %i[pointer pointer size_t
46
+ pointer size_t], :int, :blocking => true
47
+ end
48
+
49
+ ##
50
+ # The engine class shields users from the FFI interface.
51
+ # It is generally not advised to directly use this class.
52
+ #
53
+ class Engine
54
+ def self.hash_argon2i(password, salt, t_cost, m_cost, out_len = nil)
55
+ out_len = (out_len || Constants::OUT_LEN).to_i
56
+ raise ::Argon2::Errors::InvalidOutputLength if out_len < 1
57
+
58
+ result = ''
59
+ FFI::MemoryPointer.new(:char, out_len) do |buffer|
60
+ ret = Ext.argon2i_hash_raw(t_cost, 1 << m_cost, 1, password,
61
+ password.length, salt, salt.length,
62
+ buffer, out_len)
63
+ raise ::Argon2::Errors::ExtError, ERRORS[ret.abs] unless ret.zero?
64
+
65
+ result = buffer.read_string(out_len)
66
+ end
67
+ result.unpack('H*').join
68
+ end
69
+
70
+ def self.hash_argon2id(password, salt, t_cost, m_cost, out_len = nil)
71
+ out_len = (out_len || Constants::OUT_LEN).to_i
72
+ raise ::Argon2::Errors::InvalidOutputLength if out_len < 1
73
+
74
+ result = ''
75
+ FFI::MemoryPointer.new(:char, out_len) do |buffer|
76
+ ret = Ext.argon2id_hash_raw(t_cost, 1 << m_cost, 1, password,
77
+ password.length, salt, salt.length,
78
+ buffer, out_len)
79
+ raise ::Argon2::Errors::ExtError, ERRORS[ret.abs] unless ret.zero?
80
+
81
+ result = buffer.read_string(out_len)
82
+ end
83
+ result.unpack('H*').join
84
+ end
85
+
86
+ def self.hash_argon2id_encode(password, salt, t_cost, m_cost, secret)
87
+ result = ''
88
+ secretlen = secret.nil? ? 0 : secret.bytesize
89
+ passwordlen = password.nil? ? 0 : password.bytesize
90
+ raise ::Argon2::Errors::InvalidSaltSize if salt.length != Constants::SALT_LEN
91
+
92
+ FFI::MemoryPointer.new(:char, Constants::ENCODE_LEN) do |buffer|
93
+ ret = Ext.argon2_wrap(buffer, password, passwordlen,
94
+ salt, salt.length, t_cost, (1 << m_cost),
95
+ 1, secret, secretlen)
96
+ raise ::Argon2::Errors::ExtError, ERRORS[ret.abs] unless ret.zero?
97
+
98
+ result = buffer.read_string(Constants::ENCODE_LEN)
99
+ end
100
+ result.delete "\0"
101
+ end
102
+
103
+ def self.argon2_verify(pwd, hash, secret)
104
+ secretlen = secret.nil? ? 0 : secret.bytesize
105
+ passwordlen = pwd.nil? ? 0 : pwd.bytesize
106
+
107
+ ret = Ext.wrap_argon2_verify(hash, pwd, passwordlen, secret, secretlen)
108
+ return false if ERRORS[ret.abs] == 'ARGON2_DECODING_FAIL'
109
+ raise ::Argon2::Errors::ExtError, ERRORS[ret.abs] unless ret.zero?
110
+
111
+ true
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Argon2
4
+ ##
5
+ # Front-end API for the Argon2 module.
6
+ #
7
+ class Password
8
+ # Used as the default time cost if one isn't provided when calling
9
+ # Argon2::Password.create
10
+ DEFAULT_T_COST = 2
11
+ # Used to validate the minimum acceptable time cost
12
+ MIN_T_COST = 1
13
+ # Used to validate the maximum acceptable time cost
14
+ MAX_T_COST = 750
15
+ # Used as the default memory cost if one isn't provided when calling
16
+ # Argon2::Password.create
17
+ DEFAULT_M_COST = 16
18
+ # Used to validate the minimum acceptable memory cost
19
+ MIN_M_COST = 3
20
+ # Used to validate the maximum acceptable memory cost
21
+ MAX_M_COST = 31
22
+ # The complete Argon2 digest string (not to be confused with the checksum).
23
+ attr_reader :digest
24
+ # The hash portion of the stored password hash.
25
+ attr_reader :checksum
26
+ # The salt of the stored password hash.
27
+ attr_reader :salt
28
+ # Variant used (argon2i / argon2d / argon2id)
29
+ attr_reader :variant
30
+ # The version of the argon2 algorithm used to create the hash.
31
+ attr_reader :version
32
+ # The time cost factor used to create the hash.
33
+ attr_reader :t_cost
34
+ # The memory cost factor used to create the hash.
35
+ attr_reader :m_cost
36
+ # The parallelism cost factor used to create the hash.
37
+ attr_reader :p_cost
38
+
39
+ ##
40
+ # Class methods
41
+ #
42
+ class << self
43
+ ##
44
+ # Takes a user provided password and returns an Argon2::Password instance
45
+ # with the resulting Argon2 hash.
46
+ #
47
+ # Usage:
48
+ #
49
+ # Argon2::Password.create(password)
50
+ # Argon2::Password.create(password, t_cost: 4, m_cost: 20)
51
+ # Argon2::Password.create(password, secret: pepper)
52
+ # Argon2::Password.create(password, m_cost: 17, secret: pepper)
53
+ #
54
+ # Currently available options:
55
+ #
56
+ # * :t_cost
57
+ # * :m_cost
58
+ # * :secret
59
+ #
60
+ def create(password, options = {})
61
+ raise Argon2::Errors::InvalidPassword unless password.is_a?(String)
62
+
63
+ t_cost = options[:t_cost] || DEFAULT_T_COST
64
+ m_cost = options[:m_cost] || DEFAULT_M_COST
65
+
66
+ raise Argon2::Errors::InvalidTCost if t_cost < MIN_T_COST || t_cost > MAX_T_COST
67
+ raise Argon2::Errors::InvalidMCost if m_cost < MIN_M_COST || m_cost > MAX_M_COST
68
+
69
+ # TODO: Add support for changing the p_cost
70
+
71
+ salt = Engine.saltgen
72
+ secret = options[:secret]
73
+
74
+ Argon2::Password.new(
75
+ Argon2::Engine.hash_argon2id_encode(
76
+ password, salt, t_cost, m_cost, secret
77
+ )
78
+ )
79
+ end
80
+
81
+ ##
82
+ # Regex to validate if the provided String is a valid Argon2 hash output.
83
+ #
84
+ # Supports 1 and argon2id formats.
85
+ #
86
+ def valid_hash?(digest)
87
+ /^\$argon2(id?|d).{,113}/ =~ digest
88
+ end
89
+
90
+ ##
91
+ # Takes a password, Argon2 hash, and optionally a secret, then uses the
92
+ # Argon2 C Library to verify if they match.
93
+ #
94
+ # Also accepts passing another Argon2::Password instance as the password,
95
+ # in which case it will compare the final Argon2 hash for each against
96
+ # each other.
97
+ #
98
+ # Usage:
99
+ #
100
+ # Argon2::Password.verify_password(password, argon2_hash)
101
+ # Argon2::Password.verify_password(password, argon2_hash, secret)
102
+ #
103
+ def verify_password(password, digest, secret = nil)
104
+ digest = digest.to_s
105
+ if password.is_a?(Argon2::Password)
106
+ password == Argon2::Password.new(digest)
107
+ else
108
+ Argon2::Engine.argon2_verify(password, digest, secret)
109
+ end
110
+ end
111
+ end
112
+
113
+ ######################
114
+ ## Instance Methods ##
115
+ ######################
116
+
117
+ ##
118
+ # Initialize an Argon2::Password instance using any valid Argon2 digest.
119
+ #
120
+ def initialize(digest)
121
+ digest = digest.to_s
122
+
123
+ raise Argon2::Errors::InvalidHash unless valid_hash?(digest)
124
+
125
+ # Split the digest into its component pieces
126
+ split_digest = split_hash(digest)
127
+ # Assign each piece to the Argon2::Password instance
128
+ @digest = digest
129
+ @variant = split_digest[:variant]
130
+ @version = split_digest[:version]
131
+ @t_cost = split_digest[:t_cost]
132
+ @m_cost = split_digest[:m_cost]
133
+ @p_cost = split_digest[:p_cost]
134
+ @salt = split_digest[:salt]
135
+ @checksum = split_digest[:checksum]
136
+ end
137
+
138
+ ##
139
+ # Helper function to allow easily comparing an Argon2::Password against the
140
+ # provided password and secret.
141
+ #
142
+ def matches?(password, secret = nil)
143
+ self.class.verify_password(password, digest, secret)
144
+ end
145
+
146
+ ##
147
+ # Compares two Argon2::Password instances to see if they come from the same
148
+ # digest/hash.
149
+ #
150
+ def ==(other)
151
+ # TODO: Should this return false instead of raising an error?
152
+ unless other.is_a?(Argon2::Password)
153
+ raise ArgumentError,
154
+ 'Can only compare an Argon2::Password against another Argon2::Password'
155
+ end
156
+
157
+ digest == other.digest
158
+ end
159
+
160
+ ##
161
+ # Converts an Argon2::Password instance into a String.
162
+ #
163
+ def to_s
164
+ digest.to_s
165
+ end
166
+
167
+ ##
168
+ # Converts an Argon2::Password instance into a String.
169
+ #
170
+ def to_str
171
+ digest.to_str
172
+ end
173
+
174
+ private
175
+
176
+ ##
177
+ # Helper method to allow checking if a hash is valid in the initializer.
178
+ #
179
+ def valid_hash?(digest)
180
+ self.class.valid_hash?(digest)
181
+ end
182
+
183
+ # FIXME: Reduce complexity/AbcSize
184
+ # rubocop:disable Metrics/AbcSize
185
+
186
+ ##
187
+ # Helper method to extract the various values from a digest into attributes.
188
+ #
189
+ def split_hash(digest)
190
+ # TODO: Is there a better way to explode the digest into attributes?
191
+ _, variant, version, config, salt, checksum = digest.split('$')
192
+ # Regex magic to extract the values for each setting
193
+ version = /v=(\d+)/.match(version)
194
+ t_cost = /t=(\d+),/.match(config)
195
+ m_cost = /m=(\d+),/.match(config)
196
+ p_cost = /p=(\d+)/.match(config)
197
+
198
+ # Make sure none of the values are missing
199
+ raise Argon2::Errors::InvalidVersion if version.nil?
200
+ raise Argon2::Errors::InvalidTCost if t_cost.nil?
201
+ raise Argon2::Errors::InvalidMCost if m_cost.nil?
202
+ raise Argon2::Errors::InvalidPCost if p_cost.nil?
203
+
204
+ # Undo the 2^m_cost operation when encoding the hash to get the original
205
+ # m_cost input back.
206
+ m_cost = Math.log2(m_cost[1].to_i).to_i
207
+
208
+ {
209
+ variant: variant.to_str,
210
+ version: version[1].to_i,
211
+ t_cost: t_cost[1].to_i,
212
+ m_cost: m_cost,
213
+ p_cost: p_cost[1].to_i,
214
+ salt: salt.to_str,
215
+ checksum: checksum.to_str
216
+ }
217
+ end
218
+ # rubocop:enable Metrics/AbcSize
219
+ end
220
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Argon2
4
+ ##
5
+ # Standard Gem version constant.
6
+ #
7
+ VERSION = '1.0.0'
8
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'argon2/version'
6
+
7
+ version = Argon2::VERSION
8
+ repo_url = 'https://github.com/sorcery/argon2'
9
+
10
+ Gem::Specification.new do |s|
11
+ s.version = version
12
+ s.platform = Gem::Platform::RUBY
13
+ s.name = 'sorcery-argon2'
14
+ s.summary = 'A Ruby wrapper for the Argon2 Password hashing algorithm'
15
+ s.description =
16
+ 'Provides a minimal ruby wrapper for the Argon2 password hashing algorithm.'
17
+
18
+ s.required_ruby_version = '>= 2.5.0'
19
+
20
+ s.license = 'MIT'
21
+ s.author = 'Josh Buker'
22
+ s.email = 'crypto@joshbuker.com'
23
+ s.homepage = repo_url
24
+ s.metadata = {
25
+ 'bug_tracker_uri' => "#{repo_url}/issues",
26
+ 'changelog_uri' => "#{repo_url}/releases/tag/v#{version}",
27
+ 'documentation_uri' => 'https://rubydoc.info/gems/sorcery-argon2',
28
+ 'source_code_uri' => "#{repo_url}/tree/v#{version}"
29
+ }
30
+
31
+ s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
32
+ s.files << `find ext`.split
33
+
34
+ s.bindir = 'exe'
35
+ s.executables = s.files.grep(%r{^exe/}) { |f| File.basename(f) }
36
+ s.require_paths = ['lib']
37
+
38
+ s.add_dependency 'ffi', '~> 1.14'
39
+ s.add_dependency 'ffi-compiler', '~> 1.0'
40
+
41
+ # Gems required for testing the wrapper locally.
42
+ s.add_development_dependency 'bundler', '~> 2.0'
43
+ s.add_development_dependency 'minitest', '~> 5.8'
44
+ s.add_development_dependency 'rake', '~> 13.0.1'
45
+ s.add_development_dependency 'rubocop', '~> 1.7'
46
+ s.add_development_dependency 'simplecov', '~> 0.20'
47
+ s.add_development_dependency 'simplecov-lcov', '~> 0.8'
48
+
49
+ # Argon2 C Extension
50
+ s.extensions << 'ext/argon2_wrap/extconf.rb'
51
+ end
metadata ADDED
@@ -0,0 +1,191 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sorcery-argon2
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Josh Buker
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-04-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ffi
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.14'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.14'
27
+ - !ruby/object:Gem::Dependency
28
+ name: ffi-compiler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '5.8'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '5.8'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 13.0.1
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 13.0.1
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.7'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.7'
97
+ - !ruby/object:Gem::Dependency
98
+ name: simplecov
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.20'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.20'
111
+ - !ruby/object:Gem::Dependency
112
+ name: simplecov-lcov
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.8'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.8'
125
+ description: Provides a minimal ruby wrapper for the Argon2 password hashing algorithm.
126
+ email: crypto@joshbuker.com
127
+ executables: []
128
+ extensions:
129
+ - ext/argon2_wrap/extconf.rb
130
+ extra_rdoc_files: []
131
+ files:
132
+ - ".document"
133
+ - ".github/ISSUE_TEMPLATE/bug_report.md"
134
+ - ".github/ISSUE_TEMPLATE/feature_request.md"
135
+ - ".github/ISSUE_TEMPLATE/need_help.md"
136
+ - ".github/PULL_REQUEST_TEMPLATE.md"
137
+ - ".github/workflows/ruby.yml"
138
+ - ".gitignore"
139
+ - ".gitmodules"
140
+ - ".rubocop.yml"
141
+ - CHANGELOG.md
142
+ - CODE_OF_CONDUCT.md
143
+ - Gemfile
144
+ - LICENSE.md
145
+ - MAINTAINING.md
146
+ - README.md
147
+ - Rakefile
148
+ - SECURITY.md
149
+ - bin/console
150
+ - bin/setup
151
+ - bin/test
152
+ - ext/argon2_wrap/Makefile
153
+ - ext/argon2_wrap/argon_wrap.c
154
+ - ext/argon2_wrap/extconf.rb
155
+ - ext/argon2_wrap/test.c
156
+ - lib/argon2.rb
157
+ - lib/argon2/constants.rb
158
+ - lib/argon2/engine.rb
159
+ - lib/argon2/errors.rb
160
+ - lib/argon2/ffi_engine.rb
161
+ - lib/argon2/password.rb
162
+ - lib/argon2/version.rb
163
+ - sorcery-argon2.gemspec
164
+ homepage: https://github.com/sorcery/argon2
165
+ licenses:
166
+ - MIT
167
+ metadata:
168
+ bug_tracker_uri: https://github.com/sorcery/argon2/issues
169
+ changelog_uri: https://github.com/sorcery/argon2/releases/tag/v1.0.0
170
+ documentation_uri: https://rubydoc.info/gems/sorcery-argon2
171
+ source_code_uri: https://github.com/sorcery/argon2/tree/v1.0.0
172
+ post_install_message:
173
+ rdoc_options: []
174
+ require_paths:
175
+ - lib
176
+ required_ruby_version: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: 2.5.0
181
+ required_rubygems_version: !ruby/object:Gem::Requirement
182
+ requirements:
183
+ - - ">="
184
+ - !ruby/object:Gem::Version
185
+ version: '0'
186
+ requirements: []
187
+ rubygems_version: 3.1.2
188
+ signing_key:
189
+ specification_version: 4
190
+ summary: A Ruby wrapper for the Argon2 Password hashing algorithm
191
+ test_files: []