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,217 @@
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
+ require 'consttime_memequal'
27
+
28
+ # BCrypt is a blowfish based password hash function. BSD devs and users tend
29
+ # to love this function. As of writing this is the only hash function that
30
+ # OpenBSD's crypt(3) understands. Also, because `ActiveModel::SecurePassword`
31
+ # is backended by this algorithm, Ruby on Rails users tends to use it.
32
+ #
33
+ # ### Newhash:
34
+ #
35
+ # In addition to the OpenBSD-ish usage described in {file:README.md}, you can
36
+ # also use `crypto_newhash` to create a new password hash using bcrypt:
37
+ #
38
+ # ```ruby
39
+ # crypt_newhash(password, id: 'bcrypt', rounds: 4, ident: '2b')
40
+ # ```
41
+ #
42
+ # where:
43
+ #
44
+ # - `password` is the raw binary password that you want to digest.
45
+ #
46
+ # - `id` is "bcrypt" when you want a bcrypt hash.
47
+ #
48
+ # - `rounds` is an integer ranging 4 to 31 inclusive, which is the number of
49
+ # iterations.
50
+ #
51
+ # - `ident` is the name of the variant. Variants of bcrypt are described
52
+ # below. Note however that what we don't support old variants that are
53
+ # known to be problematic. This parameter changes the name of the output
54
+ # but not the contents.
55
+ #
56
+ # The generated password hash has following format.
57
+ #
58
+ # ### Format:
59
+ #
60
+ # A bcrypt hashed string has following structure:
61
+ #
62
+ # ```ruby
63
+ # %r{
64
+ # (?<id> [2] [abxy]? ){0}
65
+ # (?<cost> [0-9]{2} ){0}
66
+ # (?<salt> [A-Za-z0-9./]{22} ){0}
67
+ # (?<csum> [A-Za-z0-9./]{31} ){0}
68
+ #
69
+ # \A [$] \g<id>
70
+ # [$] \g<cost>
71
+ # [$] \g<salt>
72
+ # \g<csum>
73
+ # \z
74
+ # }x
75
+ # ```
76
+ #
77
+ # - `id` is 2-something that denotes the variant of the hash. See below.
78
+ #
79
+ # - `cost` is a zero-padded decimal integer that specifies number of
80
+ # iterations in logs,
81
+ #
82
+ # - `salt` and `csum` are the salt and checksum strings. Both are encoded in
83
+ # base64-like strings that do not strictly follow RFC4648. There is no
84
+ # separating `$` sign is between them so you have to count the characters
85
+ # to tell which is which. Also, because they are base64, there are
86
+ # "unused" bits at the end of each.
87
+ #
88
+ # ### Variants:
89
+ #
90
+ # According to Wikipedia entry, there are 5 variants of bcrypt output:
91
+ #
92
+ # - Variant `$2$`: This was the initial version. It did not take Unicodes
93
+ # into account. Not currently active.
94
+ #
95
+ # - Variant `$2a$`: Unicode problem fixed, but suffered wraparound bug.
96
+ # OpenBSD people decided to abandon this to move to `$2b$`. Also suffered
97
+ # CVE-2011-2483. The people behind that CVE requested sysadmins to replace
98
+ # their `$2a$` with `$2x`, indicating the data is broken. Not currently
99
+ # active.
100
+ #
101
+ # - Variant `$2b$`: updated algorithm to fix wraparound bug. Now active.
102
+ #
103
+ # - Variant `$2x$`: see above. No new password hash shall generate this one.
104
+ #
105
+ # - Variant `$2y$`: updated algorithm to fix CVE-2011-2483. Now active.
106
+ #
107
+ # ### Fun facts:
108
+ #
109
+ # - It is by spec that the algorithm ignores password longer than 72 octets.
110
+ #
111
+ # - According to Python Passlib, variant `$2b$` and `$2y$` are "identical in
112
+ # all but name."
113
+ #
114
+ # - Rails (bcrypt-ruby) reportedly uses `$2a$` even today. However they seem
115
+ # fixed known flaws by themselves, without changing names. So their
116
+ # algorithm is arguably safe. Maybe this can be seen as a synonym of
117
+ # `$2b$` / `$2y`.
118
+ #
119
+ # @see https://en.wikipedia.org/wiki/Bcrypt
120
+ # @see https://www.usenix.org/legacy/event/usenix99/provos/provos_html/
121
+ # @example
122
+ # crypt_newhash 'password', id: 'bcrypt'
123
+ # # => "$2b$10$JlxIYWbT2EUDNvIwrIYcxuKf8pzf58IV4xVWk9yPy5J/ni0LCmz7G"
124
+ # @example
125
+ # crypt_checkpass? 'password', '$2b$10$JlxIYWbT2EUDNvIwrIYcxuKf8pzf58IV4xVWk9yPy5J/ni0LCmz7G'
126
+ # # => true
127
+ class CryptCheckpass::Bcrypt < CryptCheckpass
128
+
129
+ # (see CryptCheckpass.understand?)
130
+ def self.understand? str
131
+ return match? str, %r{
132
+ (?<id> [2] [abxy]? ){0}
133
+ (?<cost> [0-9]{2} ){0}
134
+ (?<remain> [A-Za-z0-9./]{53} ){0}
135
+ \A [$] \g<id>
136
+ [$] \g<cost>
137
+ [$] \g<remain>
138
+ \z
139
+ }x
140
+ end
141
+
142
+ # (see CryptCheckpass.checkpass?)
143
+ def self.checkpass? pass, hash
144
+ require 'bcrypt'
145
+
146
+ # bcrypt gem accepts `$2a$` and `$2x` only. We have to tweak.
147
+ expected = hash.sub %r/\A\$2[by]\$/, "$2a$"
148
+ obj = BCrypt::Password.new expected
149
+ actual = BCrypt::Engine.hash_secret pass, obj.salt
150
+ return consttime_memequal? expected, actual
151
+ end
152
+
153
+ # (see CryptCheckpass.provide?)
154
+ def self.provide? id
155
+ return id == 'bcrypt'
156
+ end
157
+
158
+ # (see CryptCheckpass.newhash)
159
+ #
160
+ # @param pass [String] raw binary password string.
161
+ # @param id [String] name of the algorithm (ignored)
162
+ # @param rounds [Integer] 4 to 31, inclusive.
163
+ # @param ident [String] "2b" or "2y" or something like that.
164
+ def self.newhash pass, id: 'bcrypt', rounds: nil, ident: '2b'
165
+ require 'bcrypt'
166
+ len = pass.bytesize
167
+ raise ArgumentError, <<-"end", len if len > 72
168
+ password is %d bytes, which is too long (up to 72)
169
+ end
170
+
171
+ rounds ||= BCrypt::Engine::DEFAULT_COST
172
+ case rounds when 4..31 then
173
+ return __generate pass, rounds, ident
174
+ else
175
+ raise ArgumentError, <<-"end", rounds
176
+ integer %d out of range of (4..31)
177
+ end
178
+ end
179
+ end
180
+
181
+ # This is to implement OpenBSD-style `crypt_newhash()` function.
182
+ #
183
+ # @param pass [String] bare, unhashed binary password.
184
+ # @param pref [String] algorithm preference specifier.
185
+ # @return [String] hashed digest string of password.
186
+ # @raise [NotImplementedError] pref not understandable.
187
+ # @see https://github.com/libressl-portable/openbsd/blob/master/src/lib/libc/crypt/cryptutil.c
188
+ def self.new_with_openbsd_pref pass, pref
189
+ require 'bcrypt'
190
+
191
+ func, rounds = pref.split ',', 2
192
+ unless match? func, /\A(bcrypt|blowfish)\z/ then
193
+ raise NotImplementedError, <<-"end".strip, func
194
+ hash algorithm %p not supported right now.
195
+ end
196
+ end
197
+
198
+ cost = nil
199
+ case rounds
200
+ when NilClass then cost = BCrypt::Engine::DEFAULT_COST
201
+ when "a" then cost = BCrypt::Engine::DEFAULT_COST
202
+ when /\A([12][0-9]|3[01]|[4-9])\z/ then cost = rounds.to_i
203
+ else
204
+ raise NotImplementedError, <<-"end".strip, rounds
205
+ cost function %p not supported right now.
206
+ end
207
+ end
208
+ return __generate pass, cost, '2b'
209
+ end
210
+
211
+ def self.__generate pass, cost, ident
212
+ salt = BCrypt::Engine.generate_salt(cost)
213
+ ret = BCrypt::Engine.hash_secret pass, salt
214
+ return ret.sub %r/\A\$2.?\$/, "$#{ident}$"
215
+ end
216
+ private_class_method :__generate
217
+ end
@@ -0,0 +1,177 @@
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
+ require_relative 'phc_string_format'
27
+
28
+ # PBKDF2 is the most beloved algorithm by security professionals.
29
+ #
30
+ # ### Newhash:
31
+ #
32
+ # You can use `crypto_newhash` to create a new password hash using PBKDF2:
33
+ #
34
+ # ```ruby
35
+ # crypt_newhash(password, id: 'pbkdf2-sha1', rounds: 1024)
36
+ # ```
37
+ #
38
+ # where:
39
+ #
40
+ # - `password` is the raw binary password that you want to digest.
41
+ #
42
+ # - `id` is "pbkdf2-{digest}". You can specify sha1 / sha256 / sha512.
43
+ # Unlike plain SHA1, PBKDF2 + SHA1 combination still has no known weakness
44
+ # as of writing so specifying pbkdf-sha1 should just suffice normally.
45
+ #
46
+ # - `rounds` is for iteration rounds.
47
+ #
48
+ # The generated password hash has following format.
49
+ #
50
+ # ### Format:
51
+ #
52
+ # This algorithm does not have a standard hash format. Here we follow npm's
53
+ # @phc/pbkdf2.
54
+ #
55
+ # ```ruby
56
+ # %r{
57
+ # (?<id> pbkdf2-[\w\d]+ ){0}
58
+ # (?<i> i=[1-9][0-9]* ){0}
59
+ # (?<salt> [a-zA-Z0-9+/]* ){0}
60
+ # (?<csum> [a-zA-Z0-9+/]* ){0}
61
+ #
62
+ # \A [$] \g<id>
63
+ # [$] \g<i>
64
+ # [$] \g<salt>
65
+ # [$] \g<csum>
66
+ # \z
67
+ # }x
68
+ # ```
69
+ #
70
+ # - This is a strict PHC string format. See also
71
+ # {CryptCheckpass::PHCStringFormat}
72
+ #
73
+ # - The `id` can either be "pbkdf2-sha1", "pbkdf2-sha256", or "pbkdf2-sha512".
74
+ #
75
+ # - The only parameter `i` is the iteration (rounds) of the calculation.
76
+ #
77
+ # ### Other formats:
78
+ #
79
+ # Python Passlib generates something different, in the same `$pbkdf2-{digest}$`
80
+ # id. Passlib's and @phc/pbkdf2's are distinguishable because Passlib does not
81
+ # follow PHC String Format.
82
+ #
83
+ # @see https://en.wikipedia.org/wiki/PBKDF2
84
+ # @see https://tools.ietf.org/html/rfc2898
85
+ # @example
86
+ # crypt_newhash 'password', id: 'pbkdf2-sha1'
87
+ # # => "$pbkdf2-sha1$i=1024$a9b0ggwILmLgiAwV34bpzA$nJ+GYjlNDao8BJedGVc8UROXpcU"
88
+ # @example
89
+ # crypt_checkpass? 'password', '$pbkdf2-sha1$i=1024$a9b0ggwILmLgiAwV34bpzA$nJ+GYjlNDao8BJedGVc8UROXpcU'
90
+ # # => true
91
+ class CryptCheckpass::PBKDF2 < CryptCheckpass
92
+
93
+ # (see CryptCheckpass.understand?)
94
+ def self.understand? str
95
+ return match? str, %r{
96
+ (?<id> pbkdf2-sha(1|256|512) ){0}
97
+ (?<i> i=[1-9][0-9]* ){0}
98
+ (?<salt> [a-zA-Z0-9+/]* ){0}
99
+ (?<csum> [a-zA-Z0-9+/]* ){0}
100
+
101
+ \A [$] \g<id>
102
+ [$] \g<i>
103
+ [$] \g<salt>
104
+ [$] \g<csum>
105
+ \z
106
+ }x
107
+ end
108
+
109
+ # (see CryptCheckpass.checkpass?)
110
+ def self.checkpass? pass, hash
111
+ require 'consttime_memequal'
112
+
113
+ json = phcdecode hash
114
+ id = json[:id]
115
+ i = json[:params][:i]
116
+ salt = json[:salt]
117
+ expected = json[:csum]
118
+ dklen = expected.bytesize
119
+ actual = __derive_key id, i, salt, pass, dklen
120
+
121
+ return consttime_memequal? expected, actual
122
+ end
123
+
124
+ # (see CryptCheckpass.provide?)
125
+ def self.provide? id
126
+ case id when 'pbkdf2-sha1', 'pbkdf2-sha256', 'pbkdf2-sha512' then
127
+ return true
128
+ else
129
+ return false
130
+ end
131
+ end
132
+
133
+ # (see CryptCheckpass.newhash)
134
+ #
135
+ # @param pass [String] raw binary password string.
136
+ # @param id [String] name of the algorithm
137
+ # @param rounds [Integer] iteration rounds
138
+ def self.newhash pass, id: 'pbkdf2-sha1', rounds: 1024
139
+ salt = SecureRandom.random_bytes 16
140
+ csum = __derive_key id, rounds, salt, pass
141
+ return phcencode id, { i: rounds }, salt, csum
142
+ end
143
+ end
144
+
145
+ # helper routines
146
+ class << CryptCheckpass::PBKDF2
147
+ include CryptCheckpass::PHCStringFormat
148
+
149
+ private
150
+
151
+ if RUBY_VERSION >= '2.3.0'
152
+ def __load_openssl
153
+ require 'openssl'
154
+ end
155
+ else
156
+ def __load_openssl
157
+ Kernel.require 'openssl'
158
+ end
159
+ end
160
+
161
+ def __default_dklen_for digest
162
+ case digest
163
+ when 'pbkdf2-sha1' then return 20, 'sha1'
164
+ when 'pbkdf2-sha256' then return 32, 'sha256'
165
+ when 'pbkdf2-sha512' then return 64, 'sha512'
166
+ else raise "NOTREACHED: %s", id
167
+ end
168
+ end
169
+
170
+ def __derive_key id, iter, salt, pass, dklen = nil
171
+ __load_openssl
172
+
173
+ n, d = __default_dklen_for id
174
+ dklen ||= n
175
+ return OpenSSL::PKCS5.pbkdf2_hmac pass, salt, iter, dklen, d
176
+ end
177
+ end
@@ -0,0 +1,180 @@
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
+ # Helper module to handle PHC String Format-compatible strings
27
+ #
28
+ # @note Argon2, which is the winner of PHC, ignores this format and go wild.
29
+ # It is highly skeptical that any other hash authors would switch to
30
+ # PHC's recommendation.
31
+ #
32
+ # ### Format
33
+ #
34
+ # This is how we understand the PHC String Format:
35
+ #
36
+ # ```ruby
37
+ # %r{
38
+ # (?<name> [a-z0-9-]{,32} ){0}
39
+ # (?<decimal> 0|-?[1-9][0-9]* ){0}
40
+ # (?<b64> [a-zA-Z0-9/+.-]* ){0}
41
+ #
42
+ # (?<id> \g<name> ){0}
43
+ # (?<param> \g<name> ){0}
44
+ # (?<value> \g<decimal> | \g<b64> ){0}
45
+ # (?<salt> \g<b64> ){0}
46
+ # (?<csum> \g<b64> ){0}
47
+ # (?<pair> \g<param> = \g<value> ){0}
48
+ # (?<pairs> \g<pair> (?:[,] \g<pair> )* ){0}
49
+ #
50
+ # \A [$] \g<id>
51
+ # [$] \g<pairs>
52
+ # [$] \g<salt>
53
+ # [$] \g<csum>
54
+ # \z
55
+ # }x
56
+ # ```
57
+ #
58
+ # - `id` is the name of the algorithm.
59
+ #
60
+ # - `pairs` is a set of key-value pair, that are parameters to the
61
+ # algorithm. Keys should be human-readable, while values need not be.
62
+ #
63
+ # - `salt` and `csum` are the salt and checksum strings. Both are encoded in
64
+ # what the spec says the "B64" encoding, which is a very slightly modified
65
+ # version of RFC4648 (no trailing ==... padding). They both can be
66
+ # arbitrary length.
67
+ #
68
+ # @see https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md
69
+ module CryptCheckpass::PHCStringFormat
70
+ private
71
+
72
+ # B64 encoder
73
+ # @param str [String] arbitrary binary string.
74
+ # @return [String] str, encoded in B64.
75
+ def b64encode str
76
+ var = [ str ].pack 'm0'
77
+ var.delete! '='
78
+ return var
79
+ end
80
+
81
+ if RUBY_VERSION >= '2.4.0' then
82
+ def malloc n
83
+ String.new capacity: n
84
+ end
85
+ else
86
+ def malloc n
87
+ String.new
88
+ end
89
+ end
90
+
91
+ # B64 decoder
92
+ # @param str [String] str, encoded in B64.
93
+ # @return [String] decoded binary string
94
+ # @raise [ArgumentError] str not in B64 encoding.
95
+ def b64decode str
96
+ return nil if str.nil?
97
+ n, m = str.length.divmod 4
98
+ raise ArgumentError, <<-"end".strip, str.length if m == 1
99
+ malformed string of %d octets passed.
100
+ end
101
+ buf = malloc(n * 4)
102
+ buf << str
103
+ buf << ('=' * ((4 - m) % 4))
104
+ return buf.unpack('m0').first
105
+ end
106
+
107
+ # Form a PHC String Formatted string.
108
+ # @return [String] a PHC String Formatted string.
109
+ # @param id [String] name of hash algorithm.
110
+ # @param params [<<String, Integer>>] hash algorithm parameters.
111
+ # @param salt [String] raw binary salt.
112
+ # @param csum [String] raw binary checksum
113
+ # @note The spec says:
114
+ #
115
+ # > The function MUST specify the order in which parameters may
116
+ # > appear. Producers MUST NOT allow parameters to appear in any
117
+ # > other order.
118
+ #
119
+ # Ensuring that property is up to the caller of this method.
120
+ def phcencode id, params, salt, csum
121
+ return [
122
+ '',
123
+ id,
124
+ params.map {|a| a.join '=' }.join(','),
125
+ b64encode(salt),
126
+ b64encode(csum)
127
+ ].join('$')
128
+ end
129
+
130
+ # Decompose a PHC String Formatted string.
131
+ # @param str [String] str, encoded in PHC's format.
132
+ # @return [Hash] decoded JSON.
133
+ def phcdecode str
134
+ grammar = %r{
135
+ (?<name> [a-z0-9-]{,32} ){0}
136
+ (?<decimal> 0|-?[1-9][0-9]* ){0}
137
+ (?<b64> [a-zA-Z0-9/+.-]* ){0}
138
+
139
+ (?<id> \g<name> ){0}
140
+ (?<param> \g<name> ){0}
141
+ (?<value> \g<decimal> | \g<b64> ){0}
142
+ (?<salt> \g<b64> ){0}
143
+ (?<csum> \g<b64> ){0}
144
+ (?<pair> \g<param> = \g<value> ){0}
145
+ (?<pairs> \g<pair> (?:[,] \g<pair> )* ){0}
146
+ }x
147
+
148
+ md = %r{
149
+ #{grammar}
150
+
151
+ \A [$] \g<id>
152
+ [$] \g<pairs>
153
+ [$] \g<salt>
154
+ [$] \g<csum>
155
+ \z
156
+ }xo.match str
157
+ raise "not in PHC String Format: %p", str unless md
158
+
159
+ return {
160
+ id: md['id'],
161
+ params: md['pairs'] \
162
+ . split(',', -1) \
163
+ . map {|i|
164
+ m = /#{grammar}\A\g<pair>\z/o.match i
165
+ next [
166
+ m['param'],
167
+ m['decimal'] ? m['decimal'].to_i : m['b64']
168
+ ]
169
+ }.each_with_object({}) {|(k, v), h|
170
+ h.update(k.to_s.to_sym => v) { |_, w, _|
171
+ raise <<-"end".strip, md['params'], k, v, w
172
+ %p includes conflicting values for %p: %p versus %p
173
+ end
174
+ }
175
+ },
176
+ salt: b64decode(md['salt']),
177
+ csum: b64decode(md['csum']),
178
+ }
179
+ end
180
+ end