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