Subversion Repositories php_utils

Rev

Rev 73 | Rev 75 | Go to most recent revision | Only display areas with differences | Regard whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 73 Rev 74
1
<?php
1
<?php
2
 
2
 
3
/*
3
/*
4
 * ViaThinkSoft Modular Crypt Format 1.0 and vts_password_*() functions
4
 * ViaThinkSoft Modular Crypt Format 1.0 and vts_password_*() functions
5
 * Copyright 2023 Daniel Marschall, ViaThinkSoft
5
 * Copyright 2023 Daniel Marschall, ViaThinkSoft
6
 * Revision 2023-03-03
6
 * Revision 2023-03-03
7
 *
7
 *
8
 * Licensed under the Apache License, Version 2.0 (the "License");
8
 * Licensed under the Apache License, Version 2.0 (the "License");
9
 * you may not use this file except in compliance with the License.
9
 * you may not use this file except in compliance with the License.
10
 * You may obtain a copy of the License at
10
 * You may obtain a copy of the License at
11
 *
11
 *
12
 *     http://www.apache.org/licenses/LICENSE-2.0
12
 *     http://www.apache.org/licenses/LICENSE-2.0
13
 *
13
 *
14
 * Unless required by applicable law or agreed to in writing, software
14
 * Unless required by applicable law or agreed to in writing, software
15
 * distributed under the License is distributed on an "AS IS" BASIS,
15
 * distributed under the License is distributed on an "AS IS" BASIS,
16
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
 * See the License for the specific language governing permissions and
17
 * See the License for the specific language governing permissions and
18
 * limitations under the License.
18
 * limitations under the License.
19
 */
19
 */
20
 
20
 
21
/*
21
/*
22
 
22
 
23
The function vts_password_hash() replaces password_hash()
23
The function vts_password_hash() replaces password_hash()
24
and adds the ViaThinkSoft Modular Crypt Format 1.0 hash as well as
24
and adds the ViaThinkSoft Modular Crypt Format 1.0 hash as well as
25
all hashes from password_hash() and crypt().
25
all hashes from password_hash() and crypt().
26
 
26
 
27
The function vts_password_verify() replaces password_verify().
27
The function vts_password_verify() replaces password_verify().
28
 
28
 
29
ViaThinkSoft Modular Crypt Format 1.0 performs a simple hash or HMAC operation.
29
ViaThinkSoft Modular Crypt Format 1.0 performs a simple hash or HMAC operation.
30
No key derivation function or iterations are performed.
30
No key derivation function or iterations are performed.
31
Format:
31
Format:
32
        $1.3.6.1.4.1.37476.3.0.1.1$a=<algo>,m=<mode>$<salt>$<hash>
32
        $1.3.6.1.4.1.37476.3.0.1.1$a=<algo>,m=<mode>[,i=<iterations>]$<salt>$<hash>
33
where <algo> is any valid hash algorithm (name scheme of PHP hash_algos() preferred), e.g.
33
where <algo> is any valid hash algorithm (name scheme of PHP hash_algos() preferred), e.g.
34
        sha3-512
34
        sha3-512
35
        sha3-384
35
        sha3-384
36
        sha3-256
36
        sha3-256
37
        sha3-224
37
        sha3-224
38
        sha512
38
        sha512
39
        sha512/256
39
        sha512/256
40
        sha512/224
40
        sha512/224
41
        sha384
41
        sha384
42
        sha256
42
        sha256
43
        sha224
43
        sha224
44
        sha1
44
        sha1
45
        md5
45
        md5
46
Not supported are these hashes (because they have a special salt-handling and output their own crypt format):
46
Not supported are these hashes (because they have a special salt-handling and output their own crypt format):
47
        bcrypt [Standardized crypt identifier 2, 2a, 2x, 2y]
47
        bcrypt [Standardized crypt identifier 2, 2a, 2x, 2y]
48
        argon2i [Crypt identifier argon2i, not standardized]
48
        argon2i [Crypt identifier argon2i, not standardized]
49
        argon2id [Crypt identifier argon2i, not standardized]
49
        argon2id [Crypt identifier argon2i, not standardized]
50
Valid <mode> :
50
Valid <mode> :
51
        sp = salt + password
51
        sp = salt + password
52
        ps = password + salt
52
        ps = password + salt
53
        sps = salt + password + salt
53
        sps = salt + password + salt
54
        hmac = HMAC (salt is the key)
54
        hmac = HMAC (salt is the key)
55
        pbkdf2 = PBKDF2-HMAC (Additional param i= contains the number of iterations)
55
        pbkdf2 = PBKDF2-HMAC (Additional param i= contains the number of iterations)
-
 
56
<iterations> can be omitted if 0. It is required for mode=pbkdf2. For sp/ps/sps/hmac, it is optional.
56
Like most Crypt-hashes, <salt> and <hash> are Radix64 coded
57
Like most Crypt-hashes, <salt> and <hash> are Radix64 coded
57
with alphabet './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' and no padding.
58
with alphabet './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' and no padding.
58
Link to the online specification:
59
Link to the online specification:
59
        https://oidplus.viathinksoft.com/oidplus/?goto=oid%3A1.3.6.1.4.1.37476.3.0.1.1
60
        https://oidplus.viathinksoft.com/oidplus/?goto=oid%3A1.3.6.1.4.1.37476.3.0.1.1
60
Reference implementation in PHP:
61
Reference implementation in PHP:
61
        https://github.com/danielmarschall/php_utils/blob/master/vts_crypt.inc.php
62
        https://github.com/danielmarschall/php_utils/blob/master/vts_crypt.inc.php
62
 
63
 
63
*/
64
*/
64
 
65
 
65
require_once __DIR__ . '/misc_functions.inc.php';
66
require_once __DIR__ . '/misc_functions.inc.php';
66
 
67
 
67
define('OID_MCF_VTS_V1',     '1.3.6.1.4.1.37476.3.0.1.1'); // { iso(1) identified-organization(3) dod(6) internet(1) private(4) enterprise(1) 37476 specifications(3) misc(0) modular-crypt-format(1) vts-crypt-v1(1) }
68
define('OID_MCF_VTS_V1',     '1.3.6.1.4.1.37476.3.0.1.1'); // { iso(1) identified-organization(3) dod(6) internet(1) private(4) enterprise(1) 37476 specifications(3) misc(0) modular-crypt-format(1) vts-crypt-v1(1) }
68
 
69
 
69
// Valid algorithms for vts_password_hash():
70
// Valid algorithms for vts_password_hash():
70
define('PASSWORD_STD_DES',   'std-des');       // Algorithm from crypt()
71
define('PASSWORD_STD_DES',   'std-des');       // Algorithm from crypt()
71
define('PASSWORD_EXT_DES',   'ext-des');       // Algorithm from crypt()
72
define('PASSWORD_EXT_DES',   'ext-des');       // Algorithm from crypt()
72
define('PASSWORD_MD5',       'md5');           // Algorithm from crypt()
73
define('PASSWORD_MD5',       'md5');           // Algorithm from crypt()
73
define('PASSWORD_BLOWFISH',  'blowfish');      // Algorithm from crypt()
74
define('PASSWORD_BLOWFISH',  'blowfish');      // Algorithm from crypt()
74
define('PASSWORD_SHA256',    'sha256');        // Algorithm from crypt()
75
define('PASSWORD_SHA256',    'sha256');        // Algorithm from crypt()
75
define('PASSWORD_SHA512',    'sha512');        // Algorithm from crypt()
76
define('PASSWORD_SHA512',    'sha512');        // Algorithm from crypt()
76
define('PASSWORD_VTS_MCF1',  OID_MCF_VTS_V1);  // Algorithm by ViaThinkSoft
77
define('PASSWORD_VTS_MCF1',  OID_MCF_VTS_V1);  // Algorithm by ViaThinkSoft
77
// Other valid values (already defined in PHP):
78
// Other valid values (already defined in PHP):
78
// - PASSWORD_DEFAULT
79
// - PASSWORD_DEFAULT
79
// - PASSWORD_BCRYPT
80
// - PASSWORD_BCRYPT
80
// - PASSWORD_ARGON2I
81
// - PASSWORD_ARGON2I
81
// - PASSWORD_ARGON2ID
82
// - PASSWORD_ARGON2ID
82
 
83
 
83
define('PASSWORD_VTS_MCF1_MODE_SP',             'sp');     // Salt+Password
84
define('PASSWORD_VTS_MCF1_MODE_SP',             'sp');     // Salt+Password
84
define('PASSWORD_VTS_MCF1_MODE_PS',             'ps');     // Password+Salt
85
define('PASSWORD_VTS_MCF1_MODE_PS',             'ps');     // Password+Salt
85
define('PASSWORD_VTS_MCF1_MODE_SPS',            'sps');    // Salt+Password+Salt
86
define('PASSWORD_VTS_MCF1_MODE_SPS',            'sps');    // Salt+Password+Salt
86
define('PASSWORD_VTS_MCF1_MODE_HMAC',           'hmac');   // HMAC
87
define('PASSWORD_VTS_MCF1_MODE_HMAC',           'hmac');   // HMAC
87
define('PASSWORD_VTS_MCF1_MODE_PBKDF2',         'pbkdf2'); // PBKDF2-HMAC
88
define('PASSWORD_VTS_MCF1_MODE_PBKDF2',         'pbkdf2'); // PBKDF2-HMAC
88
 
