Subversion Repositories php_utils

Rev

Rev 70 | Rev 72 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

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