diff --git a/app/Auth/NexusWebGuard.php b/app/Auth/NexusWebGuard.php index 9eaa940a..43ac553e 100644 --- a/app/Auth/NexusWebGuard.php +++ b/app/Auth/NexusWebGuard.php @@ -46,9 +46,11 @@ class NexusWebGuard implements StatefulGuard } $credentials = $this->request->cookie(); if ($this->validate($credentials)) { - $user = $this->user; + $user = $this->provider->retrieveByCredentials($credentials); if ($this->provider->validateCredentials($user, $credentials)) { - return $user; + if ($user->checkIsNormal()) { + return $this->user = $user; + } } } } @@ -62,29 +64,13 @@ class NexusWebGuard implements StatefulGuard */ public function validate(array $credentials = []) { - $required = ['c_secure_pass', 'c_secure_uid', 'c_secure_login']; + $required = ['c_secure_pass']; foreach ($required as $value) { if (empty($credentials[$value])) { return false; } } - $b_id = base64($credentials["c_secure_uid"],false); - $id = intval($b_id ?? 0); - if (!$id || !is_valid_id($id) || strlen($credentials["c_secure_pass"]) != 32) { - return false; - } - $user = $this->provider->retrieveById($id); - if (!$user) { - return false; - } - try { - $user->checkIsNormal(); - $this->user = $user; - return true; - } catch (\Throwable $e) { - do_log($e->getMessage()); - return false; - } + return true; } public function logout() diff --git a/app/Auth/NexusWebUserProvider.php b/app/Auth/NexusWebUserProvider.php index 203c3e71..2805da2f 100644 --- a/app/Auth/NexusWebUserProvider.php +++ b/app/Auth/NexusWebUserProvider.php @@ -61,10 +61,15 @@ class NexusWebUserProvider implements UserProvider */ public function retrieveByCredentials(array $credentials) { - if (!empty($credentials['c_secure_uid'])) { - $b_id = base64($credentials["c_secure_uid"],false); - return $this->query->find($b_id); + list($tokenJson, $signature) = explode('.', base64_decode($credentials["c_secure_pass"])); + if (empty($tokenJson) || empty($signature)) { + return null; } + $tokenData = json_decode($tokenJson, true); + if (!isset($tokenData['user_id'])) { + return null; + } + return $this->retrieveById($tokenData['user_id']); } /** @@ -76,20 +81,9 @@ class NexusWebUserProvider implements UserProvider */ public function validateCredentials(Authenticatable $user, array $credentials) { - if ($credentials["c_secure_login"] == base64("yeah")) { - /** - * Not IP related - * @since 1.8.0 - */ - if ($credentials["c_secure_pass"] != md5($user->passhash)) { - return false; - } - } else { - if ($credentials["c_secure_pass"] !== md5($user->passhash)) { - return false; - } - } - return true; + list($tokenJson, $signature) = explode('.', base64_decode($credentials["c_secure_pass"])); + $expectedSignature = hash_hmac('sha256', $tokenJson, $user->auth_key); + return hash_equals($expectedSignature, $signature); } public function rehashPasswordIfRequired(Authenticatable $user, #[\SensitiveParameter] array $credentials, bool $force = false) diff --git a/app/Console/Commands/Test.php b/app/Console/Commands/Test.php index a20927d4..9cfb8cee 100644 --- a/app/Console/Commands/Test.php +++ b/app/Console/Commands/Test.php @@ -49,9 +49,8 @@ class Test extends Command */ public function handle() { - $r = config('nexus.ammds_secret'); -// $r = SearchBox::query()->find(4)->ss()->orWhere("mode", 0)->get(); - dd($r); + $result = getLogFile(); + dd($result); } } diff --git a/app/Http/Controllers/AuthenticateController.php b/app/Http/Controllers/AuthenticateController.php index 577a4dfa..638d560a 100644 --- a/app/Http/Controllers/AuthenticateController.php +++ b/app/Http/Controllers/AuthenticateController.php @@ -16,6 +16,7 @@ use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Cookie; use Illuminate\Validation\Rule; +use Nexus\Database\NexusDB; class AuthenticateController extends Controller { @@ -53,7 +54,7 @@ class AuthenticateController extends Controller { $deadline = Setting::get('security.login_secret_deadline'); if ($deadline && $deadline > now()->toDateTimeString()) { - $user = User::query()->where('passkey', $passkey)->first(['id', 'passhash']); + $user = User::query()->where('passkey', $passkey)->first(['id', 'passhash', 'secret', 'auth_key']); if ($user) { $ip = getip(); /** @@ -61,9 +62,10 @@ class AuthenticateController extends Controller * @since 1.8.0 */ // $passhash = md5($user->passhash . $ip); - $passhash = md5($user->passhash); - do_log(sprintf('passhash: %s, ip: %s, md5: %s', $user->passhash, $ip, $passhash)); - logincookie($user->id, $passhash,false, get_setting('system.cookie_valid_days', 365) * 86400, true, true, true); +// $passhash = md5($user->passhash); +// do_log(sprintf('passhash: %s, ip: %s, md5: %s', $user->passhash, $ip, $passhash)); +// logincookie($user->id, $passhash,false, get_setting('system.cookie_valid_days', 365) * 86400, true, true, true); + logincookie($user->id, $user->auth_key); $user->last_login = now(); $user->save(); $userRep = new UserRepository(); @@ -126,6 +128,29 @@ class AuthenticateController extends Controller } } + public function challenge(Request $request) + { + try { + $request->validate([ + 'username' => 'required|string', + ]); + $username = $request->username; + $challenge = mksecret(); + NexusDB::cache_put(get_challenge_key($username), $challenge,300); + $user = User::query()->where("username", $username)->first(['secret']); + return $this->success([ + "challenge" => $challenge, + 'secret' => $user->secret ?? mksecret(), + ]); + } catch (\Exception $exception) { + $msg = $exception->getMessage(); + $params = $request->all(); + do_log(sprintf("challenge fail: %s, params: %s", $msg, nexus_json_encode($params))); + return $this->fail($params, $msg); + } + } + + diff --git a/app/Models/Setting.php b/app/Models/Setting.php index de238063..c2b849a8 100644 --- a/app/Models/Setting.php +++ b/app/Models/Setting.php @@ -105,4 +105,9 @@ class Setting extends NexusModel return self::get("main.defaultlang"); } + public static function getIsUseChallengeResponseAuthentication(): bool + { + return self::get("security.use_challenge_response_authentication") == "yes"; + } + } diff --git a/app/Models/User.php b/app/Models/User.php index 4dc9daab..89e08b43 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -194,7 +194,7 @@ class User extends Authenticatable implements FilamentUser, HasName * @var array */ protected $hidden = [ - 'secret', 'passhash', 'passkey' + 'secret', 'passhash', 'passkey', 'auth_key' ]; /** diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 23ac8011..3448477d 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -53,8 +53,9 @@ class AuthServiceProvider extends ServiceProvider */ public function boot() { + //some plugin use this guard Auth::viaRequest('nexus-cookie', function (Request $request) { - return $this->getUserByCookie($request->cookie()); + return get_user_from_cookie($request->cookie(), false); }); Auth::extend('nexus-web', function ($app, $name, array $config) { @@ -72,33 +73,4 @@ class AuthServiceProvider extends ServiceProvider } - private function getUserByCookie($cookie) - { - if (empty($cookie["c_secure_pass"]) || empty($cookie["c_secure_uid"]) || empty($cookie["c_secure_login"])) { - return null; - } - $b_id = base64($cookie["c_secure_uid"],false); - $id = intval($b_id ?? 0); - if (!$id || !is_valid_id($id) || strlen($cookie["c_secure_pass"]) != 32) { - return null; - } - $user = User::query()->find($id); - if (!$user) { - return null; - } - if ($cookie["c_secure_login"] == base64("yeah")) { - /** - * Not IP related - * @since 1.8.0 - */ - if ($cookie["c_secure_pass"] != md5($user->passhash)) { - return null; - } - } else { - if ($cookie["c_secure_pass"] !== md5($user->passhash)) { - return null; - } - } - return $user; - } } diff --git a/database/migrations/2025_03_29_121708_change_users_table_passhash_field_length.php b/database/migrations/2025_03_29_121708_change_users_table_passhash_field_length.php new file mode 100644 index 00000000..dc3533ea --- /dev/null +++ b/database/migrations/2025_03_29_121708_change_users_table_passhash_field_length.php @@ -0,0 +1,26 @@ +string("passhash", 255)->default("")->nullable(false)->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + } +}; diff --git a/database/migrations/2025_04_03_123951_add_auth_key_to_table_users.php b/database/migrations/2025_04_03_123951_add_auth_key_to_table_users.php new file mode 100644 index 00000000..c2d124b1 --- /dev/null +++ b/database/migrations/2025_04_03_123951_add_auth_key_to_table_users.php @@ -0,0 +1,28 @@ +string("auth_key", 255)->default("")->nullable(false)->after("secret"); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn("auth_key"); + }); + } +}; diff --git a/include/functions.php b/include/functions.php index 40978360..3ed97e84 100644 --- a/include/functions.php +++ b/include/functions.php @@ -1954,7 +1954,6 @@ function dbconn($autoclean = false, $doLogin = true) } function userlogin() { -// do_log("COOKIE:" . json_encode($_COOKIE) . ", uid: " . (isset($_COOKIE['c_secure_uid']) ? base64($_COOKIE["c_secure_uid"],false) : '')); static $loginResult; if (!is_null($loginResult)) { return $loginResult; @@ -1964,7 +1963,6 @@ function userlogin() { global $SITE_ONLINE, $oldip; global $enablesqldebug_tweak, $sqldebug_tweak; unset($GLOBALS["CURUSER"]); - $log = "cookie: " . json_encode($_COOKIE); $ip = getip(); $nip = ip2long($ip); @@ -1979,68 +1977,11 @@ function userlogin() { } } - if (empty($_COOKIE["c_secure_pass"]) || empty($_COOKIE["c_secure_uid"]) || empty($_COOKIE["c_secure_login"])) { - do_log("$log, param not enough"); - return $loginResult = false; - } - if ($_COOKIE["c_secure_login"] == base64("yeah")) - { - //if (empty($_SESSION["s_secure_uid"]) || empty($_SESSION["s_secure_pass"])) - //return; - } - $b_id = base64($_COOKIE["c_secure_uid"],false); - $id = intval($b_id ?? 0); - if (!$id || !is_valid_id($id) || strlen($_COOKIE["c_secure_pass"]) != 32) { - do_log("$log, invalid c_secure_uid"); + $row = get_user_from_cookie($_COOKIE); + if (empty($row)) { return $loginResult = false; } - if ($_COOKIE["c_secure_login"] == base64("yeah")) - { - //if (strlen($_SESSION["s_secure_pass"]) != 32) - //return; - } - - $res = sql_query("SELECT * FROM users WHERE users.id = ".sqlesc($id)." AND users.enabled='yes' AND users.status = 'confirmed' LIMIT 1"); - $row = mysql_fetch_array($res); - if (!$row) { - do_log("$log, c_secure_uid not exists"); - return $loginResult = false; - } - - $sec = hash_pad($row["secret"]); - - //die(base64_decode($_COOKIE["c_secure_login"])); - - if ($_COOKIE["c_secure_login"] == base64("yeah")) - { - /** - * Not IP related - * @since 1.8.0 - */ -// $md5 = md5($row["passhash"].$ip); - $md5 = md5($row["passhash"]); - $log .= ", secure login == yeah, passhash: {$row['passhash']}, ip: $ip, md5: $md5"; - if ($_COOKIE["c_secure_pass"] != $md5) { - do_log("$log, c_secure_pass != md5"); - return $loginResult = false; - } - } - else - { - $md5 = md5($row["passhash"]); - $log .= "$log, passhash: {$row['passhash']}, md5: $md5"; - if ($_COOKIE["c_secure_pass"] !== $md5) { - do_log("$log, c_secure_pass != md5"); - return $loginResult = false; - } - } - - if ($_COOKIE["c_secure_login"] == base64("yeah")) - { - //if ($_SESSION["s_secure_pass"] !== md5($row["passhash"].$_SERVER["REMOTE_ADDR"])) - //return; - } if (!$row["passkey"]){ $passkey = md5($row['username'].date("Y-m-d H:i:s").$row['passhash']); sql_query("UPDATE users SET passkey = ".sqlesc($passkey)." WHERE id=" . sqlesc($row["id"])); @@ -3001,10 +2942,11 @@ function genbark($x,$y) { } function mksecret($len = 20) { - $ret = ""; - for ($i = 0; $i < $len; $i++) - $ret .= chr(mt_rand(100, 120)); - return $ret; +// $ret = ""; +// for ($i = 0; $i < $len; $i++) +// $ret .= chr(mt_rand(100, 120)); +// return $ret; + return bin2hex(random_bytes($len)); } function httperr($code = 404) { @@ -3013,30 +2955,23 @@ function httperr($code = 404) { exit(); } -function logincookie($id, $passhash, $updatedb = 1, $expires = 0x7fffffff, $securelogin=false, $ssl=false, $trackerssl=false) +function logincookie($id, $authKey, $duration = 0) { - if ($expires != 0x7fffffff) - $expires = time()+$expires; - - setcookie("c_secure_uid", base64($id), $expires, "/", "", false, true); - setcookie("c_secure_pass", $passhash, $expires, "/", "", false, true); - if($ssl) - setcookie("c_secure_ssl", base64("yeah"), $expires, "/", "", false, true); - else - setcookie("c_secure_ssl", base64("nope"), $expires, "/", "", false, true); - - if($trackerssl) - setcookie("c_secure_tracker_ssl", base64("yeah"), $expires, "/", "", false, true); - else - setcookie("c_secure_tracker_ssl", base64("nope"), $expires, "/", "", false, true); - - if ($securelogin) - setcookie("c_secure_login", base64("yeah"), $expires, "/", "", false, true); - else - setcookie("c_secure_login", base64("nope"), $expires, "/", "", false, true); - - - if ($updatedb) + if (empty($authKey)) { + throw new \RuntimeException("user secret or auth_key is empty"); + } + if ($duration <= 0) { + $duration = get_setting('system.cookie_valid_days', 365) * 86400; + } + $expires = time() + $duration; + $tokenData = [ + 'user_id' => $id, + 'expires' => $expires, + ]; + $tokenJson = json_encode($tokenData); + $signature = hash_hmac('sha256', $tokenJson, $authKey); + $authToken = base64_encode($tokenJson . '.' . $signature); + setcookie("c_secure_pass", $authToken, $expires, "/", "", true, true); sql_query("UPDATE users SET last_login = NOW(), lang=" . sqlesc(get_langid_from_langcookie()) . " WHERE id = ".sqlesc($id)); } @@ -3089,11 +3024,11 @@ function make_folder($pre, $folder_name) } function logoutcookie() { - setcookie("c_secure_uid", "", 0x7fffffff, "/", "", false, true); - setcookie("c_secure_pass", "", 0x7fffffff, "/", "", false, true); +// setcookie("c_secure_uid", "", 0x7fffffff, "/", "", false, true); + setcookie("c_secure_pass", "", 0x7fffffff, "/", "", true, true); // setcookie("c_secure_ssl", "", 0x7fffffff, "/", "", false, true); - setcookie("c_secure_tracker_ssl", "", 0x7fffffff, "/", "", false, true); - setcookie("c_secure_login", "", 0x7fffffff, "/", "", false, true); +// setcookie("c_secure_tracker_ssl", "", 0x7fffffff, "/", "", false, true); +// setcookie("c_secure_login", "", 0x7fffffff, "/", "", false, true); // setcookie("c_lang_folder", "", 0x7fffffff, "/", "", false, true); } diff --git a/include/globalfunctions.php b/include/globalfunctions.php index 6d885c3e..34b24376 100644 --- a/include/globalfunctions.php +++ b/include/globalfunctions.php @@ -1370,3 +1370,159 @@ function send_admin_fail_notification(string $msg = ""): void { function ability(\App\Enums\Permission\PermissionEnum $permission): string { return sprintf("ability:%s", $permission->value); } + +function get_challenge_key(string $challenge): string { + return "challenge:".$challenge; +} + +function get_user_from_cookie(array $cookie, $isArray = true): array|\App\Models\User|null { + $log = "cookie: " . json_encode($cookie); + if (empty($_COOKIE["c_secure_pass"])) { + do_log("$log, param not enough"); + return null; + } + list($tokenJson, $signature) = explode('.', base64_decode($_COOKIE["c_secure_pass"])); + if (empty($tokenJson) || empty($signature)) { + do_log("$log, no tokenJson or signature"); + return null; + } + $tokenData = json_decode($tokenJson, true); + if (!isset($tokenData['user_id'])) { + do_log("$log, no user_id"); + return null; + } + if (!isset($tokenData['expires']) || $tokenData['expires'] < time()) { + do_log("$log, signature expired"); + return null; + } + $id = $tokenData['user_id']; + $log .= ", uid = $id"; + + if ($isArray) { + $res = sql_query("SELECT * FROM users WHERE users.id = ".sqlesc($id)." AND users.enabled='yes' AND users.status = 'confirmed' LIMIT 1"); + $row = mysql_fetch_array($res); + if (!$row) { + do_log("$log, user not exists"); + return null; + } + $authKey = $row["auth_key"]; + unset($row['auth_key'], $row['passhash']); + } else { + $row = \App\Models\User::query()->find($id); + if (!$row) { + do_log("$log, user not exists"); + return null; + } + try { + $row->checkIsNormal(); + } catch (\Exception $e) { + do_log("$log, " . $e->getMessage()); + return null; + } + $authKey = $row->auth_key; + } + $expectedSignature = hash_hmac('sha256', $tokenJson, $authKey); + if (!hash_equals($expectedSignature, $signature)) { + do_log("$log, !hash_equals, expectedSignature: $expectedSignature, actualSignature: $signature"); + return null; + } + return $row; +} + +function render_password_hash_js(string $formId, string $passwordOriginalClass, string $passwordHashedName, bool $passwordRequired, string $passwordConfirmClass = "password_confirmation", string $usernameName = "username"): void { + $tipTooShort = nexus_trans('signup.password_too_short'); + $tipTooLong = nexus_trans('signup.password_too_long'); + $tipEqualUsername = nexus_trans('signup.password_equals_username'); + $tipNotMatch = nexus_trans('signup.passwords_unmatched'); + $passwordValidateJS = ""; + if ($passwordRequired) { + $passwordValidateJS = << 40) { + layer.alert("$tipTooLong") + return +} +JS; + } + $formVar = "jqForm" . md5($formId); + $js = << 0 && jqUsername.val() === password) { + layer.alert("$tipEqualUsername") + return + } + if (jqPasswordConfirm.length > 0 && password !== jqPasswordConfirm.val()) { + layer.alert("$tipNotMatch") + return + } + if (password !== "") { + sha256(password).then((passwordHashed) => { + $formVar.find("input[name={$passwordHashedName}]").val(passwordHashed) + $formVar.submit() + }) + } else { + $formVar.submit() + } +}) +JS; + \Nexus\Nexus::js($js, 'footer', false); +} + +function render_password_challenge_js(string $formId, string $usernameName, string $passwordOriginalClass): void { + $formVar = "jqForm" . md5($formId); + $js = << 0 + if (!useChallengeResponseAuthentication) { + return $formVar.submit() + } + let jqUsername = $formVar.find("[name={$usernameName}]") + let jqPassword = $formVar.find(".{$passwordOriginalClass}") + let username = jqUsername.val() + let password = jqPassword.val() + login(username, password, $formVar) +}) +async function login(username, password, jqForm) { + try { + jQuery('body').loading({stoppable: false}); + const challengeResponse = await fetch('/api/challenge', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ username: username }) + }); + jQuery('body').loading('stop'); + + const challengeData = await challengeResponse.json(); + if (challengeData.ret !== 0) { + layer.alert(challengeData.msg) + return + } + + const clientHashedPassword = await sha256(password); + + const serverSideHash = await sha256(challengeData.data.secret + clientHashedPassword); + + const clientResponse = await hmacSha256(challengeData.data.challenge, serverSideHash); + jqForm.find("input[name=response]").val(clientResponse) + jqForm.submit() + } catch (error) { + console.error(error); + layer.alert(error.toString()) + } +} +JS; + \Nexus\Nexus::js("vendor/jquery-loading/jquery.loading.min.js", 'footer', true); + \Nexus\Nexus::js($js, 'footer', false); +} diff --git a/lang/chs/lang_settings.php b/lang/chs/lang_settings.php index cc7d6ae4..b17ba11b 100644 --- a/lang/chs/lang_settings.php +++ b/lang/chs/lang_settings.php @@ -813,6 +813,8 @@ $lang_settings = array 'text_max_price_note' => '默认:1,000,000,设置为 0 无限制', 'row_paid_torrent_enabled' => '是否启用收费种子', 'text_paid_torrent_enabled_note' => '启用后,有权限的用户可以在发种时设置向下载种子的用户收取一定的魔力,默认:否', + 'row_use_challenge_response_authentication' => '挑战-响应认证', + 'text_use_challenge_response_authentication_note' => '如果启用,登录时将不传输明文密码,建议启用。未来版本会删除此配置且启用此功能。', ); ?> diff --git a/lang/cht/lang_settings.php b/lang/cht/lang_settings.php index 773a221c..5b5fa344 100644 --- a/lang/cht/lang_settings.php +++ b/lang/cht/lang_settings.php @@ -813,6 +813,8 @@ $lang_settings = array 'text_max_price_note' => '默認:1,000,000,設置為 0 無限製', 'row_paid_torrent_enabled' => '是否啟用收費種子', 'text_paid_torrent_enabled_note' => '啟用後,有權限的用戶可以在發種時設置向下載種子的用戶收取一定的魔力,默認:否', + 'row_use_challenge_response_authentication' => '挑戰-響應認證', + 'text_use_challenge_response_authentication_note' => '如果啟用,登錄時將不傳輸明文密碼,建議啟用。未來版本會刪除此配置且啟用此功能。', ); ?> diff --git a/lang/en/lang_settings.php b/lang/en/lang_settings.php index 8013f389..6f699c5a 100644 --- a/lang/en/lang_settings.php +++ b/lang/en/lang_settings.php @@ -813,6 +813,8 @@ $lang_settings = array 'text_max_price_note' => 'default: 1,000,000, set to 0 no limit', 'row_paid_torrent_enabled' => 'Whether to enable paid torrents', 'text_paid_torrent_enabled_note' => 'When enabled, users with permission can set a certain amount of bonus to be charged to users who download the torrents when they are sent, default: no', + 'row_use_challenge_response_authentication' => 'Challenge-Response Authentication', + 'text_use_challenge_response_authentication_note' => 'If enabled, no plaintext passwords will be transmitted at login, recommended. Future releases will remove this configuration and enable this feature.' , ); ?> diff --git a/nexus/Nexus.php b/nexus/Nexus.php index 89f7f4be..6c44ede1 100644 --- a/nexus/Nexus.php +++ b/nexus/Nexus.php @@ -291,7 +291,7 @@ final class Nexus public static function trans($key, $replace = [], $locale = null) { if (!IN_NEXUS) { - return trans($key, $replace, $locale ?? Locale::getDefault()); + return trans($key, $replace, $locale ?? get_langfolder_cookie(true)); } if (empty(self::$translations)) { //load from default lang dir @@ -302,7 +302,7 @@ final class Nexus self::loadTranslations($path, $namespace); } } - return self::getTranslation($key, $replace, $locale ?? Locale::getDefault()); + return self::getTranslation($key, $replace, $locale ?? get_langfolder_cookie(true)); } private static function loadTranslations($path, $namespace = null) diff --git a/public/confirm.php b/public/confirm.php index 674592d0..ceb7ff47 100644 --- a/public/confirm.php +++ b/public/confirm.php @@ -9,7 +9,7 @@ if (!$id) dbconn(); -$res = sql_query("SELECT passhash, secret, editsecret, status FROM users WHERE id = ".sqlesc($id)) or sqlerr(__FILE__, __LINE__); +$res = sql_query("SELECT passhash, secret, auth_key, editsecret, status FROM users WHERE id = ".sqlesc($id)) or sqlerr(__FILE__, __LINE__); $row = mysql_fetch_assoc($res); if (!$row) @@ -30,17 +30,18 @@ if (!mysql_affected_rows()) httperr(); -if ($securelogin == "yes") -{ - $securelogin_indentity_cookie = true; - $passh = md5($row["passhash"].$_SERVER["REMOTE_ADDR"]); -} -else // when it's op, default is not use secure login -{ - $securelogin_indentity_cookie = false; - $passh = md5($row["passhash"]); -} -logincookie($id, $passh,1,get_setting('system.cookie_valid_days', 365) * 86400,$securelogin_indentity_cookie); +//if ($securelogin == "yes") +//{ +// $securelogin_indentity_cookie = true; +// $passh = md5($row["passhash"].$_SERVER["REMOTE_ADDR"]); +//} +//else // when it's op, default is not use secure login +//{ +// $securelogin_indentity_cookie = false; +// $passh = md5($row["passhash"]); +//} +//logincookie($id, $passh,1,get_setting('system.cookie_valid_days', 365) * 86400,$securelogin_indentity_cookie); +logincookie($id, $row["auth_key"]); //sessioncookie($row["id"], $passh,false); header("Location: ok.php?type=confirm"); diff --git a/public/js/common.js b/public/js/common.js index 70726393..1af76333 100644 --- a/public/js/common.js +++ b/public/js/common.js @@ -404,3 +404,26 @@ function NewRow(anchor,up){ function DelRow(anchor){ anchor.parentNode.parentNode.parentNode.parentNode.deleteRow(anchor.parentNode.parentNode.rowIndex); } + +// 工具函数:SHA-256哈希 +async function sha256(message) { + const msgBuffer = new TextEncoder().encode(message); + const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); +} + +// 工具函数:HMAC-SHA256 +async function hmacSha256(key, message) { + const encoder = new TextEncoder(); + const keyData = encoder.encode(key); + const messageData = encoder.encode(message); + + const cryptoKey = await crypto.subtle.importKey( + 'raw', keyData, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] + ); + + const signature = await crypto.subtle.sign('HMAC', cryptoKey, messageData); + const hashArray = Array.from(new Uint8Array(signature)); + return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); +} diff --git a/public/login.php b/public/login.php index 5b6e56b9..eb3cdc79 100644 --- a/public/login.php +++ b/public/login.php @@ -48,14 +48,19 @@ if (!empty($_GET["returnto"])) { print("

" . $lang_login['p_error']. " " . $lang_login['p_after_logged_in']. "

\n"); } } +$useChallengeResponseAuthentication = \App\Models\Setting::getIsUseChallengeResponseAuthentication(); +$passwordName = 'class="password"'; +if (!$useChallengeResponseAuthentication) { + $passwordName .= ' name="password"'; +} ?> -
+


[]

- - + + - - - + + +
style="width: 180px; border: 1px solid gray"/>
/>
/>
\n"); - +if (isset($returnto)) { + print("\n"); +} +if ($useChallengeResponseAuthentication) { + print(''); +} ?> +

[]

@@ -108,4 +117,7 @@ print("
".$lang_login['sumbit_shout'].""); } +?> + ".$lang_settings['text_max_ips_note'], 1); tr($lang_settings['row_max_login_attemps']," ".$lang_settings['text_max_login_attemps_note'], 1); + yesorno($lang_settings['row_use_challenge_response_authentication'], 'use_challenge_response_authentication', $SECURITY["use_challenge_response_authentication"], $lang_settings['text_use_challenge_response_authentication_note']); $guestVisitTypeRadio = ''; $guestVisitTypeRadio .= '
'; $guestVisitTypeRadio .= '
'; diff --git a/public/signup.php b/public/signup.php index d686c1bd..cdbd35db 100644 --- a/public/signup.php +++ b/public/signup.php @@ -17,6 +17,7 @@ if ($langid) } } require_once(get_langfile_path("", false, $CURLANGDIR)); +require_once(get_langfile_path("takesignup.php", false, $CURLANGDIR)); cur_user_check (); $type = $_GET['type'] ?? ''; $isPreRegisterEmailAndUsername = get_setting("system.is_invite_pre_email_and_username") == "yes"; @@ -73,7 +74,7 @@ print("
".$lang_signup['text_select_lang']. $s . "

-

+ ");?> - - + @@ -123,8 +124,11 @@ tr($lang_signup['row_school'], "", 1);
> - + +


+

style='height: 25px'>

style='height: 25px'>

+ saveLoginLog($row['id'], $ip, 'Web', true); -if ($row["enabled"] == "no") - bark($lang_takelogin['std_account_disabled']); - -if (isset($_POST["securelogin"]) && $_POST["securelogin"] == "yes") -{ - $securelogin_indentity_cookie = true; - /** - * Not IP related - * @since 1.8.0 - */ -// $passh = md5($row["passhash"].$ip); - $passh = md5($row["passhash"]); - $log .= ", secure login == yeah, passhash: {$row['passhash']}, ip: $ip, md5: $passh"; -} -else -{ - $securelogin_indentity_cookie = false; - $passh = md5($row["passhash"]); - $log .= ", passhash: {$row['passhash']}, md5: $passh"; -} - -if ($securelogin=='yes' || (isset($_POST["ssl"]) && $_POST["ssl"] == "yes")) -{ - $pprefix = "https://"; - $ssl = true; -} -else -{ - $pprefix = "http://"; - $ssl = false; -} -if ($securetracker=='yes' || (isset($_POST["trackerssl"] ) && $_POST["trackerssl"] == "yes")) -{ - $trackerssl = true; -} -else -{ - $trackerssl = false; -} - -do_log($log); - //update user lang $language = \App\Models\Language::query()->where("site_lang_folder", get_langfolder_cookie())->first(); + if ($language && $language->id != $row["lang"]) { do_log(sprintf("update user: %s lang: %s => %s", $row["id"], $row["lang"], $language->id)); - \App\Models\User::query()->where("id", $row["id"])->update(["lang" => $language->id]); + $update["lang"] = $language->id; +} +if (empty($row['auth_key'])) { + $row['auth_key'] = $update['auth_key'] = hash('sha256', mksecret(32)); +} +if (!empty($update)) { + \App\Models\User::query()->where("id", $row["id"])->update($update); clear_user_cache($row["id"]); } if (isset($_POST["logout"]) && $_POST["logout"] == "yes") { - logincookie($row["id"], $passh,1,900,$securelogin_indentity_cookie, $ssl, $trackerssl); - //sessioncookie($row["id"], $passh,true); + logincookie($row["id"], $row['auth_key'],900); } else { - logincookie($row["id"], $passh,1,get_setting('system.cookie_valid_days', 365) * 86400,$securelogin_indentity_cookie, $ssl, $trackerssl); - //sessioncookie($row["id"], $passh,false); + logincookie($row["id"], $row['auth_key']); } if (!empty($_POST["returnto"])) diff --git a/public/takesignup.php b/public/takesignup.php index 4e6067dc..fabce8e2 100644 --- a/public/takesignup.php +++ b/public/takesignup.php @@ -73,7 +73,7 @@ $res = sql_query("SELECT username FROM users WHERE id = $inviter") or sqlerr(__F $arr = mysql_fetch_assoc($res); $invusername = $arr['username']; } -if (!mkglobal("wantusername:wantpassword:passagain:email")) { +if (!mkglobal("wantusername:wantpassword:email")) { die(); } if ($isPreRegisterEmailAndUsername && $type == 'invite' && !empty($inv["pre_register_username"]) && !empty($inv["pre_register_email"])) { @@ -111,17 +111,17 @@ if (empty($wantusername) || empty($wantpassword) || empty($email) || empty($coun if (strlen($wantusername) > 12) bark($lang_takesignup['std_username_too_long']); -if ($wantpassword != $passagain) - bark($lang_takesignup['std_passwords_unmatched']); +//if ($wantpassword != $passagain) +// bark($lang_takesignup['std_passwords_unmatched']); -if (strlen($wantpassword) < 6) - bark($lang_takesignup['std_password_too_short']); - -if (strlen($wantpassword) > 40) - bark($lang_takesignup['std_password_too_long']); - -if ($wantpassword == $wantusername) - bark($lang_takesignup['std_password_equals_username']); +//if (strlen($wantpassword) < 6) +// bark($lang_takesignup['std_password_too_short']); +// +//if (strlen($wantpassword) > 40) +// bark($lang_takesignup['std_password_too_long']); +// +//if ($wantpassword == $wantusername) +// bark($lang_takesignup['std_password_equals_username']); if (!validemail($email)) bark($lang_takesignup['std_wrong_email_address_format']); @@ -148,7 +148,8 @@ $arr = mysql_fetch_row($res); */ $secret = mksecret(); -$wantpasshash = md5($secret . $wantpassword . $secret); +//$wantpasshash = md5($secret . $wantpassword . $secret); +$wantpasshash = hash('sha256', $secret . $wantpassword); $editsecret = ($verification == 'admin' ? '' : $secret); $invite_count = (int) $invite_count; $passkey = md5($wantusername.date("Y-m-d H:i:s").$wantpasshash); @@ -162,13 +163,13 @@ $email = sqlesc($email); $country = sqlesc($country); $gender = sqlesc($gender); $sitelangid = sqlesc(get_langid_from_langcookie()); - +$authKey = sqlesc(mksecret()); $res_check_user = sql_query("SELECT * FROM users WHERE username = " . $wantusername); if(mysql_num_rows($res_check_user) == 1) bark($lang_takesignup['std_username_exists']); -$ret = sql_query("INSERT INTO users (username, passhash, passkey, secret, editsecret, email, country, gender, status, class, invites, ".($type == 'invite' ? "invited_by," : "")." added, last_access, lang, stylesheet".($showschool == 'yes' ? ", school" : "").", uploaded) VALUES (" . $wantusername . "," . $wantpasshash . "," . sqlesc($passkey) . "," . $secret . "," . $editsecret . "," . $email . "," . $country . "," . $gender . ", 'pending', ".$defaultclass_class.",". $invite_count .", ".($type == 'invite' ? "'$inviter'," : "") ." '". date("Y-m-d H:i:s") ."' , " . " '". date("Y-m-d H:i:s") ."' , ".$sitelangid . ",".$defcss.($showschool == 'yes' ? ",".$school : "").",".($iniupload_main > 0 ? $iniupload_main : 0).")") or sqlerr(__FILE__, __LINE__); +$ret = sql_query("INSERT INTO users (username, passhash, passkey, secret, auth_key, editsecret, email, country, gender, status, class, invites, ".($type == 'invite' ? "invited_by," : "")." added, last_access, lang, stylesheet".($showschool == 'yes' ? ", school" : "").", uploaded) VALUES (" . $wantusername . "," . $wantpasshash . "," . sqlesc($passkey) . "," . $secret . "," . $authKey. "," . $editsecret . "," . $email . "," . $country . "," . $gender . ", 'pending', ".$defaultclass_class.",". $invite_count .", ".($type == 'invite' ? "'$inviter'," : "") ." '". date("Y-m-d H:i:s") ."' , " . " '". date("Y-m-d H:i:s") ."' , ".$sitelangid . ",".$defcss.($showschool == 'yes' ? ",".$school : "").",".($iniupload_main > 0 ? $iniupload_main : 0).")") or sqlerr(__FILE__, __LINE__); $id = mysql_insert_id(); fire_event("user_created", \App\Models\User::query()->find($id, \App\Models\User::$commonFields)); $tmpInviteCount = get_setting('main.tmp_invite_count'); diff --git a/public/usercp.php b/public/usercp.php index ff702ad3..ee126b85 100644 --- a/public/usercp.php +++ b/public/usercp.php @@ -3,7 +3,7 @@ require "../include/bittorrent.php"; dbconn(); require_once(get_langfile_path()); loggedinorreturn(); -$userInfo = \App\Models\User::query()->findOrFail($CURUSER["id"], \App\Models\User::$commonFields); +$userInfo = \App\Models\User::query()->findOrFail($CURUSER["id"]); function bark($msg) { stdhead(); global $lang_usercp; @@ -41,12 +41,15 @@ function getimageheight ($imagewidth, $imageheight) } return $imageheight; } -function form($name) { - return print("
"); +function form($name, $type = "save", $id = "") { + if ($id == "") { + $id = "form" . random_str(); + } + return print(""); } -function submit() { +function submit($type = "submit") { global $lang_usercp; - print("".$lang_usercp['row_save_settings'].""."
"); + print("".$lang_usercp['row_save_settings'].""); } function format_tz($a) { @@ -174,11 +177,11 @@ if ($action){ } usercpmenu ("personal"); + form ("personal"); print (""); if ($type == 'saved') print("\n"); - form ("personal"); tr_small($lang_usercp['row_account_parked'], "".$lang_usercp['checkbox_pack_my_account']."
".$lang_usercp['text_account_pack_note']."" ,1); @@ -214,7 +217,7 @@ tr($lang_usercp['row_school'], "", 1); "\">
\n".$lang_usercp['text_avatar_note'].($enablebitbucket_main == 'yes' ? $lang_usercp['text_bitbucket_note'] : ""),1); tr($lang_usercp['row_info'], "
".$lang_usercp['text_info_note'], 1); submit(); - print("
".$lang_usercp['text_saved']."
"); + print(""); stdfoot(); die; break; @@ -398,6 +401,7 @@ tr($lang_usercp['row_school'], "", 1); } stdhead($lang_usercp['head_control_panel'].$lang_usercp['head_tracker_settings']); usercpmenu ("tracker"); + form ("tracker"); $brsectiontype = $browsecatmode; $spsectiontype = $specialcatmode; if ($enablespecial == 'yes' && get_user_class() >= get_setting('authority.view_special_torrent')) @@ -451,7 +455,6 @@ if ($showaudiocodec) $audiocodecs = searchbox_item_list("audiocodecs"); } */ print (""); - form ("tracker"); if ($type == 'saved') print("\n"); if ($emailnotify_smtp=='yes' && $smtptype != 'none') @@ -654,7 +657,7 @@ tr_small($lang_usercp['row_funbox'],"".$lang_usercp['text_comments_reviews'].":
".$lang_usercp['text_show_comment_number'].($showtooltipsetting ? "".$lang_usercp['text_last_comment_on_tooltip'] : ""), 1); submit(); - print("
".$lang_usercp['text_saved']."
"); + print(""); stdfoot(); die; break; @@ -689,8 +692,8 @@ tr_small($lang_usercp['row_funbox'],""); - form ("forum"); if ($type == 'saved') print("".$lang_usercp['text_saved']."\n"); @@ -703,20 +706,26 @@ tr_small($lang_usercp['row_funbox'],"".$lang_usercp['text_go_to_first_page']."".$lang_usercp['text_go_to_last_page'],1); tr_small($lang_usercp['row_forum_signature'], "
".$lang_usercp['text_signature_note'],1); submit(); - print(""); + print(""); stdfoot(); die; break; case "security": if ($type == 'confirm') { - $oldpassword = $_POST['oldpassword']; - if (!$oldpassword){ + $response = $_POST['response']; + if (!$response){ stderr($lang_usercp['std_error'], $lang_usercp['std_enter_old_password'].goback(), 0); - die; - }elseif ($CURUSER["passhash"] != md5($CURUSER["secret"] . $oldpassword . $CURUSER["secret"])){ - stderr($lang_usercp['std_error'], $lang_usercp['std_wrong_password_note'].goback(), 0); - die; - }else + } + //验证旧密码 + $challenge = \Nexus\Database\NexusDB::cache_get(get_challenge_key($userInfo->username)); + if (empty($challenge)) { + stderr($lang_usercp['std_error'], "expired!".goback(), 0); + } + $expectedResponse = hash_hmac('sha256', $userInfo->passhash, $challenge); + if (!hash_equals($expectedResponse, $response)) { + stderr($lang_usercp['std_error'], $lang_usercp['std_wrong_password_note'].goback(), 0); + } + $updateset = array(); $changedemail = 0; $passupdated = 0; @@ -724,7 +733,7 @@ tr_small($lang_usercp['row_funbox']," 40) { - stderr($lang_usercp['std_error'], $lang_usercp['std_password_too_long'].goback("-2"), 0); - die; - } - if (strlen($chpassword) < 6) { - stderr($lang_usercp['std_error'], $lang_usercp['std_password_too_short'].goback("-2"), 0); - die; - } - if ($chpassword != $passagain) { - stderr($lang_usercp['std_error'], $lang_usercp['std_passwords_unmatched'].goback("-2"), 0); - die; - } +// if ($chpassword == $CURUSER["username"]) { +// stderr($lang_usercp['std_error'], $lang_usercp['std_password_equals_username'].goback("-2"), 0); +// die; +// } +// if (strlen($chpassword) > 40) { +// stderr($lang_usercp['std_error'], $lang_usercp['std_password_too_long'].goback("-2"), 0); +// die; +// } +// if (strlen($chpassword) < 6) { +// stderr($lang_usercp['std_error'], $lang_usercp['std_password_too_short'].goback("-2"), 0); +// die; +// } +// if ($chpassword != $passagain) { +// stderr($lang_usercp['std_error'], $lang_usercp['std_passwords_unmatched'].goback("-2"), 0); +// die; +// } $sec = mksecret(); - $passhash = md5($sec . $chpassword . $sec); +// $passhash = md5($sec . $chpassword . $sec); + $passhash = hash('sha256', $sec . $chpassword); $updateset[] = "secret = " . sqlesc($sec); $updateset[] = "passhash = " . sqlesc($passhash); - //die($securelogin . base64_decode($_COOKIE["c_secure_login"])); - if ($_COOKIE["c_secure_login"] == base64("yeah")) - { - $passh = md5($passhash . $_SERVER["REMOTE_ADDR"]); - $securelogin_indentity_cookie = true; - } - else - { - $passh = md5($passhash); - $securelogin_indentity_cookie = false; - } - - if($_COOKIE["c_secure_ssl"] == base64("yeah")) - $ssl = true; - else - $ssl = false; - - logincookie($CURUSER["id"], $passh ,1,get_setting('system.cookie_valid_days', 365) * 86400,$securelogin_indentity_cookie,$ssl); - //sessioncookie($CURUSER["id"], $passh); + logincookie($CURUSER["id"], $userInfo->auth_key); $passupdated = 1; } @@ -864,13 +856,15 @@ EOD; if ($privacyupdated == 1) $to .= "&privacy=1"; clear_user_cache($CURUSER["id"]); + \Nexus\Database\NexusDB::cache_get(get_challenge_key($userInfo->username)); header("Location: $to"); } stdhead($lang_usercp['head_control_panel'].$lang_usercp['head_security_settings']); usercpmenu ("security"); + form ("security", $type == "save" ? "confirm" : "save","security"); print (""); if ($type == 'save') { - print(""); +// print(""); $resetpasskey = $_POST["resetpasskey"]; $resetauthkey = $_POST["resetauthkey"]; $email = mysql_real_escape_string( htmlspecialchars( trim($_POST["email"]) )); @@ -885,20 +879,23 @@ EOD; print(""); print(""); print(""); - print(""); +// print(""); print(""); print(""); print(""); - Print("\n"); - do_action("usercp_security_update_confirm", $_POST); - submit(); - print("
".$lang_usercp['row_security_check']."
".$lang_usercp['text_security_check_note']."
"); + Print("".$lang_usercp['row_security_check']."
".$lang_usercp['text_security_check_note']."\n"); + print(''); + print(''); + do_action("usercp_security_update_confirm", $_POST); + submit("button"); + print(""); + render_password_challenge_js("security", "username", "oldpassword"); stdfoot(); die; } if ($type == 'saved') print("".$lang_usercp['text_saved'].($_GET["mail"] == "1" ? $lang_usercp['std_confirmation_email_sent'] : "")." ".($_GET["passkey"] == "1" ? $lang_usercp['std_passkey_reset'] : "")." ".($_GET["password"] == "1" ? $lang_usercp['std_password_changed'] : "")." ".($_GET["privacy"] == "1" ? $lang_usercp['std_privacy_level_updated'] : "")."\n"); - form ("security"); + tr_small($lang_usercp['row_reset_passkey'],"".$lang_usercp['checkbox_reset_my_passkey']."
".$lang_usercp['text_reset_passkey_note']."", 1); // tr_small($lang_usercp['row_reset_authkey'],"".$lang_usercp['checkbox_reset_my_authkey']."
".$lang_usercp['text_reset_authkey_note']."", 1); @@ -927,11 +924,14 @@ EOD; if ($disableemailchange != 'no' && $smtptype != 'none') //system-wide setting tr_small($lang_usercp['row_email_address'], "
".$lang_usercp['text_email_address_note']."", 1); do_action("usercp_security_setting_form"); - tr_small($lang_usercp['row_change_password'], "", 1); - tr_small($lang_usercp['row_type_password_again'], "", 1); + tr_small($lang_usercp['row_change_password'], "", 1); + print(''); + tr_small($lang_usercp['row_type_password_again'], "", 1); tr_small($lang_usercp['row_privacy_level'], priv("normal", $lang_usercp['radio_normal']) . " " . priv("low", $lang_usercp['radio_low']) . " " . priv("strong", $lang_usercp['radio_strong']), 1); - submit(); - print(""); + submit("button"); + print(""); + + render_password_hash_js("security", "password", "chpassword", false,"passagain"); stdfoot(); die; break; diff --git a/resources/lang/en/signup.php b/resources/lang/en/signup.php new file mode 100644 index 00000000..fa415c76 --- /dev/null +++ b/resources/lang/en/signup.php @@ -0,0 +1,8 @@ + "The passwords didn't match! Must've typoed. Try again.", + 'password_too_short' => "Sorry, password is too short (min is 6 chars)", + 'password_too_long' => "Sorry, password is too long (max is 40 chars)", + 'password_equals_username' => "Sorry, password cannot be same as user name.", +]; diff --git a/resources/lang/zh_CN/signup.php b/resources/lang/zh_CN/signup.php new file mode 100644 index 00000000..dcabfce4 --- /dev/null +++ b/resources/lang/zh_CN/signup.php @@ -0,0 +1,8 @@ + "两次输入的密码不一致!请重试。", + 'password_too_short' => "对不起,密码过短(至少6个字符)", + 'password_too_long' => "对不起,密码过长(至多40个字符)", + 'password_equals_username' => "对不起,用户名和密码不能相同。", +]; diff --git a/resources/lang/zh_TW/signup.php b/resources/lang/zh_TW/signup.php new file mode 100644 index 00000000..df46d876 --- /dev/null +++ b/resources/lang/zh_TW/signup.php @@ -0,0 +1,8 @@ + "兩次輸入的密碼不壹致!請重試。", + 'password_too_short' => "對不起,密碼過短(至少6個字符)", + 'password_too_long' => "對不起,密碼過長(至多40個字符)", + 'password_equals_username' => "對不起,用戶名和密碼不能相同。", +]; diff --git a/routes/third-party.php b/routes/third-party.php index 8e6ca3c2..0b456fdb 100644 --- a/routes/third-party.php +++ b/routes/third-party.php @@ -6,6 +6,8 @@ Route::group(['middleware' => ['auth.nexus:passkey', 'locale']], function () { }); +Route::post('challenge', [\App\Http\Controllers\AuthenticateController::class, 'challenge']); + Route::post('nastools/approve', [\App\Http\Controllers\AuthenticateController::class, 'nasToolsApprove']); Route::get('iyuu/approve', [\App\Http\Controllers\AuthenticateController::class, 'iyuuApprove']); Route::post('ammds/approve', [\App\Http\Controllers\AuthenticateController::class, 'ammdsApprove']);