89
 
89
define('PASSWORD_EXT_DES_DEFAULT_ITERATIONS',   725);
90
define('PASSWORD_EXT_DES_DEFAULT_ITERATIONS',   725);
90
define('PASSWORD_BLOWFISH_DEFAULT_COST',        10);
91
define('PASSWORD_BLOWFISH_DEFAULT_COST',        10);
91
define('PASSWORD_SHA256_DEFAULT_ROUNDS',        5000);
92
define('PASSWORD_SHA256_DEFAULT_ROUNDS',        5000);
92
define('PASSWORD_SHA512_DEFAULT_ROUNDS',        5000);
93
define('PASSWORD_SHA512_DEFAULT_ROUNDS',        5000);
93
define('PASSWORD_VTS_MCF1_DEFAULT_ALGO',        'sha3-512'); // any value in hash_algos(), NOT vts_hash_algos()
94
define('PASSWORD_VTS_MCF1_DEFAULT_ALGO',        'sha3-512'); // any value in hash_algos(), NOT vts_hash_algos()
94
define('PASSWORD_VTS_MCF1_DEFAULT_MODE',        PASSWORD_VTS_MCF1_MODE_PS);
95
define('PASSWORD_VTS_MCF1_DEFAULT_MODE',        PASSWORD_VTS_MCF1_MODE_PS);
95
define('PASSWORD_VTS_MCF1_DEFAULT_ITERATIONS',  0); // only for mode=pbkdf2. 0=Default, depending on algo
96
define('PASSWORD_VTS_MCF1_DEFAULT_ITERATIONS',  0); // For PBKDF2, iterations=0 means: Default, depending on the algo
96
 
97
 
97
// --- Part 1: Modular Crypt Format encode/decode
98
// --- Part 1: Modular Crypt Format encode/decode
98
 
99
 
99
function crypt_modular_format_encode($id, $bin_salt, $bin_hash, $params=null) {
100
function crypt_modular_format_encode($id, $bin_salt, $bin_hash, $params=null) {
100
        // $<id>[$<param>=<value>(,<param>=<value>)*][$<salt>[$<hash>]]
101
        // $<id>[$<param>=<value>(,<param>=<value>)*][$<salt>[$<hash>]]
101
        $out = '$'.$id;
102
        $out = '$'.$id;
102
        if (!is_null($params)) {
103
        if (!is_null($params)) {
103
                $ary_params = array();
104
                $ary_params = array();
104
                foreach ($params as $name => $value) {
105
                foreach ($params as $name => $value) {
105
                        $ary_params[] = "$name=$value";
106
                        $ary_params[] = "$name=$value";
106
                }
107
                }
107
                $out .= '$'.implode(',',$ary_params);
108
                $out .= '$'.implode(',',$ary_params);
108
        }
109
        }
109
        $out .= '$'.crypt_radix64_encode($bin_salt);
110
        $out .= '$'.crypt_radix64_encode($bin_salt);
110
        $out .= '$'.crypt_radix64_encode($bin_hash);
111
        $out .= '$'.crypt_radix64_encode($bin_hash);
111
        return $out;
112
        return $out;
112
}
113
}
113
 
114
 
114
function crypt_modular_format_decode($mcf) {
115
function crypt_modular_format_decode($mcf) {
115
        $ary = explode('$', $mcf);
116
        $ary = explode('$', $mcf);
116
 
117
 
117
        $dummy = array_shift($ary);
118
        $dummy = array_shift($ary);
118
        if ($dummy !== '') return false;
119
        if ($dummy !== '') return false;
119
 
120
 
120
        $dummy = array_shift($ary);
121
        $dummy = array_shift($ary);
121
        $id = $dummy;
122
        $id = $dummy;
122
 
123
 
123
        $params = array();
124
        $params = array();
124
        $dummy = array_shift($ary);
125
        $dummy = array_shift($ary);
125
        if (strpos($dummy, '=') !== false) {
126
        if (strpos($dummy, '=') !== false) {
126
                $params_ary = explode(',',$dummy);
127
                $params_ary = explode(',',$dummy);
127
                foreach ($params_ary as $param) {
128
                foreach ($params_ary as $param) {
128
                        $bry = explode('=', $param, 2);
129
                        $bry = explode('=', $param, 2);
129
                        if (count($bry) > 1) {
130
                        if (count($bry) > 1) {
130
                                $params[$bry[0]] = $bry[1];
131
                                $params[$bry[0]] = $bry[1];
131
                        }
132
                        }
132
                }
133
                }
133
        } else {
134
        } else {
134
                array_unshift($ary, $dummy);
135
                array_unshift($ary, $dummy);
135
        }
136
        }
136
 
137
 
137
        if (count($ary) > 1) {
138
        if (count($ary) > 1) {
138
                $dummy = array_shift($ary);
139
                $dummy = array_shift($ary);
139
                $bin_salt = crypt_radix64_decode($dummy);
140
                $bin_salt = crypt_radix64_decode($dummy);
140
        } else {
141
        } else {
141
                $bin_salt = '';
142
                $bin_salt = '';
142
        }
143
        }
143
 
144
 
144
        $dummy = array_shift($ary);
145
        $dummy = array_shift($ary);
145
        $bin_hash = crypt_radix64_decode($dummy);
146
        $bin_hash = crypt_radix64_decode($dummy);
146
 
147
 
147
        return array('id'     => $id,
148
        return array('id'     => $id,
148
                     'salt'   => $bin_salt,
149
                     'salt'   => $bin_salt,
149
                     'hash'   => $bin_hash,
150
                     'hash'   => $bin_hash,
150
                     'params' => $params);
151
                     'params' => $params);
151
}
152
}
152
 
153
 
153
// --- Part 2: ViaThinkSoft Modular Crypt Format 1.0
154
// --- Part 2: ViaThinkSoft Modular Crypt Format 1.0
154
 
155
 
155
function vts_crypt_version($hash) {
156
function vts_crypt_version($hash) {
156
        if (str_starts_with($hash, '$'.OID_MCF_VTS_V1.'$')) {
157
        if (str_starts_with($hash, '$'.OID_MCF_VTS_V1.'$')) {
157
                return '1';
158
                return '1';
158
        } else {
159
        } else {
159
                return '0';
160
                return '0';
160
        }
161
        }
161
}
162
}
162
 
163
 
163
function vts_crypt_hash($algo, $str_password, $str_salt, $ver='1', $mode=PASSWORD_VTS_MCF1_DEFAULT_MODE, $iterations=PASSWORD_VTS_MCF1_DEFAULT_ITERATIONS) {
164
function vts_crypt_hash($algo, $str_password, $str_salt, $ver='1', $mode=PASSWORD_VTS_MCF1_DEFAULT_MODE, $iterations=PASSWORD_VTS_MCF1_DEFAULT_ITERATIONS) {
164
        if ($ver == '1') {
165
        if ($ver == '1') {
165
                if ($mode == PASSWORD_VTS_MCF1_MODE_SP) {
166
                if ($mode == PASSWORD_VTS_MCF1_MODE_SP) {
166
                        $payload = $str_salt.$str_password;
167
                        $bin_hash = hash_ex($algo, $str_salt.$str_password, true);
-
 
168
                        for ($i=0; $i<$iterations; $i++) {
167
                        $bin_hash = hash_ex($algo, $payload, true);
169
                                $bin_hash = hash_ex($algo, $str_salt.$bin_hash.$i, true);
-
 
170
                        }
168
                } else if ($mode == PASSWORD_VTS_MCF1_MODE_PS) {
171
                } else if ($mode == PASSWORD_VTS_MCF1_MODE_PS) {
169
                        $payload = $str_password.$str_salt;
172
                        $bin_hash = hash_ex($algo, $str_password.$str_salt, true);
-
 
173
                        for ($i=0; $i<$iterations; $i++) {
170
                        $bin_hash = hash_ex($algo, $payload, true);
174
                                $bin_hash = hash_ex($algo, $bin_hash.$i.$str_salt, true);
-
 
175
                        }
171
                } else if ($mode == PASSWORD_VTS_MCF1_MODE_SPS) {
176
                } else if ($mode == PASSWORD_VTS_MCF1_MODE_SPS) {
172
                        $payload = $str_salt.$str_password.$str_salt;
177
                        $bin_hash = hash_ex($algo, $str_salt.$str_password.$str_salt, true);
-
 
178
                        for ($i=0; $i<$iterations; $i++) {
173
                        $bin_hash = hash_ex($algo, $payload, true);
179
                                $bin_hash = hash_ex($algo, $str_salt.$bin_hash.$i.$str_salt, true);
-
 
180
                        }
174
                } else if ($mode == PASSWORD_VTS_MCF1_MODE_HMAC) {
181
                } else if ($mode == PASSWORD_VTS_MCF1_MODE_HMAC) {
175
                        $bin_hash = hash_hmac_ex($algo, $str_password, $str_salt, true);
182
                        $bin_hash = hash_hmac_ex($algo, $str_password, $str_salt, true);
-
 
183
                        for ($i=0; $i<$iterations; $i++) {
-
 
184
                                // https://security.stackexchange.com/questions/149299/rounds-in-a-hashing-function
-
 
185
                                $bin_hash = hash_hmac_ex($algo, $str_password, $bin_hash.$i, true);
-
 
186
                        }
176
                } else if ($mode == PASSWORD_VTS_MCF1_MODE_PBKDF2) {
187
                } else if ($mode == PASSWORD_VTS_MCF1_MODE_PBKDF2) {
177
                        $bin_hash = hash_pbkdf2_ex($algo, $str_password, $str_salt, $iterations, 0, true);
188
                        $bin_hash = hash_pbkdf2_ex($algo, $str_password, $str_salt, $iterations, 0, true);
178
                } else {
189
                } else {
179
                        throw new Exception("Invalid VTS crypt version 1 mode. Expect sp, ps, sps, hmac, or pbkdf2.");
190
                        throw new Exception("Invalid VTS crypt version 1 mode. Expect sp, ps, sps, hmac, or pbkdf2.");
180
                }
191
                }
181
                $bin_salt = $str_salt;
192
                $bin_salt = $str_salt;
182
                $params = array();
193
                $params = array();
183
                $params['a'] = $algo;
194
                $params['a'] = $algo;
184
                $params['m'] = $mode;
195
                $params['m'] = $mode;
185
                if ($mode == PASSWORD_VTS_MCF1_MODE_PBKDF2) $params['i'] = $iterations;
196
                if ($iterations != 0) $params['i'] = $iterations; // i can be omitted if it is 0.
186
                return crypt_modular_format_encode(OID_MCF_VTS_V1, $bin_salt, $bin_hash, $params);
197
                return crypt_modular_format_encode(OID_MCF_VTS_V1, $bin_salt, $bin_hash, $params);
187
        } else {
198
        } else {
188
                throw new Exception("Invalid VTS crypt version, expect 1.");
199
                throw new Exception("Invalid VTS crypt version, expect 1.");
189
        }
200
        }
190
}
201
}
191
 
