sorcery-argon2 1.0.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.
@@ -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: []