Subversion Repositories php_utils

Rev

Rev 69 | Rev 71 | Go to most recent revision | Show entire file | Regard whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 69 Rev 70
Line 1... Line 1...
1
<?php
1
<?php
2
 
2
 
3
/*
3
/*
4
 * ViaThinkSoft Modular Crypt Format 1.0 / vts_password_hash() / vts_password_verify()
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-02-28
6
 * Revision 2023-03-02
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
 *
Line 41... Line 41...
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):
-
 
47
        bcrypt [Standardized crypt identifier 2, 2a, 2x, 2y]
-
 
48
        argon2i [Crypt identifier argon2i, not standardized]
-
 
49
        argon2id [Crypt identifier argon2i, not standardized]
46
Valid <mode> :
50
Valid <mode> :
47
        sp = salt + password
51
        sp = salt + password
48
        ps = password + salt
52
        ps = password + salt
49
        sps = salt + password + salt
53
        sps = salt + password + salt
50
        hmac = HMAC (salt is the key)
54
        hmac = HMAC (salt is the key)
Line 61... Line 65...
61
require_once __DIR__ . '/misc_functions.inc.php';
65
require_once __DIR__ . '/misc_functions.inc.php';
62
 
66
 
63
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) }
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) }
64
 
68
 
65
// Valid algorithms for vts_password_hash():
69
// Valid algorithms for vts_password_hash():
66
define('PASSWORD_STD_DES',   'std_des');       // Algorithm from crypt()
70
define('PASSWORD_STD_DES',   'std-des');       // Algorithm from crypt()
67
define('PASSWORD_EXT_DES',   'ext_des');       // Algorithm from crypt()
71
define('PASSWORD_EXT_DES',   'ext-des');       // Algorithm from crypt()
68
define('PASSWORD_MD5',       'md5');           // Algorithm from crypt()
72
define('PASSWORD_MD5',       'md5');           // Algorithm from crypt()
69
define('PASSWORD_BLOWFISH',  'blowfish');      // Algorithm from crypt()
73
define('PASSWORD_BLOWFISH',  'blowfish');      // Algorithm from crypt()
70
define('PASSWORD_SHA256',    'sha256');        // Algorithm from crypt()
74
define('PASSWORD_SHA256',    'sha256');        // Algorithm from crypt()
71
define('PASSWORD_SHA512',    'sha512');        // Algorithm from crypt()
75
define('PASSWORD_SHA512',    'sha512');        // Algorithm from crypt()
72
define('PASSWORD_VTS_MCF1',  OID_MCF_VTS_V1);  // Algorithm from ViaThinkSoft
76
define('PASSWORD_VTS_MCF1',  OID_MCF_VTS_V1);  // Algorithm by ViaThinkSoft
73
// Other valid values (already defined in PHP):
77
// Other valid values (already defined in PHP):
74
// - PASSWORD_DEFAULT
78
// - PASSWORD_DEFAULT
75
// - PASSWORD_BCRYPT
79
// - PASSWORD_BCRYPT
76
// - PASSWORD_ARGON2I
80
// - PASSWORD_ARGON2I
77
// - PASSWORD_ARGON2ID
81
// - PASSWORD_ARGON2ID
78
 
82
 
-
 
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
 
79
// --- Part 1: Modular Crypt Format encode/decode
97
// --- Part 1: Modular Crypt Format encode/decode
80
 
98
 
81
function crypt_modular_format_encode($id, $bin_salt, $bin_hash, $params=null) {
99
function crypt_modular_format_encode($id, $bin_salt, $bin_hash, $params=null) {
82
        // $<id>[$<param>=<value>(,<param>=<value>)*][$<salt>[$<hash>]]
100
        // $<id>[$<param>=<value>(,<param>=<value>)*][$<salt>[$<hash>]]
83
        $out = '$'.$id;
101
        $out = '$'.$id;
Line 137... Line 155...
137
        } else {
155
        } else {
138
                return '0';
156
                return '0';
139
        }
157
        }
140
}
158
}
141
 
159
 
-
 
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
 
142
function vts_crypt_hash($algo, $str_password, $str_salt, $ver='1', $mode='ps', $iterations=0/*default*/) {
182
function vts_crypt_hash($algo, $str_password, $str_salt, $ver='1', $mode=PASSWORD_VTS_MCF1_DEFAULT_MODE, $iterations=PASSWORD_VTS_MCF1_DEFAULT_ITERATIONS) {
143
        if ($ver == '1') {
183
        if ($ver == '1') {
144
                if ($mode == 'sp') {
184
                if ($mode == PASSWORD_VTS_MCF1_MODE_SP) {
145
                        $payload = $str_salt.$str_password;
185
                        $payload = $str_salt.$str_password;
146
                        if (!hash_supported_natively($algo) && str_starts_with($algo, 'sha3-') && method_exists('\bb\Sha3\Sha3', 'hash')) {
186
                        if (!hash_supported_natively($algo) && str_starts_with($algo, 'sha3-') && method_exists('\bb\Sha3\Sha3', 'hash')) {
147
                                $bits = explode('-',$algo)[1];
187
                                $bits = explode('-',$algo)[1];
148
                                $bin_hash = \bb\Sha3\Sha3::hash($payload, $bits, true);
188
                                $bin_hash = \bb\Sha3\Sha3::hash($payload, $bits, true);
149
                        } else {
189
                        } else {
150
                                $bin_hash = hash($algo, $payload, true);
190
                                $bin_hash = hash($algo, $payload, true);
151
                        }
191
                        }
152
                } else if ($mode == 'ps') {
192
                } else if ($mode == PASSWORD_VTS_MCF1_MODE_PS) {
153
                        $payload = $str_password.$str_salt;
193
                        $payload = $str_password.$str_salt;
154
                        if (!hash_supported_natively($algo) && str_starts_with($algo, 'sha3-') && method_exists('\bb\Sha3\Sha3', 'hash')) {
194
                        if (!hash_supported_natively($algo) && str_starts_with($algo, 'sha3-') && method_exists('\bb\Sha3\Sha3', 'hash')) {
155
                                $bits = explode('-',$algo)[1];
195
                                $bits = explode('-',$algo)[1];
156
                                $bin_hash = \bb\Sha3\Sha3::hash($payload, $bits, true);
196
                                $bin_hash = \bb\Sha3\Sha3::hash($payload, $bits, true);
157
                        } else {
197
                        } else {
158
                                $bin_hash = hash($algo, $payload, true);
198
                                $bin_hash = hash($algo, $payload, true);
159
                        }
199
                        }
160
                } else if ($mode == 'sps') {
200
                } else if ($mode == PASSWORD_VTS_MCF1_MODE_SPS) {
161
                        $payload = $str_salt.$str_password.$str_salt;
201
                        $payload = $str_salt.$str_password.$str_salt;
162
                        if (!hash_supported_natively($algo) && str_starts_with($algo, 'sha3-') && method_exists('\bb\Sha3\Sha3', 'hash')) {
202
                        if (!hash_supported_natively($algo) && str_starts_with($algo, 'sha3-') && method_exists('\bb\Sha3\Sha3', 'hash')) {
163
                                $bits = explode('-',$algo)[1];
203
                                $bits = explode('-',$algo)[1];
164
                                $bin_hash = \bb\Sha3\Sha3::hash($payload, $bits, true);
204
                                $bin_hash = \bb\Sha3\Sha3::hash($payload, $bits, true);
165
                        } else {
205
                        } else {
166
                                $bin_hash = hash($algo, $payload, true);
206
                                $bin_hash = hash($algo, $payload, true);
167
                        }
207
                        }
168
                } else if ($mode == 'hmac') {
208
                } else if ($mode == PASSWORD_VTS_MCF1_MODE_HMAC) {
169
                        if (!hash_hmac_supported_natively($algo) && str_starts_with($algo, 'sha3-') && method_exists('\bb\Sha3\Sha3', 'hash_hmac')) {
209
                        if (!hash_hmac_supported_natively($algo) && str_starts_with($algo, 'sha3-') && method_exists('\bb\Sha3\Sha3', 'hash_hmac')) {
170
                                $bits = explode('-',$algo)[1];
210
                                $bits = explode('-',$algo)[1];
171
                                $bin_hash = \bb\Sha3\Sha3::hash_hmac($str_password, $str_salt, $bits, true);
211
                                $bin_hash = \bb\Sha3\Sha3::hash_hmac($str_password, $str_salt, $bits, true);
172
                        } else {
212
                        } else {
173
                                $bin_hash = hash_hmac($algo, $str_password, $str_salt, true);
213
                                $bin_hash = hash_hmac($algo, $str_password, $str_salt, true);
174
                        }
214
                        }
175
                } else if ($mode == 'pbkdf2') {
215
                } else if ($mode == PASSWORD_VTS_MCF1_MODE_PBKDF2) {
176
                        if (!hash_pbkdf2_supported_natively($algo) && str_starts_with($algo, 'sha3-') && method_exists('\bb\Sha3\Sha3', 'hash_pbkdf2')) {
216
                        if (!hash_pbkdf2_supported_natively($algo) && str_starts_with($algo, 'sha3-') && method_exists('\bb\Sha3\Sha3', 'hash_pbkdf2')) {
177
                                if ($iterations == 0) {
217
                                if ($iterations == 0/*default*/) {
178
                                        $iterations = 100; // because the userland implementation is EXTREMELY slow, we must choose a small value, sorry...
218
                                        $iterations = _default_iterations($algo, true);
179
                                }
219
                                }
180
                                $bits = explode('-',$algo)[1];
220
                                $bits = explode('-',$algo)[1];
181
                                $bin_hash = \bb\Sha3\Sha3::hash_pbkdf2($str_password, $str_salt, $iterations, $bits, 0, true);
221
                                $bin_hash = \bb\Sha3\Sha3::hash_pbkdf2($str_password, $str_salt, $iterations, $bits, 0, true);
182
                        } else {
222
                        } else {
183
                                if ($iterations == 0) {
223
                                if ($iterations == 0/*default*/) {
184
                                        // Recommendations taken from https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2
-
 
185
                                        // Note that hash_pbkdf2() implements PBKDF2-HMAC-*
-
 
186
                                        if      ($algo == 'sha3-512')    $iterations =  100000;
-
 
187
                                        else if ($algo == 'sha3-384')    $iterations =  100000;
-
 
188
                                        else if ($algo == 'sha3-256')    $iterations =  100000;
-
 
189
                                        else if ($algo == 'sha3-224')    $iterations =  100000;
-
 
190
                                        else if ($algo == 'sha512')      $iterations =  210000; // value by owasp.org cheatcheat (28 February 2023)
-
 
191
                                        else if ($algo == 'sha512/256')  $iterations =  210000; // value by owasp.org cheatcheat (28 February 2023)
-
 
192
                                        else if ($algo == 'sha512/224')  $iterations =  210000; // value by owasp.org cheatcheat (28 February 2023)
-
 
193
                                        else if ($algo == 'sha384')      $iterations =  600000;
-
 
194
                                        else if ($algo == 'sha256')      $iterations =  600000; // value by owasp.org cheatcheat (28 February 2023)
-
 
195
                                        else if ($algo == 'sha224')      $iterations =  600000;
-
 
196
                                        else if ($algo == 'sha1')        $iterations = 1300000; // value by owasp.org cheatcheat (28 February 2023)
-
 
197
                                        else if ($algo == 'md5')         $iterations = 5000000;
224
                                        $iterations = _default_iterations($algo, false);
198
                                        else                             $iterations =    5000;
-
 
199
                                }
225
                                }
200
                                $bin_hash = hash_pbkdf2($algo, $str_password, $str_salt, $iterations, 0, true);
226
                                $bin_hash = hash_pbkdf2($algo, $str_password, $str_salt, $iterations, 0, true);
201
                        }
227
                        }
202
                } else {
228
                } else {
203
                        throw new Exception("Invalid VTS crypt version 1 mode. Expect sp, ps, sps, hmac, or pbkdf2.");
229
                        throw new Exception("Invalid VTS crypt version 1 mode. Expect sp, ps, sps, hmac, or pbkdf2.");
204
                }
230
                }
205
                $bin_salt = $str_salt;
231
                $bin_salt = $str_salt;
206
                $params = array();
232
                $params = array();
207
                $params['a'] = $algo;
233
                $params['a'] = $algo;
208
                $params['m'] = $mode;
234
                $params['m'] = $mode;
209
                if ($mode == 'pbkdf2') $params['i'] = $iterations;
235
                if ($mode == PASSWORD_VTS_MCF1_MODE_PBKDF2) $params['i'] = $iterations;
210
                return crypt_modular_format_encode(OID_MCF_VTS_V1, $bin_salt, $bin_hash, $params);
236
                return crypt_modular_format_encode(OID_MCF_VTS_V1, $bin_salt, $bin_hash, $params);
211
        } else {
237
        } else {
212
                throw new Exception("Invalid VTS crypt version, expect 1.");
238
                throw new Exception("Invalid VTS crypt version, expect 1.");
213
        }
239
        }
214
}
240
}
Line 228... Line 254...
228
                $algo = $params['a'];