202
 
192
function vts_crypt_verify($password, $hash): bool {
203
function vts_crypt_verify($password, $hash): bool {
193
        $ver = vts_crypt_version($hash);
204
        $ver = vts_crypt_version($hash);
194
        if ($ver == '1') {
205
        if ($ver == '1') {
195
                // Decode the MCF hash parameters
206
                // Decode the MCF hash parameters
196
                $data = crypt_modular_format_decode($hash);
207
                $data = crypt_modular_format_decode($hash);
197
                if ($data === false) throw new Exception('Invalid auth key');
208
                if ($data === false) throw new Exception('Invalid auth key');
198
                $id = $data['id'];
209
                $id = $data['id'];
199
                $bin_salt = $data['salt'];
210
                $bin_salt = $data['salt'];
200
                $bin_hash = $data['hash'];
211
                $bin_hash = $data['hash'];
201
                $params = $data['params'];
212
                $params = $data['params'];
202
 
213
 
203
                if (!isset($params['a'])) throw new Exception('Param "a" (algo) missing');
214
                if (!isset($params['a'])) throw new Exception('Param "a" (algo) missing');
204
                $algo = $params['a'];
215
                $algo = $params['a'];
205
 
216
 
206
                if (!isset($params['m'])) throw new Exception('Param "m" (mode) missing');
217
                if (!isset($params['m'])) throw new Exception('Param "m" (mode) missing');
207
                $mode = $params['m'];
218
                $mode = $params['m'];
208
 
219
 
209
                if ($mode == PASSWORD_VTS_MCF1_MODE_PBKDF2) {
220
                if ($mode == PASSWORD_VTS_MCF1_MODE_PBKDF2) {
210
                        if (!isset($params['i'])) throw new Exception('Param "i" (iterations) missing');
221
                        if (!isset($params['i'])) throw new Exception('Param "i" (iterations) missing');
211
                        $iterations = $params['i'];
222
                        $iterations = $params['i'];
212
                } else {
223
                } else {
213
                        $iterations = 0;
224
                        $iterations = isset($params['i']) ? $params['i'] : 0;
214
                }
225
                }
215
 
226
 
216
                // Create a VTS MCF 1.0 hash based on the parameters of $hash and the password $password
227
                // Create a VTS MCF 1.0 hash based on the parameters of $hash and the password $password
217
                $calc_authkey_1 = vts_crypt_hash($algo, $password, $bin_salt, $ver, $mode, $iterations);
228
                $calc_authkey_1 = vts_crypt_hash($algo, $password, $bin_salt, $ver, $mode, $iterations);
218
 
229
 
219
                // We rewrite the MCF to make sure that they match (if params have the wrong order)
230
                // We rewrite the MCF to make sure that they match (if params have the wrong order)
220
                $calc_authkey_2 = crypt_modular_format_encode($id, $bin_salt, $bin_hash, $params);
231
                $calc_authkey_2 = crypt_modular_format_encode($id, $bin_salt, $bin_hash, $params);
221
 
232
 
222
                return hash_equals($calc_authkey_1, $calc_authkey_2);
233
                return hash_equals($calc_authkey_1, $calc_authkey_2);
223
        } else {
234
        } else {
224
                throw new Exception("Invalid VTS crypt version, expect 1.");
235
                throw new Exception("Invalid VTS crypt version, expect 1.");
225
        }
236
        }
226
}
237
}
227
 
238
 
228
// --- Part 3: Replacement of vts_password_*() functions
239
// --- Part 3: Replacement of vts_password_*() functions
229
 
240
 
230
/**
241
/**
231
 * This function replaces password_algos() by extending it with
242
 * This function replaces password_algos() by extending it with
232
 * password hashes that are implemented in vts_password_hash().
243
 * password hashes that are implemented in vts_password_hash().
233
 * @return array of hashes that can be used in vts_password_hash().
244
 * @return array of hashes that can be used in vts_password_hash().
234
 */
245
 */
235
function vts_password_algos() {
246
function vts_password_algos() {
236
        $hashes = password_algos();
247
        $hashes = password_algos();
237
        $hashes[] = PASSWORD_STD_DES;   // Algorithm from crypt()
248
        $hashes[] = PASSWORD_STD_DES;   // Algorithm from crypt()
238
        $hashes[] = PASSWORD_EXT_DES;   // Algorithm from crypt()
249
        $hashes[] = PASSWORD_EXT_DES;   // Algorithm from crypt()
239
        $hashes[] = PASSWORD_MD5;       // Algorithm from crypt()
250
        $hashes[] = PASSWORD_MD5;       // Algorithm from crypt()
240
        $hashes[] = PASSWORD_BLOWFISH;  // Algorithm from crypt()
251
        $hashes[] = PASSWORD_BLOWFISH;  // Algorithm from crypt()
241
        $hashes[] = PASSWORD_SHA256;    // Algorithm from crypt()
252
        $hashes[] = PASSWORD_SHA256;    // Algorithm from crypt()
242
        $hashes[] = PASSWORD_SHA512;    // Algorithm from crypt()
253
        $hashes[] = PASSWORD_SHA512;    // Algorithm from crypt()
243
        $hashes[] = PASSWORD_VTS_MCF1;  // Algorithm by ViaThinkSoft
254
        $hashes[] = PASSWORD_VTS_MCF1;  // Algorithm by ViaThinkSoft
244
        return $hashes;
255
        return $hashes;
245
}
256
}
246
 
257
 
247
/** vts_password_get_info() is the same as password_get_info(),
258
/** vts_password_get_info() is the same as password_get_info(),
248
 * but it adds the crypt() and ViaThinkSoft MCF 1.0 algos which can be
259
 * but it adds the crypt() and ViaThinkSoft MCF 1.0 algos which can be
249
 * produced by vts_password_hash()
260
 * produced by vts_password_hash()
250
 * @param string $hash Hash created by vts_password_hash(), password_hash(), or crypt().
261
 * @param string $hash Hash created by vts_password_hash(), password_hash(), or crypt().
251
 * @return array Same output like password_get_info().
262
 * @return array Same output like password_get_info().
252
 */
263
 */
