crypt_checkpass 1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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