crypt_checkpass 1

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,75 @@
1
+ #! /your/favourite/path/to/ruby
2
+ # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*-
3
+ # -*- frozen_string_literal: true -*-
4
+ # -*- warn_indent: true -*-
5
+
6
+ # Copyright (c) 2018 Urabe, Shyouhei
7
+ #
8
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ # of this software and associated documentation files (the "Software"), to deal
10
+ # in the Software without restriction, including without limitation the rights
11
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ # copies of the Software, and to permit persons to whom the Software is
13
+ # furnished to do so, subject to the following conditions:
14
+ #
15
+ # The above copyright notice and this permission notice shall be
16
+ # included in all copies or substantial portions of the Software.
17
+ #
18
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ # SOFTWARE.
25
+
26
+ # Parses what the given _hash_ is, apply the same hasing against _pass_, then
27
+ # compares the hashed _pass_ and the given _hash_.
28
+ #
29
+ # @param pass [String] password string.
30
+ # @param hash [String] hashed string.
31
+ # @return [true] they are identical.
32
+ # @return [false] they are distinct.
33
+ # @raise [NotImplementedError] don't know how to parse _hash_.
34
+ def crypt_checkpass? pass, hash
35
+ return CryptCheckpass::crypt_checkpass? pass, hash
36
+ end
37
+
38
+ # Generates new password hashes. The provided password is randomly salted, then
39
+ # hashed using the parameter.
40
+ #
41
+ # @overload crypt_newhash(password, perf)
42
+ # The pref argument identifies the preferred hashing algorithm and
43
+ # parameters. Possible values are:
44
+ #
45
+ # - `"bcrypt,<rounds>"`
46
+ # - `"blowfish,<rounds>"`
47
+ #
48
+ # where "rounds" can be a number between 4 and 31, or "a" for default.
49
+ #
50
+ # @note This usage is for OpenBSD fans.
51
+ # @see https://man.openbsd.org/crypt_newhash.3 crypt_newhash(3)
52
+ # @param password [String] bare, unhashed binary password.
53
+ # @param pref [String] algorithm preference specifier.
54
+ # @raise [NotImplementedError] pref not understandable.
55
+ # @return [String] hashed digest string of password.
56
+ #
57
+ # @overload crypt_newhash(password, id:, **kwargs)
58
+ # At least `:id` argument must be provided this case, which is the name of
59
+ # key deliveration function (the ID that the PHC string format says).
60
+ #
61
+ # @param password [String] bare, unhashed binary password.
62
+ # @param id [String] name of the function.
63
+ # @param kwargs [Symbol=>String,Integer] passed to the KDF.
64
+ # @return [String] hashed digest string of password.
65
+ # @raise [NotImplementedError] unknown KDF is specified.
66
+ def crypt_newhash password, pref = nil, id: nil, **kwargs
67
+ return CryptCheckpass::crypt_newhash password, pref, id: id, **kwargs
68
+ end
69
+
70
+ require_relative 'crypt_checkpass/api'
71
+ require_relative 'crypt_checkpass/argon2'
72
+ require_relative 'crypt_checkpass/bcrypt'
73
+ require_relative 'crypt_checkpass/pbkdf2'
74
+ require_relative 'crypt_checkpass/scrypt'
75
+ require_relative 'crypt_checkpass/sha2'
@@ -0,0 +1,212 @@
1
+ #! /your/favourite/path/to/ruby
2
+ # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*-
3
+ # -*- frozen_string_literal: true -*-
4
+ # -*- warn_indent: true -*-
5
+
6
+ # Copyright (c) 2018 Urabe, Shyouhei
7
+ #
8
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ # of this software and associated documentation files (the "Software"), to deal
10
+ # in the Software without restriction, including without limitation the rights
11
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ # copies of the Software, and to permit persons to whom the Software is
13
+ # furnished to do so, subject to the following conditions:
14
+ #
15
+ # The above copyright notice and this permission notice shall be
16
+ # included in all copies or substantial portions of the Software.
17
+ #
18
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ # SOFTWARE.
25
+
26
+ # Mother of all KDF classes.
27
+ #
28
+ # Subclasses of this are expected to implement the following 4 class methods:
29
+ #
30
+ # - `subclass.provide?(id)`
31
+ # - `subclass.newhash(pass, id: id, ...)`
32
+ # - `subclass.understand?(hash)`
33
+ # - `subclass.checkpass?(pass, hash)`
34
+ #
35
+ # If a subclass's `provide?` returns `true` for an id, then that class is
36
+ # responsible for generating new hash of that id. Likewise if `understand?`
37
+ # returns `true` for a hash, that should be able to checkpass.
38
+ #
39
+ # Caveats:
40
+ #
41
+ # - You don't have to provide all of those methods. It is completely
42
+ # reasonable to have a hash that is unable to generate new one, but still
43
+ # able to check existing ones.
44
+ class CryptCheckpass
45
+ @kdfs = [] # see below
46
+ end
47
+
48
+ class << CryptCheckpass
49
+ public
50
+
51
+ # @!group API entry points
52
+
53
+ # (see ::#crypt_checkpass?)
54
+ def crypt_checkpass? pass, hash
55
+ kdf = find_kdf_by_string hash
56
+ return kdf.checkpass? pass, hash
57
+ end
58
+
59
+ # (see ::#crypt_newhash)
60
+ def crypt_newhash password, pref = nil, id: nil, **kwargs
61
+ raise ArgumentError, <<-"end".strip if pref && id
62
+ wrong number of arguments (given 2, expected 1)
63
+ end
64
+ raise ArgumentError, <<-"end".strip, kwargs.keys if pref &&! kwargs.empty?
65
+ unknown key: %p
66
+ end
67
+
68
+ if pref then
69
+ require_relative 'bcrypt'
70
+ return CryptCheckpass::Bcrypt.new_with_openbsd_pref password, pref
71
+ else
72
+ kdf = find_kdf_by_id id
73
+ return kdf.newhash password, id: id, **kwargs
74
+ end
75
+ end
76
+
77
+ # @!endgroup
78
+
79
+ # @!group Inteacts with subclasses
80
+
81
+ # Checks if the given ID can be handled by this class. A class is
82
+ # free to handle several IDs, like 'argon2i', 'argon2d', ...
83
+ #
84
+ # @param id [String] hash function ID.
85
+ # @return [true] it does.
86
+ # @return [false] it desn't.
87
+ def provide? id
88
+ return false # default false
89
+ end
90
+
91
+ # Checks if the given hash string can be handled by this class.
92
+ #
93
+ # @param str [String] a good hashed string.
94
+ # @return [true] it does.
95
+ # @return [false] it desn't.
96
+ def understand? str
97
+ return false # default false
98
+ end
99
+
100
+ # Checks if the given password matches the hash.
101
+ #
102
+ # @param pass [String] a password to test.
103
+ # @param hash [String] a good hash digest string.
104
+ # @return [true] they are identical.
105
+ # @return [false] they are distinct.
106
+ # @raise [NotImplementedError] don't know how to parse _hash_.
107
+ def checkpass? pass, hash
108
+ return false # default false
109
+ end
110
+
111
+ # Generate a new password hash string.
112
+ #
113
+ # @note There is no way to specify salt. That's a bad idea.
114
+ # @return [String] hashed digest string of password.
115
+ def newhash *;
116
+ raise 'NOTREACHED'
117
+ end
118
+
119
+ private
120
+
121
+ undef :new
122
+
123
+ # @!group @shyouhei's "angry extension to core" corner
124
+
125
+ # Utility raise + printf function. It is quite hard to think of exceptions
126
+ # that only concern fixed strings. @shyouhei really doesn't understand why
127
+ # this is not a canon.
128
+ #
129
+ # @overload raise(klass, fmt, *va_args)
130
+ # @param klass [Class] exception class.
131
+ # @param fmt [String] printf-format string.
132
+ # @param va_args [Array] anything.
133
+ # @raise [klass] always raises a klass instance.
134
+ #
135
+ # @overload raise(fmt, *va_args)
136
+ # @param fmt [String] printf-format string.
137
+ # @param va_args [Array] anything.
138
+ # @raise [RuntimeError] always raises a RuntimeError.
139
+ def raise class_or_string, *argv
140
+ case class_or_string
141
+ when Class, Exception then
142
+ klass = class_or_string
143
+ string = sprintf(*argv)
144
+ when String then
145
+ klass = RuntimeError
146
+ string = class_or_string % argv
147
+ else # recursion
148
+ raise TypeError, <<-"end".strip
149
+ wrong argument type %p (%p expected)
150
+ end
151
+ end
152
+ return super klass, string, caller
153
+ end
154
+
155
+ # Utility gem + require function. It is often the case a library is a gem
156
+ # and calling gem before require is desirable.@shyouhei really doesn't
157
+ # understand why this is not a canon.
158
+ #
159
+ # @return [void]
160
+ # @param gem [String] gem name.
161
+ # @param lib [String] library name.
162
+ # @raise [Gem::LoadError] gem not found.
163
+ # @raise [LoadError] lib not found.
164
+ def require gem, lib = gem
165
+ Kernel.gem gem
166
+ Kernel.require lib
167
+ end
168
+
169
+ if defined? %r/match?/.match? then
170
+ # Fallback routine counterpart.
171
+ # @param re [Regexp] the language to accept.
172
+ # @param str [String] target string to test.
173
+ # @return [true] accepted.
174
+ # @return [false] otherwise.
175
+ def match? re, str
176
+ return re.match? str
177
+ end
178
+ else
179
+ # Fallback routine for ruby versions without Regexp#match?
180
+ # @param re [Regexp] the language to accept.
181
+ # @param str [String] target string to test.
182
+ # @return [true] accepted.
183
+ # @return [false] otherwise.
184
+ def match? re, str
185
+ md = re.match str
186
+ return !!md
187
+ end
188
+ end
189
+
190
+ # @!endgroup
191
+
192
+ def inherited klass
193
+ super
194
+ @kdfs.push klass
195
+ end
196
+
197
+ def find_kdf_by_id id
198
+ kdf = @kdfs.find {|i| i.provide? id }
199
+ return kdf if kdf
200
+ raise ArgumentError, <<-"end".strip, id
201
+ don't know how to generate %s hash.
202
+ end
203
+ end
204
+
205
+ def find_kdf_by_string str
206
+ kdf = @kdfs.find {|i| i.understand? str }
207
+ return kdf if kdf
208
+ raise ArgumentError, <<-"end".strip, str
209
+ don't know how to parse %p, maybe clobbered?
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,204 @@
1
+ #! /your/favourite/path/to/ruby
2
+ # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*-
3
+ # -*- frozen_string_literal: true -*-
4
+ # -*- warn_indent: true -*-
5
+
6
+ # Copyright (c) 2018 Urabe, Shyouhei
7
+ #
8
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ # of this software and associated documentation files (the "Software"), to deal
10
+ # in the Software without restriction, including without limitation the rights
11
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ # copies of the Software, and to permit persons to whom the Software is
13
+ # furnished to do so, subject to the following conditions:
14
+ #
15
+ # The above copyright notice and this permission notice shall be
16
+ # included in all copies or substantial portions of the Software.
17
+ #
18
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ # SOFTWARE.
25
+
26
+ # Argon2 the Password Hashing Competition winner.
27
+ #
28
+ # ### Newhash:
29
+ #
30
+ # You can use `crypto_newhash` to create a new password hash using argon2:
31
+ #
32
+ # ```ruby
33
+ # crypt_newhash(password, id: 'argon2i', m_cost: 12, t_cost: 3)
34
+ # ```
35
+ #
36
+ # where:
37
+ #
38
+ # - `password` is the raw binary password that you want to digest.
39
+ #
40
+ # - `id` is "argon2i" when you want an argon2 hash. Due to underlying
41
+ # ruby-argons gem's restriction we do not support other argon2 variants.
42
+ #
43
+ # - `m_cost` and `t_cost` are both integer parameter to the algorighm.
44
+ #
45
+ # The generated password hash has following format.
46
+ #
47
+ # ### Format:
48
+ #
49
+ # Argon2 specifies its hash structure in detail named "PHC String Format",
50
+ # **then ignored the format by itself**. See also [1]. Empirical findings
51
+ # show that the algorithm now has the following output format:
52
+ #
53
+ # ```ruby
54
+ # %r{
55
+ # (?<digits> 0|[1-9]\d* ){0}
56
+ # (?<b64> [a-zA-Z0-9+/] ){0}
57
+ # (?<id> argon2 (i|d|id) ){0}
58
+ # (?<v> v=19 ){0}
59
+ # (?<m> m=\g<digits> ){0}
60
+ # (?<t> t=\g<digits> ){0}
61
+ # (?<p> p=\g<digits> ){0}
62
+ # (?<salt> \g<b64>+ ){0}
63
+ # (?<csum> \g<b64>+ ){0}
64
+ #
65
+ # \A [$] \g<id>
66
+ # (?: [$] \g<v> )?
67
+ # [$] \g<m>
68
+ # [,] \g<t>
69
+ # [,] \g<p>
70
+ # [$] \g<salt>
71
+ # [$] \g<csum>
72
+ # \z
73
+ # }x
74
+ # ```
75
+ #
76
+ # - `id` is "argon2" + something that denotes the variant of the
77
+ # hash. Variant "argon2i" seems most widely adopted.
78
+ #
79
+ # - `v` is, when available, a number 19. That doesn't mean anything. What
80
+ # is important is the _absense_ of that parameter, which means the hash was
81
+ # genrated using old argon2 1.0 and shall be out of date.
82
+ #
83
+ # - `m` is the amount of memory filled by the algorithm (2**m KiB). Memory
84
+ # consumption depends on this parameter.
85
+ #
86
+ # - `t` is the mumber of passes over the memory. The running time depends
87
+ # linearly on this parameter.
88
+ #
89
+ # - `p` is the degree of parallelism, called "lanes" in the C implementation.
90
+ #
91
+ # - `salt` and `csum` are the salt and checksum strings. Both are encoded in
92
+ # base64-like strings that do not strictly follow RFC4648. They both can
93
+ # be arbitrary length. In case there are "unused" bits at the end of those
94
+ # fields, they shall be zero-filled.
95
+ #
96
+ # [1]: https://github.com/P-H-C/phc-winner-argon2/issues/157
97
+ #
98
+ # ### Implementation limitations:
99
+ #
100
+ # Ruby binding of argon2 library (ruby-argon2) is pretty well designed and can
101
+ # be recommended for daily uses. You really should use it whenever possible.
102
+ # The big problem is however, that it only supports argon2i. That is
103
+ # definitely OK for hash generation. However in verifying, it is desirable to
104
+ # support other variants.
105
+ #
106
+ # In order to reroute this problem we load the ruby-argon2 gem, then ignore its
107
+ # ruby part and directly call the canonical C implementation via FFI.
108
+ #
109
+ # @see https://en.wikipedia.org/wiki/Argon2
110
+ # @see https://www.cryptolux.org/index.php/Argon2
111
+ # @example
112
+ # crypt_newhash 'password', id: 'argon2i'
113
+ # # => "$argon2i$v=19$m=4096,t=3,p=1$b9AqucWUJADOdNMW8fW+0A$s3+Yno9+X7rpA2AsaG7KnoBtjQiE+AUevLvT7u1lXeA"
114
+ # @example
115
+ # crypt_checkpass? 'password', '$argon2i$v=19$m=4096,t=3,p=1$b9AqucWUJADOdNMW8fW+0A$s3+Yno9+X7rpA2AsaG7KnoBtjQiE+AUevLvT7u1lXeA'
116
+ # # => true
117
+ class CryptCheckpass::Argon2 < CryptCheckpass
118
+
119
+ # (see CryptCheckpass.understand?)
120
+ def self.understand? str
121
+ return match? str, %r{
122
+ (?<id> argon2 (i|d|id) ){0}
123
+ (?<digits> 0|[1-9]\d* ){0}
124
+ (?<b64> [a-zA-Z0-9+/] ){0}
125
+ (?<v> v=19 ){0}
126
+ (?<m> m=\g<digits> ){0}
127
+ (?<t> t=\g<digits> ){0}
128
+ (?<p> p=\g<digits> ){0}
129
+ (?<salt> \g<b64>+ ){0}
130
+ (?<csum> \g<b64>+ ){0}
131
+
132
+ \A [$] \g<id>
133
+ (?: [$] \g<v> )?
134
+ [$] \g<m>
135
+ [,] \g<t>
136
+ [,] \g<p>
137
+ [$] \g<salt>
138
+ [$] \g<csum>
139
+ \z
140
+ }x
141
+ end
142
+
143
+ # (see CryptCheckpass.checkpass?)
144
+ def self.checkpass? pass, hash
145
+ h = hash
146
+ p = pass
147
+ n = pass.bytesize
148
+
149
+ __load_argon2_dll
150
+
151
+ case hash
152
+ when /\A\$argon2i\$/ then ret = @dll.argon2i_verify h, p, n
153
+ when /\A\$argon2d\$/ then ret = @dll.argon2d_verify h, p, n
154
+ when /\A\$argon2id\$/ then ret = @dll.argon2id_verify h, p, n
155
+ else raise ArgumentError, "unknown hash format %p", hash
156
+ end
157
+
158
+ case ret
159
+ when 0 then return true
160
+ when -35 then return false # ARGON2_VERIFY_MISMATCH
161
+ else
162
+ errstr = ::Argon2::ERRORS[ret.abs] || ret.to_s
163
+ raise ::Argon2::ArgonHashFail, "got %s", errstr
164
+ end
165
+ end
166
+
167
+ # (see CryptCheckpass.provide?)
168
+ # @note we don't support generating argon2d hashs.
169
+ def self.provide? id
170
+ return id == 'argon2i'
171
+ end
172
+
173
+ # (see CryptCheckpass.newhash)
174
+ #
175
+ # @param pass [String] raw binary password string.
176
+ # @param id [String] name of the algorithm (ignored)
177
+ # @param m_cost [Integer] argon2 memory usage (2^m KiB)
178
+ # @param t_cost [Integer] argon2 iterations.
179
+ def self.newhash pass, id: 'argon2i', m_cost: 12, t_cost: 3
180
+ require 'argon2'
181
+
182
+ argon2 = ::Argon2::Password.new m_cost: m_cost, t_cost: t_cost
183
+ return argon2.create pass
184
+ end
185
+
186
+ @m = Thread::Mutex.new
187
+
188
+ def self.__load_argon2_dll
189
+ @m.synchronize do
190
+ next if defined? @dll
191
+ require 'argon2'
192
+ @dll = Module.new do
193
+ extend FFI::Library
194
+ lib = FFI::Compiler::Loader.find 'argon2_wrap'
195
+ fun = %i[argon2i_verify argon2d_verify argon2id_verify]
196
+ ffi_lib lib
197
+ fun.each do |f|
198
+ attach_function f, %i[pointer pointer size_t], :int, blocking: true
199
+ end
200
+ end
201
+ end
202
+ end
203
+ private_class_method :__load_argon2_dll
204
+ end