253
function vts_password_get_info($hash) {
264
function vts_password_get_info($hash) {
254
        if (vts_crypt_version($hash) == '1') {
265
        if (vts_crypt_version($hash) == '1') {
255
                // OID_MCF_VTS_V1
266
                // OID_MCF_VTS_V1
256
                $mcf = crypt_modular_format_decode($hash);
267
                $mcf = crypt_modular_format_decode($hash);
257
 
268
 
258
                //$options['salt_length'] = strlen($mcf['salt']);  // Note: salt_length is not a MCF option! It's just a hint for vts_password_hash()
269
                //$options['salt_length'] = strlen($mcf['salt']);  // Note: salt_length is not a MCF option! It's just a hint for vts_password_hash()
259
 
270
 
260
                if (!isset($mcf['params']['a'])) throw new Exception('Param "a" (algo) missing');
271
                if (!isset($mcf['params']['a'])) throw new Exception('Param "a" (algo) missing');
261
                $options['algo'] = $mcf['params']['a'];
272
                $options['algo'] = $mcf['params']['a'];
262
 
273
 
263
                if (!isset($mcf['params']['m'])) throw new Exception('Param "m" (mode) missing');
274
                if (!isset($mcf['params']['m'])) throw new Exception('Param "m" (mode) missing');
264
                $options['mode'] = $mcf['params']['m'];
275
                $options['mode'] = $mcf['params']['m'];
265
 
276
 
266
                if ($options['mode'] == PASSWORD_VTS_MCF1_MODE_PBKDF2) {
277
                if ($options['mode'] == PASSWORD_VTS_MCF1_MODE_PBKDF2) {
267
                        if (!isset($mcf['params']['i'])) throw new Exception('Param "i" (iterations) missing');
278
                        if (!isset($mcf['params']['i'])) throw new Exception('Param "i" (iterations) missing');
268
                        $options['iterations'] = (int)$mcf['params']['i'];
279
                        $options['iterations'] = (int)$mcf['params']['i'];
-
 
280
                } else {
-
 
281
                        $options['iterations'] = isset($mcf['params']['i']) ? (int)$mcf['params']['i'] : 0;
269
                }
282
                }
270
 
283
 
271
                return array(
284
                return array(
272
                        "algo" => PASSWORD_VTS_MCF1,
285
                        "algo" => PASSWORD_VTS_MCF1,
273
                        "algoName" => "vts-mcf-v1",
286
                        "algoName" => "vts-mcf-v1",
274
                        "options" => $options
287
                        "options" => $options
275
                );
288
                );
276
        } else if (!str_starts_with($hash, '$') && (strlen($hash) == 13)) {
289
        } else if (!str_starts_with($hash, '$') && (strlen($hash) == 13)) {
277
                // PASSWORD_STD_DES
290
                // PASSWORD_STD_DES
278
                return array(
291
                return array(
279
                        "algo" => PASSWORD_STD_DES,
292
                        "algo" => PASSWORD_STD_DES,
280
                        "algoName" => "std-des",
293
                        "algoName" => "std-des",
281
                        "options" => array(
294
                        "options" => array(
282
                                // None
295
                                // None
283
                        )
296
                        )
284
                );
297
                );
285
        } else if (str_starts_with($hash, '_') && (strlen($hash) == 20)) {
298
        } else if (str_starts_with($hash, '_') && (strlen($hash) == 20)) {
286
                // PASSWORD_EXT_DES
299
                // PASSWORD_EXT_DES
287
                return array(
300
                return array(
288
                        "algo" => PASSWORD_EXT_DES,
301
                        "algo" => PASSWORD_EXT_DES,
289
                        "algoName" => "ext-des",
302
                        "algoName" => "ext-des",
290
                        "options" => array(
303
                        "options" => array(
291
                                "iterations" => (int)base64_int_decode(substr($hash,1,4))
304
                                "iterations" => (int)base64_int_decode(substr($hash,1,4))
292
                        )
305
                        )
293
                );
306
                );
294
        } else if (str_starts_with($hash, '$1$')) {
307
        } else if (str_starts_with($hash, '$1$')) {
295
                // PASSWORD_MD5
308
                // PASSWORD_MD5
296
                return array(
309
                return array(
297
                        "algo" => PASSWORD_MD5,
310
                        "algo" => PASSWORD_MD5,
298
                        "algoName" => "md5",
311
                        "algoName" => "md5",
299
                        "options" => array(
312
                        "options" => array(
300
                                // None
313
                                // None
301
                        )
314
                        )
302
                );
315
                );
303
        } else if (str_starts_with($hash, '$2$')  || str_starts_with($hash, '$2a$') ||
316
        } else if (str_starts_with($hash, '$2$')  || str_starts_with($hash, '$2a$') ||
304
                   str_starts_with($hash, '$2x$') || str_starts_with($hash, '$2y$')) {
317
                   str_starts_with($hash, '$2x$') || str_starts_with($hash, '$2y$')) {
305
                // PASSWORD_BLOWFISH
318
                // PASSWORD_BLOWFISH
306
                return array(
319
                return array(
307
                        "algo" => PASSWORD_BLOWFISH,
320
                        "algo" => PASSWORD_BLOWFISH,
308
                        "algoName" => "blowfish",
321
                        "algoName" => "blowfish",
309
                        "options" => array(
322
                        "options" => array(
310
                                "cost" => (int)ltrim(explode('$',$hash)[2],'0')
323
                                "cost" => (int)ltrim(explode('$',$hash)[2],'0')
311
                        )
324
                        )
312
                );
325
                );
313
        } else if (str_starts_with($hash, '$5$')) {
326
        } else if (str_starts_with($hash, '$5$')) {
314
                // PASSWORD_SHA256
327
                // PASSWORD_SHA256
315
                return array(
328
                return array(
316
                        "algo" => PASSWORD_SHA256,
329
                        "algo" => PASSWORD_SHA256,
317
                        "algoName" => "sha256",
330
                        "algoName" => "sha256",
318
                        "options" => array(
331
                        "options" => array(
319
                                'rounds' => (int)str_replace('rounds=','',explode('$',$hash)[2])
332
                                'rounds' => (int)str_replace('rounds=','',explode('$',$hash)[2])
320
                        )
333
                        )
321
                );
334
                );
322
        } else if (str_starts_with($hash, '$6$')) {
335
        } else if (str_starts_with($hash, '$6$')) {
323
                // PASSWORD_SHA512
336
                // PASSWORD_SHA512
324
                return array(
337
                return array(
325
                        "algo" => PASSWORD_SHA512,
338
                        "algo" => PASSWORD_SHA512,
326
                        "algoName" => "sha512",
339
                        "algoName" => "sha512",
327
                        "options" => array(
340
                        "options" => array(
328
                                'rounds' => (int)str_replace('rounds=','',explode('$',$hash)[2])
341
                                'rounds' => (int)str_replace('rounds=','',explode('$',$hash)[2])
329
                        )
342
                        )
330
                );
343
                );
331
        } else {
344
        } else {
332
                // PASSWORD_DEFAULT
345
                // PASSWORD_DEFAULT
333
                // PASSWORD_BCRYPT
346
                // PASSWORD_BCRYPT
334
                // PASSWORD_ARGON2I
347
                // PASSWORD_ARGON2I
335
                // PASSWORD_ARGON2ID
348
                // PASSWORD_ARGON2ID
336
                return password_get_info($hash);
349
                return password_get_info($hash);
337
        }
350
        }
338
}
351
}
339
 
352
 
340
/** This function extends password_hash() with the algorithms supported by crypt().
353
/** This function extends password_hash() with the algorithms supported by crypt().
341
 * It also adds vts_crypt_hash() which implements the ViaThinkSoft Modular Crypt Format 1.0.
354
 * It also adds vts_crypt_hash() which implements the ViaThinkSoft Modular Crypt Format 1.0.
342
 * The result can be verified using vts_password_verify().
355
 * The result can be verified using vts_password_verify().
343
 * @param string $password to be hashed
356
 * @param string $password to be hashed
344
 * @param mixed $algo algorithm
357
 * @param mixed $algo algorithm
345
 * @param array $options options for the hashing algorithm
358
 * @param array $options options for the hashing algorithm
346
 * @return string Crypt style password hash
359
 * @return string Crypt style password hash
347
 */
360
 */