254
                $algo = $params['a'];
229
 
255
 
230
                if (!isset($params['m'])) throw new Exception('Param "m" (mode) missing');
256
                if (!isset($params['m'])) throw new Exception('Param "m" (mode) missing');
231
                $mode = $params['m'];
257
                $mode = $params['m'];
232
 
258
 
233
                if ($mode == 'pbkdf2') {
259
                if ($mode == PASSWORD_VTS_MCF1_MODE_PBKDF2) {
234
                        if (!isset($params['i'])) throw new Exception('Param "i" (iterations) missing');
260
                        if (!isset($params['i'])) throw new Exception('Param "i" (iterations) missing');
235
                        $iterations = $params['i'];
261
                        $iterations = $params['i'];
236
                } else {
262
                } else {
237
                        $iterations = 0;
263
                        $iterations = 0;
238
                }
264
                }
Line 247... Line 273...
247
        } else {
273
        } else {
248
                throw new Exception("Invalid VTS crypt version, expect 1.");
274
                throw new Exception("Invalid VTS crypt version, expect 1.");
249
        }
275
        }
250
}
276
}
251
 
277
 
252
// --- Part 3: vts_password_hash() and vts_password_verify()
278
// --- Part 3: Replacement of vts_password_*() functions
253
 
279
 
