diff options
| author | Geo Halkiadakis <gchalkiadakis@sklavenitis.co.gr> | 2023-04-20 17:10:06 +0300 |
|---|---|---|
| committer | Geo Halkiadakis <gchalkiadakis@sklavenitis.co.gr> | 2023-04-20 17:10:06 +0300 |
| commit | d9bce23822117d03b1630f002d3ee0491ac83d4b (patch) | |
| tree | a7a2ac4864043f72ba1a54c2b98de5fd13daca67 | |
| parent | 423505cdf54c52947482509329c2a1c8ece97c39 (diff) | |
| download | classroom-d9bce23822117d03b1630f002d3ee0491ac83d4b.tar.gz classroom-d9bce23822117d03b1630f002d3ee0491ac83d4b.tar.bz2 classroom-d9bce23822117d03b1630f002d3ee0491ac83d4b.zip | |
hashing passwords; compatibility with dotNet
| -rw-r--r-- | public/app/routes/backend.php | 14 | ||||
| -rw-r--r-- | public/app/views/admin/admin-menu.php | 10 | ||||
| -rw-r--r-- | public/app/views/admin/edit_lesson.php (renamed from public/app/views/admin/new_lesson.php) | 16 | ||||
| -rw-r--r-- | public/app/views/admin/skeleton.php | 28 | ||||
| -rw-r--r-- | public/assets/css/overides.css | 13 | ||||
| -rw-r--r-- | public/assets/js/admin/edit_lesson.js | 0 | ||||
| -rw-r--r-- | tests/algo-hashes.php | 52 | ||||
| -rw-r--r-- | tests/dotnet-pass.php | 503 | ||||
| -rw-r--r-- | tests/register.php | 43 |
9 files changed, 613 insertions, 66 deletions
diff --git a/public/app/routes/backend.php b/public/app/routes/backend.php index 5356407..88bf4b8 100644 --- a/public/app/routes/backend.php +++ b/public/app/routes/backend.php @@ -88,11 +88,21 @@ Route::add('/admin/api/privileges', function () { // new lesson // ----------------------------------------------------------------------------- -Route::add('/admin/new_lesson', function () { +Route::add('/admin/edit_lesson', function () { Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles Render::view('admin/skeleton', [ 'title' => 'Νέο Μάθημα', - 'action' => 'new_lesson' + 'action' => 'edit_lesson', ]); }); +// new lesson +// ----------------------------------------------------------------------------- +Route::add('/admin/edit_lesson/([0-9]*)', function ($id) { + Auth::allowRoles([1, 2, 3]); // allow few admin-panel roles + Render::view('admin/skeleton', [ + 'title' => 'Νέο Μάθημα', + 'action' => 'edit_lesson', + 'id' => intval($id) + ]); +}); diff --git a/public/app/views/admin/admin-menu.php b/public/app/views/admin/admin-menu.php index d7e7b09..a06d2eb 100644 --- a/public/app/views/admin/admin-menu.php +++ b/public/app/views/admin/admin-menu.php @@ -4,26 +4,26 @@ * * imported variables: * --- - * @param $action (string) : array of articles + * @param $action (string): array of articles + * @param $id (int): id of entity (when editing entity) | 0: new entity * * example call: * --- * Render::template("sections/admin_panel.php",[ * 'action' => 'posts', + * 'id' => 4 * ]) * ----------------------------------------------------------------------------- */ - $admin_options = [ - 'new_lesson' => 'Νέο Μάθημα', + 'edit_lesson' => 'Νέο Μάθημα', 'lessons' => 'Μαθήματα', 'categories' => 'Κατηγορίες', 'pages' => 'Σελίδες', 'privileges' => 'Πρόσβαση', 'users' => 'Χρήστες' ]; - ?> <div class="top-bar"> @@ -32,7 +32,7 @@ $admin_options = [ <?php foreach($admin_options as $opt => $label) : ?> - <?php if ($opt == $action) : ?> + <?php if (($opt == $action) & (!$id)) : ?> <span class="btn btn-primary selected"><?=$label?></span> <?php else : ?> diff --git a/public/app/views/admin/new_lesson.php b/public/app/views/admin/edit_lesson.php index c48fc82..30dd42f 100644 --- a/public/app/views/admin/new_lesson.php +++ b/public/app/views/admin/edit_lesson.php @@ -1,11 +1,19 @@ <?php -// if no (lesson-)id is specified, then id=0 (new lesson) -if (!isset($id)) $id=0; + // some labels (if editing new or existing record) + // --- -- -- - - - + if ($id == 0) { + $action_title = "Δημιουργία Μαθήματος"; + $action_button = "Δημιουργία"; + + } else { + $action_title = "Επεξεργασία Μαθήματος"; + $action_button = "Αποθήκευση Αλλαγών"; + } ?> <!-- title bar --> <div class="title-bar"> - <div><h5>Επεξεργασία Μαθήματος</h5></div> + <div><h5><?=$action_title?></h5></div> <div> <button type="button" class="btn pull-right" data-toggle="modal" data-target="#editorModal" data-action="preview"> @@ -129,7 +137,7 @@ if (!isset($id)) $id=0; <div class="col-4"> <button class="btn btn-primary col-sm-12" type="submit"> - Καταχώριση + <?=$action_button?> </button> </div> diff --git a/public/app/views/admin/skeleton.php b/public/app/views/admin/skeleton.php index fea346f..927bd2d 100644 --- a/public/app/views/admin/skeleton.php +++ b/public/app/views/admin/skeleton.php @@ -7,7 +7,9 @@ <?php // header includes //////////////////////////////////////////////////////////////////////// - Render::view('components/header_includes', ['administration' => true]); + Render::view('components/header_includes', //////////////////////////// + ['administration' => true] + ); ?> @@ -17,16 +19,26 @@ <div class="classroom admin-container"> <?php - Render::view('admin/admin-menu',[ - 'action' => $action - ]) - ?> - - <?php - Render::view("admin/{$action}"); + // sometimes an ID (int) is needed + // ex. editing a page or a lesson we need page-ID or lesson-ID + // --- -- -- - - - + if (!isset($id)) $id=0; + + // render menu + //////////////////////////////////////////////////////////////////// + Render::view('admin/admin-menu',[ //////////////////////////// + 'action' => $action, + 'id' => $id + ]); + + // render main content + //////////////////////////////////////////////////////////////////// + Render::view("admin/{$action}",[ 'id' => $id ]); //////////////// ?> </div> + <!-- js admin script --> + <script>var entity_id = <?= $id ?>;</script> <script src="/assets/js/admin/<?= $action ?>.js"></script> </body> diff --git a/public/assets/css/overides.css b/public/assets/css/overides.css index 0709f4d..b217f65 100644 --- a/public/assets/css/overides.css +++ b/public/assets/css/overides.css @@ -85,9 +85,8 @@ ul.select2-results__options li { font-size: 14px; padding: 4px 8px; } /* hack select2 caret styling * (like bootstrap5 select caret) - * ----------------------------------------------------------------------------- + * ----------------------------------------------------------------------- start */ - .select2-container--default .select2-selection--single .select2-selection__arrow b { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"); background-color: transparent; @@ -101,5 +100,11 @@ ul.select2-results__options li { font-size: 14px; padding: 4px 8px; } height: 14px; } .select2-container--default .select2-selection--single .select2-selection__arrow { width: 24px; } -.select2-container--default .select2-selection--single { height: 31px; } -
\ No newline at end of file +.select2-container--default .select2-selection--single { height: 31px; } +/* ------------------------------------------------------------------------- end +*/ + +.admin-container .edit-post textarea[name=body] { height: calc(100vh - 420px); } /* almost all avialable height */ +.admin-container .edit-post textarea[name=intro] { height: 73px ;} /* almost 3 (half-)lines */ + + diff --git a/public/assets/js/admin/edit_lesson.js b/public/assets/js/admin/edit_lesson.js new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/public/assets/js/admin/edit_lesson.js diff --git a/tests/algo-hashes.php b/tests/algo-hashes.php new file mode 100644 index 0000000..8457949 --- /dev/null +++ b/tests/algo-hashes.php @@ -0,0 +1,52 @@ +<?php +$userManager = new UserManager(); + +$password = $userManager->cryptPassword('102030!!!'); + +$user = (new User()) + ->setUserName('geo@roptron.gr') + ->setPassword($password) + ->setRoles([1]); + +$userManager->createUserToken($user); + + +$algos = [ + [ PASSWORD_DEFAULT, 10 ], + [ PASSWORD_ARGON2I, 10 ], + [ CRYPT_BLOWFISH, 10 ], + [ PASSWORD_DEFAULT, 9 ], + [ PASSWORD_ARGON2I, 9 ], + [ CRYPT_BLOWFISH, 9 ], + [ PASSWORD_DEFAULT, 11 ], + [ PASSWORD_ARGON2I, 11 ], + [ CRYPT_BLOWFISH, 11 ], + [ PASSWORD_DEFAULT, 12 ], + [ PASSWORD_ARGON2I, 12 ], + [ CRYPT_BLOWFISH, 12 ], + [ PASSWORD_DEFAULT, 8 ], + [ PASSWORD_ARGON2I, 8 ], + [ CRYPT_BLOWFISH, 8 ], + [ PASSWORD_DEFAULT, 7 ], + [ PASSWORD_ARGON2I, 7 ], + [ CRYPT_BLOWFISH, 7] +] +?> +<html> + <head> + <style>pre{ font: 400 14px/18px 'JetBrains Mono NL', 'Ununtu Mono', Consolas, Monaco; }</style> + <body> + <pre> + +<?php +foreach($algos as $algo) { + echo "\n\nalgo= {$algo[0]}, cost={$algo[1]}" + ."\nhash= ". ( $x = password_hash('102030', $algo[0], ['cost' => $algo[1]]) ) + ."\nbase64=". base64_encode($x); +} + +?> + + </pre> + </body> +</html> diff --git a/tests/dotnet-pass.php b/tests/dotnet-pass.php new file mode 100644 index 0000000..eeba77f --- /dev/null +++ b/tests/dotnet-pass.php @@ -0,0 +1,503 @@ +<?php + +/** NOTE: + * ref, original AspNet Code: + * Github: aspnet/Identity + * (path) Identity/src/Microsoft.AspNetCore.Identity/PasswordHasher.cs + * (code) https://github.com/aspnet/Identity/blob/4ef80fabf66624b464e77ac9dd6e8c4461759e0c/src/Microsoft.AspNetCore.Identity/PasswordHasher.cs + * + * credits: + * https://stackoverflow.com/questions/46753050/reading-asp-hashed-password-in-php (answer by: ZerosAndOnes) + */ + + + +trait DotNetHasherChecker +{ + /** Verify_AspNet_HashedPassword() + * + * Check the given plain password against an already hashed one + * using DotNetHasher + * + * @param string $value + * @param string $hashedValue + * @param array $options + * @return bool + */ + public function Verify_AspNet_HashedPassword($value, $hashedValue, array $options = []) + { + echo "\nhash= ". $hashedValue; + + /** original code + * + byte[] decodedHashedPassword = Convert.FromBase64String(hashedPassword); + // read the format marker from the hashed password + if (decodedHashedPassword.Length == 0) + { + return PasswordVerificationResult.Failed; + } + switch (decodedHashedPassword[0]) + * + */ + + if (strlen($hashedValue) === 0) { + return false; + } + + $hash = base64_decode($hashedValue); + + $version = ord($hash[0]); // get first byte of first hash's char + // shall be 0 -or- 1; TODO: die if not + + // case 0x00: + // VerifyHashedPasswordV2() + if ($version === 0) { + + + /** version V2 + * + algo is sha1 + * + has fixed iterations, subKeyLength, salt size + * --- + * PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations. + * (See also: SDL crypto guidelines v5.1, Part III) + * Format: { 0x00, salt, subkey } + */ + + echo "\nhash is V2"; + + // no need to decode anything else ///////////////////////////////// + + // Format: { 0x00, salt, subkey } + $iterations = 1000; // default for Rfc2898DeriveBytes + $subKeyLength = 32; // 256 bits (/8 = 32bytes) + $saltSize = 16; // = 128 bits (/8 = 16bytes) + $salt = substr($hash, 1, $saltSize); + + echo "\nsalt= ". base64_encode($salt); + + echo "\nalgo= sha1"; + + $derived = hash_pbkdf2('sha1', $value, $salt, $iterations, $subKeyLength, true); + $newHash = chr(0x00) . $salt . $derived; + + /** oririnal AspNet code: + * + * + private static bool VerifyHashedPasswordV2(byte[] hashedPassword, string password) + { + const KeyDerivationPrf Pbkdf2Prf = KeyDerivationPrf.HMACSHA1; // default for Rfc2898DeriveBytes + const int Pbkdf2IterCount = 1000; // default for Rfc2898DeriveBytes + const int Pbkdf2SubkeyLength = 256 / 8; // 256 bits + const int SaltSize = 128 / 8; // 128 bits + + // We know ahead of time the exact length of a valid hashed password payload. + if (hashedPassword.Length != 1 + SaltSize + Pbkdf2SubkeyLength) + { + return false; // bad size + } + + byte[] salt = new byte[SaltSize]; + Buffer.BlockCopy(hashedPassword, 1, salt, 0, salt.Length); + + byte[] expectedSubkey = new byte[Pbkdf2SubkeyLength]; + Buffer.BlockCopy(hashedPassword, 1 + salt.Length, expectedSubkey, 0, expectedSubkey.Length); + + // Hash the incoming password and verify it + byte[] actualSubkey = KeyDerivation.Pbkdf2(password, salt, Pbkdf2Prf, Pbkdf2IterCount, Pbkdf2SubkeyLength); + return ByteArraysEqual(actualSubkey, expectedSubkey); + } + * + */ + + + + } else + + // case 0x01: + // VerifyHashedPasswordV3() + if ($version === 1) { + + /** version V3 + * + algo is any of sha1, sha256, sha512 + * + various options (iterations, subKeyLength, salt size); + * + options are pack(-ed) into final hash + * --- + * PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations. + * Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey } + * (All UInt32s are stored big-endian.) + */ + + echo "\nhash is V3"; + + // Read header information (decode options) + + // Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey } + $unp = unpack('N3', substr($hash, 1, 12)); + $prf = $unp[1]; + + echo "\nunp= "; print_r($unp); + + $algos = [ 'sha1', 'sha256', 'sha512' ]; // 0=sha1 1=sha256 2=sha512 + $algorithm = $algos[$prf]; // shall be [0-2]; TODO: die if not + $iterations = $unp[2]; + $saltLength = $unp[3]; // should be > 128 bits; TODO: die if not + $subKeyLength = 32; + + echo "\nalgo= ". $algorithm; + + $salt = substr($hash, 13, $saltLength); + // calculate derived + $derived = hash_pbkdf2($algorithm, $value, $salt, $iterations, $subKeyLength, true); + // pack all parts to construct the $newHash (to be compaired against $hash) + $newHash = chr(0x01) . pack('N3', $prf, $iterations, $saltLength) . $salt . $derived; + + + } // TODO: else { ERROR on version ; die() } + + + + echo "\nnewHash= ". base64_encode($newHash); + + return $hash === $newHash; + } + + +} + + +class SomeUserClass +{ + use DotNetHasherChecker; +} + + +?> +<html> + <head> + <style>pre{ font: 400 16px/24px 'JetBrains Mono NL', 'Ununtu Mono', Consolas, Monaco; }</style> + <body> + <pre> + + +<?php + $h = new SomeUserClass(); + + $result = $h->Verify_AspNet_HashedPassword( + '3212', + 'AMHM+lMKiSdmucVv8KhQ9yyF/sVj8Pay16HJkBDORJxvHDm0WFP2vww9DBVo3JTEoA==' + ); + + echo "\nresult= ". json_encode([ 'match' => $result ]); +?> + + + </pre> + </body> +</html> + + +<?php + +/** RAW + * + * /src/Microsoft.AspNetCore.Identity/PasswordHasher.cs + * ... +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; +using Microsoft.AspNetCore.Cryptography.KeyDerivation; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Identity +{ + /// <summary> + /// Implements the standard Identity password hashing. + /// </summary> + /// <typeparam name="TUser">The type used to represent a user.</typeparam> + public class PasswordHasher<TUser> : IPasswordHasher<TUser> where TUser : class + { + // ======================= + // HASHED PASSWORD FORMATS + // ======================= + // + // Version 2: + // PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations. + // (See also: SDL crypto guidelines v5.1, Part III) + // Format: { 0x00, salt, subkey } + // + // Version 3: + // PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations. + // Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey } + // (All UInt32s are stored big-endian.) + /// + + private readonly PasswordHasherCompatibilityMode _compatibilityMode; + private readonly int _iterCount; + private readonly RandomNumberGenerator _rng; + + /// <summary> + /// Creates a new instance of <see cref="PasswordHasher{TUser}"/>. + /// </summary> + /// <param name="optionsAccessor">The options for this instance.</param> + public PasswordHasher(IOptions<PasswordHasherOptions> optionsAccessor = null) + { + var options = optionsAccessor?.Value ?? new PasswordHasherOptions(); + + _compatibilityMode = options.CompatibilityMode; + switch (_compatibilityMode) + { + case PasswordHasherCompatibilityMode.IdentityV2: + // nothing else to do + break; + + case PasswordHasherCompatibilityMode.IdentityV3: + _iterCount = options.IterationCount; + if (_iterCount < 1) + { + throw new InvalidOperationException(Resources.InvalidPasswordHasherIterationCount); + } + break; + + default: + throw new InvalidOperationException(Resources.InvalidPasswordHasherCompatibilityMode); + } + + _rng = options.Rng; + } + + // Compares two byte arrays for equality. The method is specifically written so that the loop is not optimized. + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + private static bool ByteArraysEqual(byte[] a, byte[] b) + { + if (a == null && b == null) + { + return true; + } + if (a == null || b == null || a.Length != b.Length) + { + return false; + } + var areSame = true; + for (var i = 0; i < a.Length; i++) + { + areSame &= (a[i] == b[i]); + } + return areSame; + } + + /// <summary> + /// Returns a hashed representation of the supplied <paramref name="password"/> for the specified <paramref name="user"/>. + /// </summary> + /// <param name="user">The user whose password is to be hashed.</param> + /// <param name="password">The password to hash.</param> + /// <returns>A hashed representation of the supplied <paramref name="password"/> for the specified <paramref name="user"/>.</returns> + public virtual string HashPassword(TUser user, string password) + { + if (password == null) + { + throw new ArgumentNullException(nameof(password)); + } + + if (_compatibilityMode == PasswordHasherCompatibilityMode.IdentityV2) + { + return Convert.ToBase64String(HashPasswordV2(password, _rng)); + } + else + { + return Convert.ToBase64String(HashPasswordV3(password, _rng)); + } + } + + private static byte[] HashPasswordV2(string password, RandomNumberGenerator rng) + { + const KeyDerivationPrf Pbkdf2Prf = KeyDerivationPrf.HMACSHA1; // default for Rfc2898DeriveBytes + const int Pbkdf2IterCount = 1000; // default for Rfc2898DeriveBytes + const int Pbkdf2SubkeyLength = 256 / 8; // 256 bits + const int SaltSize = 128 / 8; // 128 bits + + // Produce a version 2 (see comment above) text hash. + byte[] salt = new byte[SaltSize]; + rng.GetBytes(salt); + byte[] subkey = KeyDerivation.Pbkdf2(password, salt, Pbkdf2Prf, Pbkdf2IterCount, Pbkdf2SubkeyLength); + + var outputBytes = new byte[1 + SaltSize + Pbkdf2SubkeyLength]; + outputBytes[0] = 0x00; // format marker + Buffer.BlockCopy(salt, 0, outputBytes, 1, SaltSize); + Buffer.BlockCopy(subkey, 0, outputBytes, 1 + SaltSize, Pbkdf2SubkeyLength); + return outputBytes; + } + + private byte[] HashPasswordV3(string password, RandomNumberGenerator rng) + { + return HashPasswordV3(password, rng, + prf: KeyDerivationPrf.HMACSHA256, + iterCount: _iterCount, + saltSize: 128 / 8, + numBytesRequested: 256 / 8); + } + + private static byte[] HashPasswordV3(string password, RandomNumberGenerator rng, KeyDerivationPrf prf, int iterCount, int saltSize, int numBytesRequested) + { + // Produce a version 3 (see comment above) text hash. + byte[] salt = new byte[saltSize]; + rng.GetBytes(salt); + byte[] subkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, numBytesRequested); + + var outputBytes = new byte[13 + salt.Length + subkey.Length]; + outputBytes[0] = 0x01; // format marker + WriteNetworkByteOrder(outputBytes, 1, (uint)prf); + WriteNetworkByteOrder(outputBytes, 5, (uint)iterCount); + WriteNetworkByteOrder(outputBytes, 9, (uint)saltSize); + Buffer.BlockCopy(salt, 0, outputBytes, 13, salt.Length); + Buffer.BlockCopy(subkey, 0, outputBytes, 13 + saltSize, subkey.Length); + return outputBytes; + } + + private static uint ReadNetworkByteOrder(byte[] buffer, int offset) + { + return ((uint)(buffer[offset + 0]) << 24) + | ((uint)(buffer[offset + 1]) << 16) + | ((uint)(buffer[offset + 2]) << 8) + | ((uint)(buffer[offset + 3])); + } + + /// <summary> + /// Returns a <see cref="PasswordVerificationResult"/> indicating the result of a password hash comparison. + /// </summary> + /// <param name="user">The user whose password should be verified.</param> + /// <param name="hashedPassword">The hash value for a user's stored password.</param> + /// <param name="providedPassword">The password supplied for comparison.</param> + /// <returns>A <see cref="PasswordVerificationResult"/> indicating the result of a password hash comparison.</returns> + /// <remarks>Implementations of this method should be time consistent.</remarks> + public virtual PasswordVerificationResult VerifyHashedPassword(TUser user, string hashedPassword, string providedPassword) + { + if (hashedPassword == null) + { + throw new ArgumentNullException(nameof(hashedPassword)); + } + if (providedPassword == null) + { + throw new ArgumentNullException(nameof(providedPassword)); + } + + byte[] decodedHashedPassword = Convert.FromBase64String(hashedPassword); + + // read the format marker from the hashed password + if (decodedHashedPassword.Length == 0) + { + return PasswordVerificationResult.Failed; + } + switch (decodedHashedPassword[0]) + { + case 0x00: + if (VerifyHashedPasswordV2(decodedHashedPassword, providedPassword)) + { + // This is an old password hash format - the caller needs to rehash if we're not running in an older compat mode. + return (_compatibilityMode == PasswordHasherCompatibilityMode.IdentityV3) + ? PasswordVerificationResult.SuccessRehashNeeded + : PasswordVerificationResult.Success; + } + else + { + return PasswordVerificationResult.Failed; + } + + case 0x01: + int embeddedIterCount; + if (VerifyHashedPasswordV3(decodedHashedPassword, providedPassword, out embeddedIterCount)) + { + // If this hasher was configured with a higher iteration count, change the entry now. + return (embeddedIterCount < _iterCount) + ? PasswordVerificationResult.SuccessRehashNeeded + : PasswordVerificationResult.Success; + } + else + { + return PasswordVerificationResult.Failed; + } + + default: + return PasswordVerificationResult.Failed; // unknown format marker + } + } + + private static bool VerifyHashedPasswordV2(byte[] hashedPassword, string password) + { + const KeyDerivationPrf Pbkdf2Prf = KeyDerivationPrf.HMACSHA1; // default for Rfc2898DeriveBytes + const int Pbkdf2IterCount = 1000; // default for Rfc2898DeriveBytes + const int Pbkdf2SubkeyLength = 256 / 8; // 256 bits + const int SaltSize = 128 / 8; // 128 bits + + // We know ahead of time the exact length of a valid hashed password payload. + if (hashedPassword.Length != 1 + SaltSize + Pbkdf2SubkeyLength) + { + return false; // bad size + } + + byte[] salt = new byte[SaltSize]; + Buffer.BlockCopy(hashedPassword, 1, salt, 0, salt.Length); + + byte[] expectedSubkey = new byte[Pbkdf2SubkeyLength]; + Buffer.BlockCopy(hashedPassword, 1 + salt.Length, expectedSubkey, 0, expectedSubkey.Length); + + // Hash the incoming password and verify it + byte[] actualSubkey = KeyDerivation.Pbkdf2(password, salt, Pbkdf2Prf, Pbkdf2IterCount, Pbkdf2SubkeyLength); + return ByteArraysEqual(actualSubkey, expectedSubkey); + } + + private static bool VerifyHashedPasswordV3(byte[] hashedPassword, string password, out int iterCount) + { + iterCount = default(int); + + try + { + // Read header information + KeyDerivationPrf prf = (KeyDerivationPrf)ReadNetworkByteOrder(hashedPassword, 1); + iterCount = (int)ReadNetworkByteOrder(hashedPassword, 5); + int saltLength = (int)ReadNetworkByteOrder(hashedPassword, 9); + + // Read the salt: must be >= 128 bits + if (saltLength < 128 / 8) + { + return false; + } + byte[] salt = new byte[saltLength]; + Buffer.BlockCopy(hashedPassword, 13, salt, 0, salt.Length); + + // Read the subkey (the rest of the payload): must be >= 128 bits + int subkeyLength = hashedPassword.Length - 13 - salt.Length; + if (subkeyLength < 128 / 8) + { + return false; + } + byte[] expectedSubkey = new byte[subkeyLength]; + Buffer.BlockCopy(hashedPassword, 13 + salt.Length, expectedSubkey, 0, expectedSubkey.Length); + + // Hash the incoming password and verify it + byte[] actualSubkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, subkeyLength); + return ByteArraysEqual(actualSubkey, expectedSubkey); + } + catch + { + // This should never occur except in the case of a malformed payload, where + // we might go off the end of the array. Regardless, a malformed payload + // implies verification failed. + return false; + } + } + + private static void WriteNetworkByteOrder(byte[] buffer, int offset, uint value) + { + buffer[offset + 0] = (byte)(value >> 24); + buffer[offset + 1] = (byte)(value >> 16); + buffer[offset + 2] = (byte)(value >> 8); + buffer[offset + 3] = (byte)(value >> 0); + } + } +} + +--- +*/
\ No newline at end of file diff --git a/tests/register.php b/tests/register.php deleted file mode 100644 index 7f778af..0000000 --- a/tests/register.php +++ /dev/null @@ -1,43 +0,0 @@ -<?php -$userManager = new UserManager(); - -$password = $userManager->cryptPassword('102030!!!'); - -$user = (new User()) - ->setUserName('geo@roptron.gr') - ->setPassword($password) - ->setRoles([1]); - -$userManager->createUserToken($user); - - -$algos = [ - password_hash('root1234', PASSWORD_DEFAULT, ['cost' => 10 ]), - password_hash('root1234', PASSWORD_ARGON2I, ['cost' => 10 ]), - password_hash('root1234', CRYPT_BLOWFISH, ['cost' => 10 ]), - password_hash('root1234', PASSWORD_DEFAULT, ['cost' => 9 ]), - password_hash('root1234', PASSWORD_ARGON2I, ['cost' => 9 ]), - password_hash('root1234', CRYPT_BLOWFISH, ['cost' => 9 ]), - password_hash('root1234', PASSWORD_DEFAULT, ['cost' => 11 ]), - password_hash('root1234', PASSWORD_ARGON2I, ['cost' => 11 ]), - password_hash('root1234', CRYPT_BLOWFISH, ['cost' => 11 ]), - password_hash('root1234', PASSWORD_DEFAULT, ['cost' => 12 ]), - password_hash('root1234', PASSWORD_ARGON2I, ['cost' => 12 ]), - password_hash('root1234', CRYPT_BLOWFISH, ['cost' => 12 ]), - password_hash('root1234', PASSWORD_DEFAULT, ['cost' => 8 ]), - password_hash('root1234', PASSWORD_ARGON2I, ['cost' => 8 ]), - password_hash('root1234', CRYPT_BLOWFISH, ['cost' => 8 ]), - password_hash('root1234', PASSWORD_DEFAULT, ['cost' => 7 ]), - password_hash('root1234', PASSWORD_ARGON2I, ['cost' => 7 ]), - password_hash('root1234', CRYPT_BLOWFISH, ['cost' => 7 ]) - -] -?><pre><?php - -foreach($algos as $a) { - echo $a ."\n"; -} - - - -?></pre>
\ No newline at end of file |