348
function vts_password_hash($password, $algo, $options=array()): string {
361
function vts_password_hash($password, $algo, $options=array()): string {
349
        $options = vts_password_fill_default_options($algo, $options);
362
        $options = vts_password_fill_default_options($algo, $options);
350
 
363
 
351
        $crypt_salt = null;
364
        $crypt_salt = null;
352
        if (($algo === PASSWORD_STD_DES) && defined('CRYPT_STD_DES')) {
365
        if (($algo === PASSWORD_STD_DES) && defined('CRYPT_STD_DES')) {
353
                // Standard DES-based hash with a two character salt from the alphabet "./0-9A-Za-z". Using invalid characters in the salt will cause crypt() to fail.
366
                // Standard DES-based hash with a two character salt from the alphabet "./0-9A-Za-z". Using invalid characters in the salt will cause crypt() to fail.
354
                $crypt_salt = des_compat_salt(2);
367
                $crypt_salt = des_compat_salt(2);
355
        } else if (($algo === PASSWORD_EXT_DES) && defined('CRYPT_EXT_DES')) {
368
        } else if (($algo === PASSWORD_EXT_DES) && defined('CRYPT_EXT_DES')) {
356
                // Extended DES-based hash. The "salt" is a 9-character string consisting of an underscore followed by 4 characters of iteration count and 4 characters of salt. Each of these 4-character strings encode 24 bits, least significant character first. The values 0 to 63 are encoded as ./0-9A-Za-z. Using invalid characters in the salt will cause crypt() to fail.
369
                // Extended DES-based hash. The "salt" is a 9-character string consisting of an underscore followed by 4 characters of iteration count and 4 characters of salt. Each of these 4-character strings encode 24 bits, least significant character first. The values 0 to 63 are encoded as ./0-9A-Za-z. Using invalid characters in the salt will cause crypt() to fail.
357
                $iterations = $options['iterations'];
370
                $iterations = $options['iterations'];
358
                $crypt_salt = '_' . base64_int_encode($iterations,4) . des_compat_salt(4);
371
                $crypt_salt = '_' . base64_int_encode($iterations,4) . des_compat_salt(4);
359
        } else if (($algo === PASSWORD_MD5) && defined('CRYPT_MD5')) {
372
        } else if (($algo === PASSWORD_MD5) && defined('CRYPT_MD5')) {
360
                // MD5 hashing with a twelve character salt starting with $1$
373
                // MD5 hashing with a twelve character salt starting with $1$
361
                $crypt_salt = '$1$'.des_compat_salt(12).'$';
374
                $crypt_salt = '$1$'.des_compat_salt(12).'$';
362
        } else if (($algo === PASSWORD_BLOWFISH) && defined('CRYPT_BLOWFISH')) {
375
        } else if (($algo === PASSWORD_BLOWFISH) && defined('CRYPT_BLOWFISH')) {
363
                // Blowfish hashing with a salt as follows: "$2a$", "$2x$" or "$2y$", a two digit cost parameter, "$", and 22 characters from the alphabet "./0-9A-Za-z". Using characters outside of this range in the salt will cause crypt() to return a zero-length string. The two digit cost parameter is the base-2 logarithm of the iteration count for the underlying Blowfish-based hashing algorithm and must be in range 04-31, values outside this range will cause crypt() to fail. "$2x$" hashes are potentially weak; "$2a$" hashes are compatible and mitigate this weakness. For new hashes, "$2y$" should be used.
376
                // Blowfish hashing with a salt as follows: "$2a$", "$2x$" or "$2y$", a two digit cost parameter, "$", and 22 characters from the alphabet "./0-9A-Za-z". Using characters outside of this range in the salt will cause crypt() to return a zero-length string. The two digit cost parameter is the base-2 logarithm of the iteration count for the underlying Blowfish-based hashing algorithm and must be in range 04-31, values outside this range will cause crypt() to fail. "$2x$" hashes are potentially weak; "$2a$" hashes are compatible and mitigate this weakness. For new hashes, "$2y$" should be used.
364
                $algo = '$2y$'; // most secure
377
                $algo = '$2y$'; // most secure
365
                $cost = $options['cost'];
378
                $cost = $options['cost'];
366
                $crypt_salt = $algo.str_pad($cost,2,'0',STR_PAD_LEFT).'$'.des_compat_salt(22).'$';
379
                $crypt_salt = $algo.str_pad($cost,2,'0',STR_PAD_LEFT).'$'.des_compat_salt(22).'$';
367
        } else if (($algo === PASSWORD_SHA256) && defined('CRYPT_SHA256')) {
380
        } else if (($algo === PASSWORD_SHA256) && defined('CRYPT_SHA256')) {
368
                // SHA-256 hash with a sixteen character salt prefixed with $5$. If the salt string starts with 'rounds=<N>$', the numeric value of N is used to indicate how many times the hashing loop should be executed, much like the cost parameter on Blowfish. The default number of rounds is 5000, there is a minimum of 1000 and a maximum of 999,999,999. Any selection of N outside this range will be truncated to the nearest limit.
381
                // SHA-256 hash with a sixteen character salt prefixed with $5$. If the salt string starts with 'rounds=<N>$', the numeric value of N is used to indicate how many times the hashing loop should be executed, much like the cost parameter on Blowfish. The default number of rounds is 5000, there is a minimum of 1000 and a maximum of 999,999,999. Any selection of N outside this range will be truncated to the nearest limit.
369
                $algo = '$5$';
382
                $algo = '$5$';
370
                $rounds = $options['rounds'];
383
                $rounds = $options['rounds'];
371
                $crypt_salt = $algo.'rounds='.$rounds.'$'.des_compat_salt(16).'$';
384
                $crypt_salt = $algo.'rounds='.$rounds.'$'.des_compat_salt(16).'$';
372
        } else if (($algo === PASSWORD_SHA512) && defined('CRYPT_SHA512')) {
385
        } else if (($algo === PASSWORD_SHA512) && defined('CRYPT_SHA512')) {
373
                // SHA-512 hash with a sixteen character salt prefixed with $6$. If the salt string starts with 'rounds=<N>$', the numeric value of N is used to indicate how many times the hashing loop should be executed, much like the cost parameter on Blowfish. The default number of rounds is 5000, there is a minimum of 1000 and a maximum of 999,999,999. Any selection of N outside this range will be truncated to the nearest limit.
386
                // SHA-512 hash with a sixteen character salt prefixed with $6$. If the salt string starts with 'rounds=<N>$', the numeric value of N is used to indicate how many times the hashing loop should be executed, much like the cost parameter on Blowfish. The default number of rounds is 5000, there is a minimum of 1000 and a maximum of 999,999,999. Any selection of N outside this range will be truncated to the nearest limit.
374
                $algo = '$6$';
387
                $algo = '$6$';
375
                $rounds = $options['rounds'];
388
                $rounds = $options['rounds'];
376
                $crypt_salt = $algo.'rounds='.$rounds.'$'.des_compat_salt(16).'$';
389
                $crypt_salt = $algo.'rounds='.$rounds.'$'.des_compat_salt(16).'$';
377
        }
390
        }
378
 
391
 
379
        if (!is_null($crypt_salt)) {
392
        if (!is_null($crypt_salt)) {
380
                // Algorithms: PASSWORD_STD_DES
393
                // Algorithms: PASSWORD_STD_DES
381
                //             PASSWORD_EXT_DES
394
                //             PASSWORD_EXT_DES
382
                //             PASSWORD_MD5
395
                //             PASSWORD_MD5
383
                //             PASSWORD_BLOWFISH
396
                //             PASSWORD_BLOWFISH
384
                //             PASSWORD_SHA256
397
                //             PASSWORD_SHA256
385
                //             PASSWORD_SHA512
398
                //             PASSWORD_SHA512
386
                $out = crypt($password, $crypt_salt);
399
                $out = crypt($password, $crypt_salt);
387
                if (strlen($out) < 13) throw new Exception("crypt() failed");
400
                if (strlen($out) < 13) throw new Exception("crypt() failed");
388
                return $out;
401
                return $out;
389
        } else if ($algo === PASSWORD_VTS_MCF1) {
402
        } else if ($algo === PASSWORD_VTS_MCF1) {
390
                // Algorithms: PASSWORD_VTS_MCF1
403
                // Algorithms: PASSWORD_VTS_MCF1
391
                $ver  = '1';
404
                $ver  = '1';
392
                $algo = $options['algo'];
405
                $algo = $options['algo'];
393
                $mode = $options['mode'];
406
                $mode = $options['mode'];
394
                $iterations = $options['iterations'];
407
                $iterations = $options['iterations'];
395
                $salt_len = isset($options['salt_length']) ? $options['salt_length'] : 50; // Note: salt_length is not a MCF option! It's just a hint for vts_password_hash()
408
                $salt_len = isset($options['salt_length']) ? $options['salt_length'] : 50; // Note: salt_length is not a MCF option! It's just a hint for vts_password_hash()
396
                $salt = random_bytes_ex($salt_len, true, true);
409
                $salt = random_bytes_ex($salt_len, true, true);
397
                return vts_crypt_hash($algo, $password, $salt, $ver, $mode, $iterations);
410
                return vts_crypt_hash($algo, $password, $salt, $ver, $mode, $iterations);
398
        } else {
411
        } else {
399
                // Algorithms: PASSWORD_DEFAULT
412
                // Algorithms: PASSWORD_DEFAULT
400
                //             PASSWORD_BCRYPT
413
                //             PASSWORD_BCRYPT
401
                //             PASSWORD_ARGON2I
414
                //             PASSWORD_ARGON2I
402
                //             PASSWORD_ARGON2ID
415
                //             PASSWORD_ARGON2ID
403
                return password_hash($password, $algo, $options);
416
                return password_hash($password, $algo, $options);
404
        }
417
        }
405
}
418
}
406
 
419
 
407
/** This function replaces password_needs_rehash() by adding additional algorithms
420
/** This function replaces password_needs_rehash() by adding additional algorithms
408
 * supported by vts_password_hash().
421
 * supported by vts_password_hash().
409
 * @param string $hash The current hash
422
 * @param string $hash The current hash
410
 * @param string|int|null $algo Desired new default algo
423
 * @param string|int|null $algo Desired new default algo
411
 * @param array $options Desired new default options
424
 * @param array $options Desired new default options
412
 * @return bool True if algo or options of the current hash don't match the current desired values ($algo and $options), otherwise false.
425
 * @return bool True if algo or options of the current hash don't match the current desired values ($algo and $options), otherwise false.
413
 */
426
 */