-
 
280
/**
254
/** This function extends password_verify() by adding ViaThinkSoft Modular Crypt Format 1.0.
281
 * This function replaces password_algos() by extending it with
255
 * @param string $password to be checked
-
 
256
 * @param string $hash Hash created by crypt(), password_hash(), or vts_password_hash().
282
 * password hashes that are implemented in vts_password_hash().
257
 * @return bool true if password is valid
283
 * @return array of hashes that can be used in vts_password_hash().
258
 */
284
 */
-
 
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
 */
259
function vts_password_verify($password, $hash): bool {
303
function vts_password_get_info($hash) {
260
        if (vts_crypt_version($hash) != '0') {
304
        if (vts_crypt_version($hash) == '1') {
-
 
305
                // OID_MCF_VTS_V1
261
                // Hash created by vts_password_hash(), or vts_crypt_hash()
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');
-
 
318
                        $options['iterations'] = $mcf['params']['i'];
-
 
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(
-
 
341
                                "iterations" => base64_int_decode(substr($hash,1,4))
-
 
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(
-
 
360
                                "cost" => explode('$',$hash)[2]
-
 
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(
-
 
369
                                'rounds' => str_replace('rounds=','',explode('$',$hash)[2])
-
 
370
                        )
-
 
371
                );
262
                return vts_crypt_verify($password, $hash);
372
        } else if (str_starts_with($hash, '$6$')) {
-
 
373
                // PASSWORD_SHA512
-
 
374
                return array(
-
 
375
                        "algo" => PASSWORD_SHA512,
-
 
376
                        "algoName" => "sha512",
-
 
377
                        "options" => array(
-
 
378
                                'rounds' => str_replace('rounds=','',explode('$',$hash)[2])
-
 
379
                        )
-
 
380
                );
263
        } else {
381
        } else {
-
 
382
                // PASSWORD_DEFAULT
-
 
383
                // PASSWORD_BCRYPT
-
 
384
                // PASSWORD_ARGON2I
264
                // Hash created by vts_password_hash(), password_hash(), or crypt()
385
                // PASSWORD_ARGON2ID
265
                return password_verify($password, $hash);
386
                return password_get_info($hash);
266
        }
387
        }
267
}
388
}
268
 
389
 
269
/** This function extends password_hash() with the algorithms supported by crypt().
390
/** This function extends password_hash() with the algorithms supported by crypt().
270
 * It also adds vts_crypt_hash() which implements the ViaThinkSoft Modular Crypt Format 1.0.
391
 * It also adds vts_crypt_hash() which implements the ViaThinkSoft Modular Crypt Format 1.0.
Line 273... Line 394...
273
 * @param mixed $algo algorithm
394
 * @param mixed $algo algorithm
274
 * @param array $options options for the hashing algorithm
395
 * @param array $options options for the hashing algorithm
275
 * @return string Crypt style password hash
396
 * @return string Crypt style password hash
276
 */
397
 */
277
function vts_password_hash($password, $algo, $options=array()): string {
398
function vts_password_hash($password, $algo, $options=array()): string {
-
 
399
        $options = vts_password_fill_default_options($algo, $options);
-
 
400
 
278
        $crypt_salt = null;
401
        $crypt_salt = null;
279
        if (($algo === PASSWORD_STD_DES) && defined('CRYPT_STD_DES')) {
402
        if (($algo === PASSWORD_STD_DES) && defined('CRYPT_STD_DES')) {
280
                // 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.
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.
281
                $crypt_salt = des_compat_salt(2);
404
                $crypt_salt = des_compat_salt(2);
282
        } else if (($algo === PASSWORD_EXT_DES) && defined('CRYPT_EXT_DES')) {
405
        } else if (($algo === PASSWORD_EXT_DES) && defined('CRYPT_EXT_DES')) {
283
                // 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.
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.
284
                $iterations = isset($options['iterations']) ? $options['iterations'] : 725;
407
                $iterations = $options['iterations'];
285
                $crypt_salt = '_' . base64_int_encode($iterations) . des_compat_salt(4);
408
                $crypt_salt = '_' . base64_int_encode($iterations,4) . des_compat_salt(4);
286
        } else if (($algo === PASSWORD_MD5) && defined('CRYPT_MD5')) {
409
        } else if (($algo === PASSWORD_MD5) && defined('CRYPT_MD5')) {
287
                // MD5 hashing with a twelve character salt starting with $1$
410
                // MD5 hashing with a twelve character salt starting with $1$
288
                $crypt_salt = '$1$'.des_compat_salt(12).'$';
411
                $crypt_salt = '$1$'.des_compat_salt(12).'$';
289
        } else if (($algo === PASSWORD_BLOWFISH) && defined('CRYPT_BLOWFISH')) {
412
        } else if (($algo === PASSWORD_BLOWFISH) && defined('CRYPT_BLOWFISH')) {
290
                // 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.
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.
291
                $algo = '$2y$'; // most secure
414
                $algo = '$2y$'; // most secure
292
                $cost = isset($options['cost']) ? $options['cost'] : 10;
415
                $cost = $options['cost'];
293
                $crypt_salt = $algo.str_pad($cost,2,'0',STR_PAD_LEFT).'$'.des_compat_salt(22).'$';
416
                $crypt_salt = $algo.str_pad($cost,2,'0',STR_PAD_LEFT).'$'.des_compat_salt(22).'$';
294
        } else if (($algo === PASSWORD_SHA256) && defined('CRYPT_SHA256')) {
417
        } else if (($algo === PASSWORD_SHA256) && defined('CRYPT_SHA256')) {
295
                // 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.
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.
296
                $algo = '$5$';
419
                $algo = '$5$';
297
                $rounds = isset($options['rounds']) ? $options['rounds'] : 5000;
420
                $rounds = $options['rounds'];
298
                $crypt_salt = $algo.'rounds='.$rounds.'$'.des_compat_salt(16).'$';
421
                $crypt_salt = $algo.'rounds='.$rounds.'$'.des_compat_salt(16).'$';
299
        } else if (($algo === PASSWORD_SHA512) && defined('CRYPT_SHA512')) {
422
        } else if (($algo === PASSWORD_SHA512) && defined('CRYPT_SHA512')) {
300
                // 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.
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.
301
                $algo = '$6$';
424
                $algo = '$6$';
302
                $rounds = isset($options['rounds']) ? $options['rounds'] : 5000;
425
                $rounds = $options['rounds'];
303
                $crypt_salt = $algo.'rounds='.$rounds.'$'.des_compat_salt(16).'$';
426
                $crypt_salt = $algo.'rounds='.$rounds.'$'.des_compat_salt(16).'$';
304
        }
427
        }
305
 
428
 
306
        if (!is_null($crypt_salt)) {
429
        if (!is_null($crypt_salt)) {
307
                // Algorithms: PASSWORD_STD_DES
430
                // Algorithms: PASSWORD_STD_DES
Line 314... Line 437...
314
                if (strlen($out) < 13) throw new Exception("crypt() failed");
437
                if (strlen($out) < 13) throw new Exception("crypt() failed");
315
                return $out;
438
                return $out;
316
        } else if ($algo === PASSWORD_VTS_MCF1) {
439
        } else if ($algo === PASSWORD_VTS_MCF1) {
317
                // Algorithms: PASSWORD_VTS_MCF1
440
                // Algorithms: PASSWORD_VTS_MCF1
318
                $ver  = '1';
441
                $ver  = '1';
319
                $algo = isset($options['algo']) ? $options['algo'] : 'sha3-512';
442
                $algo = $options['algo'];
320
                $mode = isset($options['mode']) ? $options['mode'] : 'ps';
443
                $mode = $options['mode'];
321
                $iterations = isset($options['iterations']) ? $options['iterations'] : 0/*default*/;
444
                $iterations = $options['iterations'];
322
                $salt_len = isset($options['salt_length']) ? $options['salt_length'] : 50;
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()
323
                $salt = random_bytes_ex($salt_len, true, true);
446
                $salt = random_bytes_ex($salt_len, true, true);
324
                return vts_crypt_hash($algo, $password, $salt, $ver, $mode, $iterations);
447
                return vts_crypt_hash($algo, $password, $salt, $ver, $mode, $iterations);
325
        } else {
448
        } else {
326
                // Algorithms: PASSWORD_DEFAULT
449
                // Algorithms: PASSWORD_DEFAULT
327
                //             PASSWORD_BCRYPT
450
                //             PASSWORD_BCRYPT
Line 329... Line 452...
329
                //             PASSWORD_ARGON2ID
452
                //             PASSWORD_ARGON2ID
330
                return password_hash($password, $algo, $options);
453
                return password_hash($password, $algo, $options);
331
        }
454
        }
332
}
455
}
333
 
456
 
-
 
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
 
334
// --- Part 4: Useful functions required by the crypt-functions
513
// --- Part 4: Useful functions required by the crypt-functions
335
 
514
 
336
define('BASE64_RFC4648_ALPHABET', '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/');
515
define('BASE64_RFC4648_ALPHABET', '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/');
337
define('BASE64_CRYPT_ALPHABET',   './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz');
516
define('BASE64_CRYPT_ALPHABET',   './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz');
338
 
517
 
Line 345... Line 524...
345
                $salt .= $characters[ord($bytes[$i]) % strlen($characters)];
524
                $salt .= $characters[ord($bytes[$i]) % strlen($characters)];
346
        }
