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.
- checksums.yaml +7 -0
- data/.gitignore +32 -0
- data/.rubocop.yml +96 -0
- data/.yardopts +5 -0
- data/Gemfile +41 -0
- data/LICENSE.txt +19 -0
- data/README.md +114 -0
- data/Rakefile +46 -0
- data/bin/console +29 -0
- data/bin/setup +24 -0
- data/crypt_checkpass.gemspec +55 -0
- data/lib/crypt_checkpass.rb +75 -0
- data/lib/crypt_checkpass/api.rb +212 -0
- data/lib/crypt_checkpass/argon2.rb +204 -0
- data/lib/crypt_checkpass/bcrypt.rb +217 -0
- data/lib/crypt_checkpass/pbkdf2.rb +177 -0
- data/lib/crypt_checkpass/phc_string_format.rb +180 -0
- data/lib/crypt_checkpass/scrypt.rb +214 -0
- data/lib/crypt_checkpass/sha2.rb +162 -0
- metadata +201 -0
@@ -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
|