414
function vts_password_needs_rehash($hash, $algo, $options=array()) {
427
function vts_password_needs_rehash($hash, $algo, $options=array()) {
415
        $options = vts_password_fill_default_options($algo, $options);
428
        $options = vts_password_fill_default_options($algo, $options);
416
 
429
 
417
        $info = vts_password_get_info($hash);
430
        $info = vts_password_get_info($hash);
418
        $algo2 = $info['algo'];
431
        $algo2 = $info['algo'];
419
        $options2 = $info['options'];
432
        $options2 = $info['options'];
420
 
433
 
421
        // Check if algorithm matches
434
        // Check if algorithm matches
422
        if ($algo !== $algo2) return true;
435
        if ($algo !== $algo2) return true;
423
 
436
 
424
        if (vts_crypt_version($hash) == '1') {
437
        if (vts_crypt_version($hash) == '1') {
425
                if (isset($options['salt_length'])) {
438
                if (isset($options['salt_length'])) {
426
                        // For VTS MCF 1.0, salt_length is a valid option for vts_password_hash(),
439
                        // For VTS MCF 1.0, salt_length is a valid option for vts_password_hash(),
427
                        // but it is not a valid option inside the MCF options
440
                        // but it is not a valid option inside the MCF options
428
                        // and it is not a valid option for vts_password_get_info().
441
                        // and it is not a valid option for vts_password_get_info().
429
                        unset($options['salt_length']);
442
                        unset($options['salt_length']);
430
                }
443
                }
431
 
444
 
432
                // iterations=0 means: Default, depending on the algo
445
                // For PBKDF2, iterations=0 means: Default, depending on the algo
433
                if (($options['iterations'] == 0/*default*/) && ($options2['mode'] == PASSWORD_VTS_MCF1_MODE_PBKDF2)) {
446
                if (($options['iterations'] == 0/*default*/) && ($options2['mode'] == PASSWORD_VTS_MCF1_MODE_PBKDF2)) {
434
                        $algo = $options2['algo'];
447
                        $algo = $options2['algo'];
435
                        $userland = !hash_pbkdf2_supported_natively($algo) && str_starts_with($algo, 'sha3-') && method_exists('\bb\Sha3\Sha3', 'hash_pbkdf2');
448
                        $userland = !hash_pbkdf2_supported_natively($algo) && str_starts_with($algo, 'sha3-') && method_exists('\bb\Sha3\Sha3', 'hash_pbkdf2');
436
                        $options['iterations'] = _vts_password_default_iterations($algo, $userland);
449
                        $options['iterations'] = _vts_password_default_iterations($algo, $userland);
437
                }
450
                }
438
        }
451
        }
439
 
452
 
440
        // Check if options match
453
        // Check if options match
441
        if (count($options) !== count($options2)) return true;
454
        if (count($options) !== count($options2)) return true;
442
        foreach ($options as $name => $val) {
455
        foreach ($options as $name => $val) {
443
                if ($options2[$name] != $val) return true;
456
                if ($options2[$name] != $val) return true;
444
        }
457
        }
445
        return false;
458
        return false;
446
}
459
}
447
 
460
 
448
/** This function extends password_verify() by adding ViaThinkSoft Modular Crypt Format 1.0.
461
/** This function extends password_verify() by adding ViaThinkSoft Modular Crypt Format 1.0.
449
 * @param string $password to be checked
462
 * @param string $password to be checked
450
 * @param string $hash Hash created by crypt(), password_hash(), or vts_password_hash().
463
 * @param string $hash Hash created by crypt(), password_hash(), or vts_password_hash().
451
 * @return bool true if password is valid
464
 * @return bool true if password is valid
452
 */
465
 */
453
function vts_password_verify($password, $hash): bool {
466
function vts_password_verify($password, $hash): bool {
454
        if (vts_crypt_version($hash) != '0') {
467
        if (vts_crypt_version($hash) != '0') {
455
                // Hash created by vts_password_hash(), or vts_crypt_hash()
468
                // Hash created by vts_password_hash(), or vts_crypt_hash()
456
                return vts_crypt_verify($password, $hash);
469
                return vts_crypt_verify($password, $hash);
457
        } else {
470
        } else {
458
                // Hash created by vts_password_hash(), password_hash(), or crypt()
471
                // Hash created by vts_password_hash(), password_hash(), or crypt()
459
                return password_verify($password, $hash);
472
                return password_verify($password, $hash);
460
        }
473
        }
461
}
474
}
462
 
475
 
463
// --- Part 4: Functions which include a fallback to a pure-PHP sha3 implementation (requires https://github.com/danielmarschall/php-sha3 )
476
// --- Part 4: Functions which include a fallback to a pure-PHP sha3 implementation (requires https://github.com/danielmarschall/php-sha3 )
464
 
477
 
465
function hash_ex($algo, $data, $binary=false, $options=array()) {
478
function hash_ex($algo, $data, $binary=false, $options=array()) {
466
        if (!hash_supported_natively($algo) && str_starts_with($algo, 'sha3-') && method_exists('\bb\Sha3\Sha3', 'hash')) {
479
        if (!hash_supported_natively($algo) && str_starts_with($algo, 'sha3-') && method_exists('\bb\Sha3\Sha3', 'hash')) {
467
                $bits = explode('-',$algo)[1];
480
                $bits = explode('-',$algo)[1];
468
                $hash = \bb\Sha3\Sha3::hash($data, $bits, $binary);
481
                $hash = \bb\Sha3\Sha3::hash($data, $bits, $binary);
469
        } else {
482
        } else {
470
                $hash = hash($algo, $data, $binary);
483
                $hash = hash($algo, $data, $binary);
471
        }
484
        }
472
        return $hash;
485
        return $hash;
473
}
486
}
474
 
487
 
475
function hash_hmac_ex($algo, $data, $key, $binary=false) {
488
function hash_hmac_ex($algo, $data, $key, $binary=false) {
476
        if (!hash_hmac_supported_natively($algo) && str_starts_with($algo, 'sha3-') && method_exists('\bb\Sha3\Sha3', 'hash_hmac')) {
489
        if (!hash_hmac_supported_natively($algo) && str_starts_with($algo, 'sha3-') && method_exists('\bb\Sha3\Sha3', 'hash_hmac')) {
477
                $bits = explode('-',$algo)[1];
490
                $bits = explode('-',$algo)[1];
478
                $hash = \bb\Sha3\Sha3::hash_hmac($data, $key, $bits, $binary);
491
                $hash = \bb\Sha3\Sha3::hash_hmac($data, $key, $bits, $binary);
479
        } else {
492
        } else {
480
                $hash = hash_hmac($algo, $data, $key, $binary);
493
                $hash = hash_hmac($algo, $data, $key, $binary);
481
        }
494
        }
482
        return $hash;
495
        return $hash;
483
}
496
}
484
 
497
 
485
function hash_pbkdf2_ex($algo, $password, $salt, &$iterations=0, $length=0, $binary=false) {
498
function hash_pbkdf2_ex($algo, $password, $salt, &$iterations=0, $length=0, $binary=false) {
486
        if (!hash_pbkdf2_supported_natively($algo) && str_starts_with($algo, 'sha3-') && method_exists('\bb\Sha3\Sha3', 'hash_pbkdf2')) {
499
        if (!hash_pbkdf2_supported_natively($algo) && str_starts_with($algo, 'sha3-') && method_exists('\bb\Sha3\Sha3', 'hash_pbkdf2')) {
487
                if ($iterations == 0/*default*/) {
500
                if ($iterations == 0/*default*/) {
488
                        $iterations = _vts_password_default_iterations($algo, true);
501
                        $iterations = _vts_password_default_iterations($algo, true);
489
                }
502
                }
490
                $bits = explode('-',$algo)[1];
503
                $bits = explode('-',$algo)[1];
491
                $hash = \bb\Sha3\Sha3::hash_pbkdf2($password, $salt, $iterations, $bits, $length, $binary);
504
                $hash = \bb\Sha3\Sha3::hash_pbkdf2($password, $salt, $iterations, $bits, $length, $binary);
492
        } else {
505
        } else {
493
                if ($iterations == 0/*default*/) {
506
                if ($iterations == 0/*default*/) {
494
                        $iterations = _vts_password_default_iterations($algo, false);
507
                        $iterations = _vts_password_default_iterations($algo, false);
495
                }
508
                }
496
                $hash = hash_pbkdf2($algo, $password, $salt, $iterations, $length, $binary);
509
                $hash = hash_pbkdf2($algo, $password, $salt, $iterations, $length, $binary);
497
        }
510
        }
498
        return $hash;
511
        return $hash;
499
}
512
}
500
 
513
 
501
// --- Part 5: Useful functions required by the crypt-functions
514
// --- Part 5: Useful functions required by the crypt-functions
502
 
515
 
503
define('BASE64_RFC4648_ALPHABET', '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/');
516
define('BASE64_RFC4648_ALPHABET', '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/');
504
define('BASE64_CRYPT_ALPHABET',   './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz');
517
define('BASE64_CRYPT_ALPHABET',   './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz');
505
 
518
 
506
function des_compat_salt($salt_len) {
519
function des_compat_salt($salt_len) {
507
        if ($salt_len <= 0) return '';
520
        if ($salt_len <= 0) return '';
508
        $characters = BASE64_CRYPT_ALPHABET;
521
        $characters = BASE64_CRYPT_ALPHABET;
509
        $salt = '';
522
        $salt = '';
510
        $bytes = random_bytes_ex($salt_len, true, true);
523
        $bytes = random_bytes_ex($salt_len, true, true);
511
        for ($i=0; $i<$salt_len; $i++) {
524
        for ($i=0; $i<$salt_len; $i++) {
512
                $salt .= $characters[ord($bytes[$i]) % strlen($characters)];
525
                $salt .= $characters[ord($bytes[$i]) % strlen($characters)];
513
        }
526
        }
514
        return $salt;
527
        return $salt;
515
}
528
}
516
 
529
 