525
        }
347
        return $salt;
526
        return $salt;
348
}
527
}
349
 
528
 
350
function base64_int_encode($num) {
529
function base64_int_encode($num, $len) {
351
        // https://stackoverflow.com/questions/15534982/which-iteration-rules-apply-on-crypt-using-crypt-ext-des
530
        // https://stackoverflow.com/questions/15534982/which-iteration-rules-apply-on-crypt-using-crypt-ext-des
352
        $alphabet_raw = BASE64_CRYPT_ALPHABET;
531
        $alphabet_raw = BASE64_CRYPT_ALPHABET;
353
        $alphabet = str_split($alphabet_raw);
532
        $alphabet = str_split($alphabet_raw);
354
        $arr = array();
533
        $arr = array();
355
        $base = sizeof($alphabet);
534
        $base = sizeof($alphabet);
Line 357... Line 536...
357
                $rem = $num % $base;
536
                $rem = $num % $base;
358
                $num = (int)($num / $base);
537
                $num = (int)($num / $base);
359
                $arr[] = $alphabet[$rem];
538
                $arr[] = $alphabet[$rem];
360
        }
539
        }
361
        $string = implode($arr);
540
        $string = implode($arr);
362
        return str_pad($string, 4, '.', STR_PAD_RIGHT);
