summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--public/app/routes/backend.php14
-rw-r--r--public/app/views/admin/admin-menu.php10
-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.php28
-rw-r--r--public/assets/css/overides.css13
-rw-r--r--public/assets/js/admin/edit_lesson.js0
-rw-r--r--tests/algo-hashes.php52
-rw-r--r--tests/dotnet-pass.php503
-rw-r--r--tests/register.php43
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