517
function base64_int_encode($num, $len) {
530
function base64_int_encode($num, $len) {
518
        // https://stackoverflow.com/questions/15534982/which-iteration-rules-apply-on-crypt-using-crypt-ext-des
531
        // https://stackoverflow.com/questions/15534982/which-iteration-rules-apply-on-crypt-using-crypt-ext-des
519
        $alphabet_raw = BASE64_CRYPT_ALPHABET;
532
        $alphabet_raw = BASE64_CRYPT_ALPHABET;
520
        $alphabet = str_split($alphabet_raw);
533
        $alphabet = str_split($alphabet_raw);
521
        $arr = array();
534
        $arr = array();
522
        $base = sizeof($alphabet);
535
        $base = sizeof($alphabet);
523
        while ($num) {
536
        while ($num) {
524
                $rem = $num % $base;
537
                $rem = $num % $base;
525
                $num = (int)($num / $base);
538
                $num = (int)($num / $base);
526
                $arr[] = $alphabet[$rem];
539
                $arr[] = $alphabet[$rem];
527
        }
540
        }
528
        $string = implode($arr);
541
        $string = implode($arr);
529
        return str_pad($string, $len, '.', STR_PAD_RIGHT);
542
        return str_pad($string, $len, '.', STR_PAD_RIGHT);
530
}
543
}
531
 
544
 
532
function base64_int_decode($base64) {
545
function base64_int_decode($base64) {
533
        $num = 0;
546
        $num = 0;
534
        for ($i=strlen($base64)-1;$i>=0;$i--) {
547
        for ($i=strlen($base64)-1;$i>=0;$i--) {
535
                $num += strpos(BASE64_CRYPT_ALPHABET, $base64[$i])*pow(strlen(BASE64_CRYPT_ALPHABET),$i);
548
                $num += strpos(BASE64_CRYPT_ALPHABET, $base64[$i])*pow(strlen(BASE64_CRYPT_ALPHABET),$i);
536
        }
549
        }
537
        return $num;
550
        return $num;
538
}
551
}
539
 
552
 
540
function crypt_radix64_encode($str) {
553
function crypt_radix64_encode($str) {
541
        $x = $str;
554
        $x = $str;
542
        $x = base64_encode($x);
555
        $x = base64_encode($x);
543
        $x = rtrim($x, '='); // remove padding
556
        $x = rtrim($x, '='); // remove padding
544
        $x = strtr($x, BASE64_RFC4648_ALPHABET, BASE64_CRYPT_ALPHABET);
557
        $x = strtr($x, BASE64_RFC4648_ALPHABET, BASE64_CRYPT_ALPHABET);
545
        return $x;
558
        return $x;
546
}
559
}
547
 
560
 
548
function crypt_radix64_decode($str) {
561
function crypt_radix64_decode($str) {
549
        $x = $str;
562
        $x = $str;
550
        $x = strtr($x, BASE64_CRYPT_ALPHABET, BASE64_RFC4648_ALPHABET);
563
        $x = strtr($x, BASE64_CRYPT_ALPHABET, BASE64_RFC4648_ALPHABET);
551
        $x = base64_decode($x);
564
        $x = base64_decode($x);
552
        return $x;
565
        return $x;
553
}
566
}
554
 
567
 
555
function hash_supported_natively($algo) {
568
function hash_supported_natively($algo) {
556
        if (version_compare(PHP_VERSION, '5.1.2') >= 0) {
569
        if (version_compare(PHP_VERSION, '5.1.2') >= 0) {
557
                return in_array($algo, hash_algos());
570
                return in_array($algo, hash_algos());
558
        } else {
571
        } else {
559
                return false;
572
                return false;
560
        }
573
        }
561
}
574
}
562
 
575
 
563
function hash_hmac_supported_natively($algo): bool {
576
function hash_hmac_supported_natively($algo): bool {
564
        if (version_compare(PHP_VERSION, '7.2.0') >= 0) {
577
        if (version_compare(PHP_VERSION, '7.2.0') >= 0) {
565
                return in_array($algo, hash_hmac_algos());
578
                return in_array($algo, hash_hmac_algos());
566
        } else if (version_compare(PHP_VERSION, '5.1.2') >= 0) {
579
        } else if (version_compare(PHP_VERSION, '5.1.2') >= 0) {
567
                return in_array($algo, hash_algos());
580
                return in_array($algo, hash_algos());
568
        } else {
581
        } else {
569
                return false;
582
                return false;
570
        }
583
        }
571
}
584
}
572
 
585
 
573
function hash_pbkdf2_supported_natively($algo) {
586
function hash_pbkdf2_supported_natively($algo) {
574
        return hash_supported_natively($algo);
587
        return hash_supported_natively($algo);
575
}
588
}
576
 
589
 
577
function vts_password_fill_default_options($algo, $options) {
590
function vts_password_fill_default_options($algo, $options) {
578
        if ($algo === PASSWORD_STD_DES) {
591
        if ($algo === PASSWORD_STD_DES) {
579
                // No options
592
                // No options
580
        } else if ($algo === PASSWORD_EXT_DES) {
593
        } else if ($algo === PASSWORD_EXT_DES) {
581
                if (!isset($options['iterations'])) {
594
                if (!isset($options['iterations'])) {
582
                        $options['iterations'] = PASSWORD_EXT_DES_DEFAULT_ITERATIONS;
595
                        $options['iterations'] = PASSWORD_EXT_DES_DEFAULT_ITERATIONS;
583
                }
596
                }
584
        } else if ($algo === PASSWORD_MD5) {
597
        } else if ($algo === PASSWORD_MD5) {
585
                // No options
598
                // No options
586
        } else if ($algo === PASSWORD_BLOWFISH) {
599
        } else if ($algo === PASSWORD_BLOWFISH) {
587
                if (!isset($options['cost'])) {
600
                if (!isset($options['cost'])) {
588
                        $options['cost'] = PASSWORD_BLOWFISH_DEFAULT_COST;
601
                        $options['cost'] = PASSWORD_BLOWFISH_DEFAULT_COST;
589
                }
602
                }
590
        } else if ($algo === PASSWORD_SHA256) {
603
        } else if ($algo === PASSWORD_SHA256) {
591
                if (!isset($options['rounds'])) {
604
                if (!isset($options['rounds'])) {
592
                        $options['rounds'] = PASSWORD_SHA256_DEFAULT_ROUNDS;
605
                        $options['rounds'] = PASSWORD_SHA256_DEFAULT_ROUNDS;
593
                }
606
                }
594
        } else if ($algo === PASSWORD_SHA512) {
607
        } else if ($algo === PASSWORD_SHA512) {
595
                if (!isset($options['rounds'])) {
608
                if (!isset($options['rounds'])) {
596
                        $options['rounds'] = PASSWORD_SHA512_DEFAULT_ROUNDS;
609
                        $options['rounds'] = PASSWORD_SHA512_DEFAULT_ROUNDS;
597
                }
610
                }
598
        } else if ($algo === PASSWORD_VTS_MCF1) {
611
        } else if ($algo === PASSWORD_VTS_MCF1) {
599
                if (!isset($options['algo'])) {
612
                if (!isset($options['algo'])) {
600
                        $options['algo'] = PASSWORD_VTS_MCF1_DEFAULT_ALGO;
613
                        $options['algo'] = PASSWORD_VTS_MCF1_DEFAULT_ALGO;
601
                }
614
                }
602
                if (!isset($options['mode'])) {
615
                if (!isset($options['mode'])) {
603
                        $options['mode'] = PASSWORD_VTS_MCF1_DEFAULT_MODE;
616
                        $options['mode'] = PASSWORD_VTS_MCF1_DEFAULT_MODE;
604
                }
617
                }
605
                if ($options['mode'] == PASSWORD_VTS_MCF1_MODE_PBKDF2) {
618
                if ($options['mode'] == PASSWORD_VTS_MCF1_MODE_PBKDF2) {
606
                        if (!isset($options['iterations'])) {
619
                        if (!isset($options['iterations'])) {
607
                                $options['iterations'] = PASSWORD_VTS_MCF1_DEFAULT_ITERATIONS;
620
                                $options['iterations'] = PASSWORD_VTS_MCF1_DEFAULT_ITERATIONS;
608
                        }
621
                        }
609
                } else {
622
                } else {
610
                        unset($options['iterations']);
623
                        $options['iterations'] = isset($options['iterations']) ? $options['iterations'] : 0;
611
                }
624
                }
612
        }
625
        }
613
        return $options;
626
        return $options;
614
}
627
}
615
 
628
 