541
        return str_pad($string, $len, '.', STR_PAD_RIGHT);
-
 
542
}
-
 
543
 
-
 
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;
363
}
550
}
364
 
551
 
365
function crypt_radix64_encode($str) {
552
function crypt_radix64_encode($str) {
366
        $x = $str;
553
        $x = $str;
367
        $x = base64_encode($x);
554
        $x = base64_encode($x);
Line 397... Line 584...
397
 
584
 
398
function hash_pbkdf2_supported_natively($algo) {
585
function hash_pbkdf2_supported_natively($algo) {
399
        return hash_supported_natively($algo);
586
        return hash_supported_natively($algo);
400
}
587
}
401
 
588
 
-
 
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
 
402
// --- Part 5: Selftest
628
// --- Part 5: Selftest
403
 
629
 
-
 
630
for ($i=0; $i<9999; $i++) {
-
 
631
        assert($i===base64_int_decode(base64_int_encode($i,4)));
404
/*
632
}
-
 
633
 
405
$rnd = random_bytes_ex(50, true, true);
634
$rnd = random_bytes_ex(50, true, true);
406
assert(crypt_radix64_decode(crypt_radix64_encode($rnd)) === $rnd);
635
assert(crypt_radix64_decode(crypt_radix64_encode($rnd)) === $rnd);
407
 
636
 
408
$password = random_bytes_ex(20, false, true);
637
$password = random_bytes_ex(20, false, true);
-
 
638
 
409
assert(vts_password_verify($password,vts_password_hash($password, PASSWORD_STD_DES)));
639
assert(vts_password_verify($password,$dummy = vts_password_hash($password, PASSWORD_STD_DES)));
-
 
640
//echo "'$dummy' ".strlen($dummy)."\n";
-
 
641
//var_dump(vts_password_get_info($dummy));
-
 
642
 
410
assert(vts_password_verify($password,vts_password_hash($password, PASSWORD_EXT_DES)));
643
assert(vts_password_verify($password,$dummy = vts_password_hash($password, PASSWORD_EXT_DES)));
-
 
644
//echo "'$dummy' ".strlen($dummy)."\n";
-
 
645
//var_dump(vts_password_get_info($dummy));
-
 
646
 
411
assert(vts_password_verify($password,vts_password_hash($password, PASSWORD_MD5)));
647
assert(vts_password_verify($password,$dummy = vts_password_hash($password, PASSWORD_MD5)));
-
 
648
//echo "'$dummy' ".strlen($dummy)."\n";
-
 
649
//var_dump(vts_password_get_info($dummy));
-
 
650
 
412
assert(vts_password_verify($password,vts_password_hash($password, PASSWORD_BLOWFISH)));
651
assert(vts_password_verify($password,$dummy = vts_password_hash($password, PASSWORD_BLOWFISH)));
-
 
652
//echo "'$dummy' ".strlen($dummy)."\n";
-
 
653
//var_dump(vts_password_get_info($dummy));
-
 
654
 
413
assert(vts_password_verify($password,vts_password_hash($password, PASSWORD_SHA256)));
655
assert(vts_password_verify($password,$dummy = vts_password_hash($password, PASSWORD_SHA256)));
-
 
656
//echo "'$dummy' ".strlen($dummy)."\n";
-
 
657
//var_dump(vts_password_get_info($dummy));
-
 
658
 
414
assert(vts_password_verify($password,vts_password_hash($password, PASSWORD_SHA512)));
659
assert(vts_password_verify($password,$dummy = vts_password_hash($password, PASSWORD_SHA512)));
-
 
660
//echo "'$dummy' ".strlen($dummy)."\n";
-
 
661
//var_dump(vts_password_get_info($dummy));
-
 
662
 
415
assert(vts_password_verify($password,$debug = vts_password_hash($password, PASSWORD_VTS_MCF1, array(
663
assert(vts_password_verify($password,$dummy = vts_password_hash($password, PASSWORD_VTS_MCF1, array(
416
        'algo' => 'sha3-512',
664
        'algo' => 'sha3-512',
417
        'mode' => 'pbkdf2',
665
        'mode' => 'pbkdf2',
418
        'iterations' => 5000
666
        'iterations' => 0
419
))));
667
))));
420
echo "$debug\n";
668
//echo "'$dummy' ".strlen($dummy)."\n";
421
assert(vts_password_verify($password,vts_password_hash($password, PASSWORD_DEFAULT)));
669
//var_dump(vts_password_get_info($dummy));
422
assert(vts_password_verify($password,vts_password_hash($password, PASSWORD_BCRYPT)));
670
assert(false===vts_password_needs_rehash($dummy,PASSWORD_VTS_MCF1,array(
-
 
671
        'salt_length' => 51,
-
 
672
        'algo' => 'sha3-512',
423
if (defined('PASSWORD_ARGON2I'))
673
        'mode' => 'pbkdf2',
-
 
674
        'iterations' => 0
-
 
675
)));
424
        assert(vts_password_verify($password,vts_password_hash($password, PASSWORD_ARGON2I)));
676
assert(true===vts_password_needs_rehash($dummy,PASSWORD_VTS_MCF1,array(
-
 
677
        'salt_length' => 50,
-
 
678
        'algo' => 'sha3-256',
425
if (defined('PASSWORD_ARGON2ID'))
679
        'mode' => 'pbkdf2',
426
        assert(vts_password_verify($password,vts_password_hash($password, PASSWORD_ARGON2ID)));
680
        'iterations' => 0
-
 
681
)));
-
 
682
 
427
echo "OK, Password $password\n";
683
echo "OK, password $password\n";
428
*/
-