616
function _vts_password_default_iterations($algo, $userland) {
629
function _vts_password_default_iterations($algo, $userland) {
617
        if ($userland) {
630
        if ($userland) {
618
                return 100; // because the userland implementation is EXTREMELY slow, we must choose a small value, sorry...
631
                return 100; // because the userland implementation is EXTREMELY slow, we must choose a small value, sorry...
619
        } else {
632
        } else {
620
                // Recommendations taken from https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2
633
                // Recommendations taken from https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2
621
                // Note that hash_pbkdf2() implements PBKDF2-HMAC-*
634
                // Note that hash_pbkdf2() implements PBKDF2-HMAC-*
622
                if      ($algo == 'sha3-512')    return  100000;
635
                if      ($algo == 'sha3-512')    return  100000;
623
                else if ($algo == 'sha3-384')    return  100000;
636
                else if ($algo == 'sha3-384')    return  100000;
624
                else if ($algo == 'sha3-256')    return  100000;
637
                else if ($algo == 'sha3-256')    return  100000;
625
                else if ($algo == 'sha3-224')    return  100000;
638
                else if ($algo == 'sha3-224')    return  100000;
626
                else if ($algo == 'sha512')      return  210000; // value by owasp.org cheatcheat (28 February 2023)
639
                else if ($algo == 'sha512')      return  210000; // value by owasp.org cheatcheat (28 February 2023)
627
                else if ($algo == 'sha512/256')  return  210000; // value by owasp.org cheatcheat (28 February 2023)
640
                else if ($algo == 'sha512/256')  return  210000; // value by owasp.org cheatcheat (28 February 2023)
628
                else if ($algo == 'sha512/224')  return  210000; // value by owasp.org cheatcheat (28 February 2023)
641
                else if ($algo == 'sha512/224')  return  210000; // value by owasp.org cheatcheat (28 February 2023)
629
                else if ($algo == 'sha384')      return  600000;
642
                else if ($algo == 'sha384')      return  600000;
630
                else if ($algo == 'sha256')      return  600000; // value by owasp.org cheatcheat (28 February 2023)
643
                else if ($algo == 'sha256')      return  600000; // value by owasp.org cheatcheat (28 February 2023)
631
                else if ($algo == 'sha224')      return  600000;
644
                else if ($algo == 'sha224')      return  600000;
632
                else if ($algo == 'sha1')        return 1300000; // value by owasp.org cheatcheat (28 February 2023)
645
                else if ($algo == 'sha1')        return 1300000; // value by owasp.org cheatcheat (28 February 2023)
633
                else if ($algo == 'md5')         return 5000000;
646
                else if ($algo == 'md5')         return 5000000;
634
                else                             return    5000;
647
                else                             return    5000;
635
        }
648
        }
636
}
649
}
637
 
650
 
638
// --- Part 6: Selftest
651
// --- Part 6: Selftest
639
 
652
 
640
/*
653
/*
641
for ($i=0; $i<9999; $i++) {
654
for ($i=0; $i<9999; $i++) {
642
        assert($i===base64_int_decode(base64_int_encode($i,4)));
655
        assert($i===base64_int_decode(base64_int_encode($i,4)));
643
}
656
}
644
 
657
 
645
$rnd = random_bytes_ex(50, true, true);
658
$rnd = random_bytes_ex(50, true, true);
646
assert(crypt_radix64_decode(crypt_radix64_encode($rnd)) === $rnd);
659
assert(crypt_radix64_decode(crypt_radix64_encode($rnd)) === $rnd);
647
 
660
 
648
$password = random_bytes_ex(20, false, true);
661
$password = random_bytes_ex(20, false, true);
649
 
662
 
650
assert(vts_password_verify($password,$dummy = vts_password_hash($password, PASSWORD_STD_DES)));
663
assert(vts_password_verify($password,$dummy = vts_password_hash($password, PASSWORD_STD_DES)));
651
//echo "'$dummy' ".strlen($dummy)."\n";
664
//echo "'$dummy' ".strlen($dummy)."\n";
652
//var_dump(vts_password_get_info($dummy));
665
//var_dump(vts_password_get_info($dummy));
653
 
666
 
654
assert(vts_password_verify($password,$dummy = vts_password_hash($password, PASSWORD_EXT_DES)));
667
assert(vts_password_verify($password,$dummy = vts_password_hash($password, PASSWORD_EXT_DES)));
655
//echo "'$dummy' ".strlen($dummy)."\n";
668
//echo "'$dummy' ".strlen($dummy)."\n";
656
//var_dump(vts_password_get_info($dummy));
669
//var_dump(vts_password_get_info($dummy));
657
 
670
 
658
assert(vts_password_verify($password,$dummy = vts_password_hash($password, PASSWORD_MD5)));
671
assert(vts_password_verify($password,$dummy = vts_password_hash($password, PASSWORD_MD5)));
659
//echo "'$dummy' ".strlen($dummy)."\n";
672
//echo "'$dummy' ".strlen($dummy)."\n";
660
//var_dump(vts_password_get_info($dummy));
673
//var_dump(vts_password_get_info($dummy));
661
 
674
 
662
assert(vts_password_verify($password,$dummy = vts_password_hash($password, PASSWORD_BLOWFISH)));
675
assert(vts_password_verify($password,$dummy = vts_password_hash($password, PASSWORD_BLOWFISH)));
663
//echo "'$dummy' ".strlen($dummy)."\n";
676
//echo "'$dummy' ".strlen($dummy)."\n";
664
//var_dump(vts_password_get_info($dummy));
677
//var_dump(vts_password_get_info($dummy));
665
 
678
 
666
assert(vts_password_verify($password,$dummy = vts_password_hash($password, PASSWORD_SHA256)));
679
assert(vts_password_verify($password,$dummy = vts_password_hash($password, PASSWORD_SHA256)));
667
//echo "'$dummy' ".strlen($dummy)."\n";
680
//echo "'$dummy' ".strlen($dummy)."\n";
668
//var_dump(vts_password_get_info($dummy));
681
//var_dump(vts_password_get_info($dummy));
669
 
682
 
670
assert(vts_password_verify($password,$dummy = vts_password_hash($password, PASSWORD_SHA512)));
683
assert(vts_password_verify($password,$dummy = vts_password_hash($password, PASSWORD_SHA512)));
671
//echo "'$dummy' ".strlen($dummy)."\n";
684
//echo "'$dummy' ".strlen($dummy)."\n";
672
//var_dump(vts_password_get_info($dummy));
685
//var_dump(vts_password_get_info($dummy));
673
 
686
 
674
assert(vts_password_verify($password,$dummy = vts_password_hash($password, PASSWORD_VTS_MCF1, array(
687
assert(vts_password_verify($password,$dummy = vts_password_hash($password, PASSWORD_VTS_MCF1, array(
675
        'algo' => 'sha3-512',
688
        'algo' => 'sha3-512',
676
        'mode' => 'pbkdf2',
689
        'mode' => 'pbkdf2',
677
        'iterations' => 0
690
        'iterations' => 0
678
))));
691
))));
679
//echo "'$dummy' ".strlen($dummy)."\n";
692
//echo "'$dummy' ".strlen($dummy)."\n";
680
//var_dump(vts_password_get_info($dummy));
693
//var_dump(vts_password_get_info($dummy));
681
assert(false===vts_password_needs_rehash($dummy,PASSWORD_VTS_MCF1,array(
694
assert(false===vts_password_needs_rehash($dummy,PASSWORD_VTS_MCF1,array(
682
        'salt_length' => 51,
695
        'salt_length' => 51,
683
        'algo' => 'sha3-512',
696
        'algo' => 'sha3-512',
684
        'mode' => 'pbkdf2',
697
        'mode' => 'pbkdf2',
685
        'iterations' => 0
698
        'iterations' => 0
686
)));
699
)));
687
assert(true===vts_password_needs_rehash($dummy,PASSWORD_VTS_MCF1,array(
700
assert(true===vts_password_needs_rehash($dummy,PASSWORD_VTS_MCF1,array(
688
        'salt_length' => 50,
701
        'salt_length' => 50,
689
        'algo' => 'sha3-256',
702
        'algo' => 'sha3-256',
690
        'mode' => 'pbkdf2',
703
        'mode' => 'pbkdf2',
691
        'iterations' => 0
704
        'iterations' => 0
692
)));
705
)));
693
 
706
 
-
 
707
assert(vts_password_verify($password,$dummy = vts_password_hash($password, PASSWORD_VTS_MCF1, array(
-
 
708
        'algo' => 'sha3-512',
-
 
709
        'mode' => 'sps',
-
 
710
        'iterations' => 2
-
 
711
))));
-
 
712
//echo "'$dummy' ".strlen($dummy)."\n";
-
 
713
//var_dump(vts_password_get_info($dummy));
-
 
714
assert(false===vts_password_needs_rehash($dummy,PASSWORD_VTS_MCF1,array(
-
 
715
        'salt_length' => 51,
-
 
716
        'algo' => 'sha3-512',
-
 
717
        'mode' => 'sps',
-
 
718
        'iterations' => 2
-
 
719
)));
-
 
720
assert(true===vts_password_needs_rehash($dummy,PASSWORD_VTS_MCF1,array(
-
 
721
        'salt_length' => 50,
-
 
722
        'algo' => 'sha3-256',
-
 
723
        'mode' => 'sps',
-
 
724
        'iterations' => 2
-
 
725
)));
-
 
726
 
-
 
727
assert(vts_password_verify($password,$dummy = vts_password_hash($password, PASSWORD_VTS_MCF1, array(
-
 
728
        'algo' => 'sha3-512',
-
 
729
        'mode' => 'hmac',
-
 
730
        'iterations' => 2
-
 
731
))));
-
 
732
//echo "'$dummy' ".strlen($dummy)."\n";
-
 
733
//var_dump(vts_password_get_info($dummy));
-
 
734
assert(false===vts_password_needs_rehash($dummy,PASSWORD_VTS_MCF1,array(
-
 
735
        'salt_length' => 51,
-
 
736
        'algo' => 'sha3-512',
-
 
737
        'mode' => 'hmac',
-
 
738
        'iterations' => 2
-
 
739
)));
-
 
740
assert(true===vts_password_needs_rehash($dummy,PASSWORD_VTS_MCF1,array(
-
 
741
        'salt_length' => 50,
-
 
742
        'algo' => 'sha3-256',
-
 
743
        'mode' => 'hmac',
-
 
744
        'iterations' => 2
-
 
745
)));
694
*/
746
*/
695
 
747