diff --git a/README.fr.md b/README.fr.md index 8de07aab0..2d391c99b 100644 --- a/README.fr.md +++ b/README.fr.md @@ -236,11 +236,11 @@ Voir le [dépôt dédié à ces extensions](https://github.com/FreshRSS/Extensio | Langage | Progression | | | - | - | - | -| العربية (ar) | ■■■■■■■■■■ 100% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Far+%2F%28TODO%7CDIRTY%29%24%2F) | +| العربية (ar) | ■■■■■■■■■・ 99% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Far+%2F%28TODO%7CDIRTY%29%24%2F) | | Azərbaycanca (az) | ■■■■■■■■■・ 99% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Faz+%2F%28TODO%7CDIRTY%29%24%2F) | | Беларуская (be) | ■■■■■■■■■・ 99% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fbe+%2F%28TODO%7CDIRTY%29%24%2F) | | Čeština (cs) | ■■■■■■■■・・ 81% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fcs+%2F%28TODO%7CDIRTY%29%24%2F) | -| Deutsch (de) | ■■■■■■■■■■ 100% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fde+%2F%28TODO%7CDIRTY%29%24%2F) | +| Deutsch (de) | ■■■■■■■■■・ 99% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fde+%2F%28TODO%7CDIRTY%29%24%2F) | | Ελληνικά (el) | ■■■■■■■・・・ 75% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fel+%2F%28TODO%7CDIRTY%29%24%2F) | | English (en) | ■■■■■■■■■■ 100% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fen+%2F%28TODO%7CDIRTY%29%24%2F) | | English (United States) (en-US) | ■■■■■■■■■■ 100% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fen-US+%2F%28TODO%7CDIRTY%29%24%2F) | @@ -256,16 +256,16 @@ Voir le [dépôt dédié à ces extensions](https://github.com/FreshRSS/Extensio | 한국어 (ko) | ■■■■■■■■・・ 80% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fko+%2F%28TODO%7CDIRTY%29%24%2F) | | Lietuvių (lt) | ■■■■■■■■■・ 99% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Flt+%2F%28TODO%7CDIRTY%29%24%2F) | | Latviešu (lv) | ■■■■■■■■・・ 81% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Flv+%2F%28TODO%7CDIRTY%29%24%2F) | -| Nederlands (nl) | ■■■■■■■■■■ 100% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fnl+%2F%28TODO%7CDIRTY%29%24%2F) | +| Nederlands (nl) | ■■■■■■■■■・ 99% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fnl+%2F%28TODO%7CDIRTY%29%24%2F) | | Occitan (oc) | ■■■■■■■・・・ 73% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Foc+%2F%28TODO%7CDIRTY%29%24%2F) | | Polski (pl) | ■■■■■■■■■■ 100% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fpl+%2F%28TODO%7CDIRTY%29%24%2F) | | Português (Brasil) (pt-BR) | ■■■■■■■■■・ 99% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fpt-BR+%2F%28TODO%7CDIRTY%29%24%2F) | -| Português (Portugal) (pt-PT) | ■■■■■■■■■■ 100% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fpt-PT+%2F%28TODO%7CDIRTY%29%24%2F) | +| Português (Portugal) (pt-PT) | ■■■■■■■■■・ 99% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fpt-PT+%2F%28TODO%7CDIRTY%29%24%2F) | | Русский (ru) | ■■■■■■■■■・ 98% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fru+%2F%28TODO%7CDIRTY%29%24%2F) | -| Slovenčina (sk) | ■■■■■■■■・・ 81% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fsk+%2F%28TODO%7CDIRTY%29%24%2F) | -| Türkçe (tr) | ■■■■■■■■■■ 100% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ftr+%2F%28TODO%7CDIRTY%29%24%2F) | +| Slovenčina (sk) | ■■■■■■■■・・ 80% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fsk+%2F%28TODO%7CDIRTY%29%24%2F) | +| Türkçe (tr) | ■■■■■■■■■・ 99% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ftr+%2F%28TODO%7CDIRTY%29%24%2F) | | Українська (uk) | ■■■■■■■■■・ 98% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fuk+%2F%28TODO%7CDIRTY%29%24%2F) | -| 简体中文 (zh-CN) | ■■■■■■■■■・ 96% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fzh-CN+%2F%28TODO%7CDIRTY%29%24%2F) | +| 简体中文 (zh-CN) | ■■■■■■■■■・ 95% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fzh-CN+%2F%28TODO%7CDIRTY%29%24%2F) | | 正體中文 (zh-TW) | ■■■■■■■■■・ 97% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fzh-TW+%2F%28TODO%7CDIRTY%29%24%2F) | @@ -311,7 +311,6 @@ et [l’API Fever](https://freshrss.github.io/FreshRSS/fr/users/06_Fever_API.htm * [SimplePie](https://simplepie.org/) * [php-http-304](https://alexandre.alapetite.fr/doc-alex/php-http-304/) * [lib_opml](https://framagit.org/marienfressinaud/lib_opml) -* [bcrypt.js](https://github.com/dcodeIO/bcrypt.js) * [PhpGt/CssXPath](https://github.com/PhpGt/CssXPath) * [PHPMailer](https://github.com/PHPMailer/PHPMailer) * [Chart.js](https://www.chartjs.org) diff --git a/README.md b/README.md index 667e32ac9..66a1e6c2d 100644 --- a/README.md +++ b/README.md @@ -132,11 +132,11 @@ See the [repository dedicated to those extensions](https://github.com/FreshRSS/E | Language | Progress | | | - | - | - | -| العربية (ar) | ■■■■■■■■■■ 100% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Far+%2F%28TODO%7CDIRTY%29%24%2F) | +| العربية (ar) | ■■■■■■■■■・ 99% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Far+%2F%28TODO%7CDIRTY%29%24%2F) | | Azərbaycanca (az) | ■■■■■■■■■・ 99% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Faz+%2F%28TODO%7CDIRTY%29%24%2F) | | Беларуская (be) | ■■■■■■■■■・ 99% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fbe+%2F%28TODO%7CDIRTY%29%24%2F) | | Čeština (cs) | ■■■■■■■■・・ 81% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fcs+%2F%28TODO%7CDIRTY%29%24%2F) | -| Deutsch (de) | ■■■■■■■■■■ 100% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fde+%2F%28TODO%7CDIRTY%29%24%2F) | +| Deutsch (de) | ■■■■■■■■■・ 99% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fde+%2F%28TODO%7CDIRTY%29%24%2F) | | Ελληνικά (el) | ■■■■■■■・・・ 75% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fel+%2F%28TODO%7CDIRTY%29%24%2F) | | English (en) | ■■■■■■■■■■ 100% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fen+%2F%28TODO%7CDIRTY%29%24%2F) | | English (United States) (en-US) | ■■■■■■■■■■ 100% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fen-US+%2F%28TODO%7CDIRTY%29%24%2F) | @@ -152,16 +152,16 @@ See the [repository dedicated to those extensions](https://github.com/FreshRSS/E | 한국어 (ko) | ■■■■■■■■・・ 80% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fko+%2F%28TODO%7CDIRTY%29%24%2F) | | Lietuvių (lt) | ■■■■■■■■■・ 99% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Flt+%2F%28TODO%7CDIRTY%29%24%2F) | | Latviešu (lv) | ■■■■■■■■・・ 81% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Flv+%2F%28TODO%7CDIRTY%29%24%2F) | -| Nederlands (nl) | ■■■■■■■■■■ 100% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fnl+%2F%28TODO%7CDIRTY%29%24%2F) | +| Nederlands (nl) | ■■■■■■■■■・ 99% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fnl+%2F%28TODO%7CDIRTY%29%24%2F) | | Occitan (oc) | ■■■■■■■・・・ 73% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Foc+%2F%28TODO%7CDIRTY%29%24%2F) | | Polski (pl) | ■■■■■■■■■■ 100% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fpl+%2F%28TODO%7CDIRTY%29%24%2F) | | Português (Brasil) (pt-BR) | ■■■■■■■■■・ 99% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fpt-BR+%2F%28TODO%7CDIRTY%29%24%2F) | -| Português (Portugal) (pt-PT) | ■■■■■■■■■■ 100% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fpt-PT+%2F%28TODO%7CDIRTY%29%24%2F) | +| Português (Portugal) (pt-PT) | ■■■■■■■■■・ 99% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fpt-PT+%2F%28TODO%7CDIRTY%29%24%2F) | | Русский (ru) | ■■■■■■■■■・ 98% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fru+%2F%28TODO%7CDIRTY%29%24%2F) | -| Slovenčina (sk) | ■■■■■■■■・・ 81% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fsk+%2F%28TODO%7CDIRTY%29%24%2F) | -| Türkçe (tr) | ■■■■■■■■■■ 100% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ftr+%2F%28TODO%7CDIRTY%29%24%2F) | +| Slovenčina (sk) | ■■■■■■■■・・ 80% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fsk+%2F%28TODO%7CDIRTY%29%24%2F) | +| Türkçe (tr) | ■■■■■■■■■・ 99% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ftr+%2F%28TODO%7CDIRTY%29%24%2F) | | Українська (uk) | ■■■■■■■■■・ 98% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fuk+%2F%28TODO%7CDIRTY%29%24%2F) | -| 简体中文 (zh-CN) | ■■■■■■■■■・ 96% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fzh-CN+%2F%28TODO%7CDIRTY%29%24%2F) | +| 简体中文 (zh-CN) | ■■■■■■■■■・ 95% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fzh-CN+%2F%28TODO%7CDIRTY%29%24%2F) | | 正體中文 (zh-TW) | ■■■■■■■■■・ 97% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fzh-TW+%2F%28TODO%7CDIRTY%29%24%2F) | @@ -207,7 +207,6 @@ and [Fever API](https://freshrss.github.io/FreshRSS/en/developers/06_Fever_API.h * [SimplePie](https://simplepie.org/) * [php-http-304](https://alexandre.alapetite.fr/doc-alex/php-http-304/) * [lib_opml](https://framagit.org/marienfressinaud/lib_opml) -* [bcrypt.js](https://github.com/dcodeIO/bcrypt.js) * [PhpGt/CssXPath](https://github.com/PhpGt/CssXPath) * [PHPMailer](https://github.com/PHPMailer/PHPMailer) * [Chart.js](https://www.chartjs.org) diff --git a/app/Controllers/authController.php b/app/Controllers/authController.php index c5a1b6ead..3680dcd46 100644 --- a/app/Controllers/authController.php +++ b/app/Controllers/authController.php @@ -94,12 +94,12 @@ class FreshRSS_auth_Controller extends FreshRSS_ActionController { * This action handles form login page. * * If this action is reached through a POST request, username and password - * are compared to login the current user. + * are compared against the bcrypt password hash stored in the user configuration file + * to authenticate the current user. * * Parameters are: - * - nonce (default: false) * - username (default: '') - * - challenge (default: '') + * - passwordPlain (default: '') * - keep_logged_in (default: false) * * @throws Exception @@ -108,32 +108,25 @@ class FreshRSS_auth_Controller extends FreshRSS_ActionController { invalidateHttpCache(); FreshRSS_View::prependTitle(_t('gen.auth.login') . ' · '); - FreshRSS_View::appendScript(Minz_Url::display('/scripts/vendor/bcrypt.js?' . @filemtime(PUBLIC_PATH . '/scripts/vendor/bcrypt.js'))); $this->view->auth_autofills = FreshRSS_FormAuth::getAutofilledFields(); $limits = FreshRSS_Context::systemConf()->limits; $this->view->cookie_days = (int)round($limits['cookie_duration'] / 86400, 1); - $isPOST = Minz_Request::isPost() && !Minz_Session::paramBoolean('POST_to_GET'); - Minz_Session::_param('POST_to_GET'); - - if ($isPOST) { - $nonce = Minz_Session::paramString('nonce'); - Minz_Session::_param('nonce', false); // One-time: consume the challenge so it cannot be replayed. + if (Minz_Request::isPost()) { $username = Minz_Request::paramString('username'); - $challenge = Minz_Request::paramString('challenge'); + $passwordPlain = Minz_Request::paramString('passwordPlain', plaintext: true, trim: false); $ip_address = Minz_Request::connectionRemoteAddress(); - if ($nonce === '') { - Minz_Log::warning("Invalid session during login for user={$username}, nonce={$nonce}, ip_address={$ip_address}"); + if (!Minz_Session::paramBoolean('sessionOk')) { + Minz_Log::warning("Invalid session during login for user={$username}, ip_address={$ip_address}"); header('HTTP/1.1 403 Forbidden'); - Minz_Session::_param('POST_to_GET', true); //Prevent infinite internal redirect Minz_Request::setBadNotification(_t('install.session.nok')); - Minz_Request::forward(['c' => 'auth', 'a' => 'login'], false); + Minz_Request::forward(['c' => 'auth', 'a' => 'login'], redirect: true); return; } - usleep(random_int(100, 10000)); //Primitive mitigation of timing attacks, in μs + usleep(random_int(100, 10000)); // Primitive mitigation of timing attacks, in μs FreshRSS_Context::initUser($username); if (!FreshRSS_Context::hasUserConf()) { @@ -144,25 +137,25 @@ class FreshRSS_auth_Controller extends FreshRSS_ActionController { } if (!FreshRSS_Context::userConf()->enabled || FreshRSS_Context::userConf()->passwordHash == '') { - usleep(random_int(100, 5000)); //Primitive mitigation of timing attacks, in μs + usleep(random_int(100, 5000)); // Primitive mitigation of timing attacks, in μs Minz_Error::error(403, _t('feedback.auth.login.invalid'), false); return; } $ok = FreshRSS_FormAuth::checkCredentials( - $username, FreshRSS_Context::userConf()->passwordHash, $nonce, $challenge + $username, FreshRSS_Context::userConf()->passwordHash, $passwordPlain ); if ($ok) { - // Set session parameter to give access to the user. try { Minz_Session::regenerateID('FreshRSS'); } catch (RuntimeException $e) { Minz_Log::error("Session could not be regenerated during login for user={$username}, ip_address={$ip_address}: {$e->getMessage()}"); header('HTTP/1.1 500 Internal Server Error'); Minz_Request::setBadNotification(_t('install.session.nok')); - Minz_Request::forward(['c' => 'auth', 'a' => 'login'], false); + Minz_Request::forward(['c' => 'auth', 'a' => 'login'], redirect: true); return; } + // Set session parameters to give access to the user. Minz_Session::_params([ Minz_User::CURRENT_USER => $username, 'passwordHash' => FreshRSS_Context::userConf()->passwordHash, @@ -192,14 +185,14 @@ class FreshRSS_auth_Controller extends FreshRSS_ActionController { showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0 ); } else { - Minz_Log::warning("Password mismatch for user={$username}, nonce={$nonce}, c={$challenge}, ip_address={$ip_address}"); + Minz_Log::warning("Password mismatch for user={$username}, ip_address={$ip_address}"); header('HTTP/1.1 403 Forbidden'); - Minz_Session::_param('POST_to_GET', true); //Prevent infinite internal redirect Minz_Request::setBadNotification(_t('feedback.auth.login.invalid')); - Minz_Request::forward(['c' => 'auth', 'a' => 'login'], false); + Minz_Request::forward(['c' => 'auth', 'a' => 'login'], redirect: true); } } else { Minz_Session::deleteLegacyCookie('FreshRSS'); // Delete legacy cookie (before 1.29.0) + Minz_Session::_param('sessionOk', true); // To be checked in the POST login request. } } @@ -216,12 +209,10 @@ class FreshRSS_auth_Controller extends FreshRSS_ActionController { } if (Minz_Request::isPost()) { $username = Minz_User::name() ?? ''; - $nonce = Minz_Session::paramString('nonce'); - Minz_Session::_param('nonce', false); // One-time: consume the challenge so it cannot be replayed. - $challenge = Minz_Request::paramString('challenge'); + $passwordPlain = Minz_Request::paramString('passwordPlain', plaintext: true, trim: false); if (!FreshRSS_FormAuth::checkCredentials( - $username, FreshRSS_Context::userConf()->passwordHash, $nonce, $challenge - )) { + $username, FreshRSS_Context::userConf()->passwordHash, $passwordPlain + )) { Minz_Request::setBadNotification(_t('feedback.auth.login.invalid')); } else { try { @@ -238,7 +229,6 @@ class FreshRSS_auth_Controller extends FreshRSS_ActionController { } } FreshRSS_View::prependTitle(_t('gen.auth.reauth.title') . ' · '); - FreshRSS_View::appendScript(Minz_Url::display('/scripts/vendor/bcrypt.js?' . @filemtime(PUBLIC_PATH . '/scripts/vendor/bcrypt.js'))); $this->view->auth_autofills = FreshRSS_FormAuth::getAutofilledFields(); } diff --git a/app/Controllers/javascriptController.php b/app/Controllers/javascriptController.php index a60c8dfbd..497f21bdc 100644 --- a/app/Controllers/javascriptController.php +++ b/app/Controllers/javascriptController.php @@ -66,48 +66,4 @@ class FreshRSS_javascript_Controller extends FreshRSS_ActionController { $tagDAO = FreshRSS_Factory::createTagDao(); $this->view->tags = $tagDAO->listTags(precounts: true); } - - //For Web-form login - - /** - * @throws Exception - */ - public function nonceAction(): void { - header('Content-Type: application/json; charset=UTF-8'); - header('Last-Modified: ' . gmdate('D, d M Y H:i:s \\G\\M\\T')); - header('Expires: 0'); - header('Cache-Control: private, no-cache, no-store, must-revalidate'); - header('Pragma: no-cache'); - - $user = Minz_Request::paramString('user'); - if ($user === '') { - Minz_Error::error(400); - return; - } - $user_conf = FreshRSS_UserConfiguration::getForUser($user); - if ($user_conf !== null) { - try { - $s = $user_conf->passwordHash; - if (strlen($s) >= 60) { - //CRYPT_BLOWFISH Salt: "$2a$", a two digit cost parameter, "$", and 22 characters from the alphabet "./0-9A-Za-z". - $this->view->salt1 = substr($s, 0, 29); - $this->view->nonce = hash('sha256', FreshRSS_Context::systemConf()->salt . $user . random_bytes(32)); - Minz_Session::_param('nonce', $this->view->nonce); - return; //Success - } - } catch (Minz_Exception $me) { - Minz_Log::warning('Nonce failure: ' . $me->getMessage()); - } - } else { - Minz_Log::notice('Nonce failure due to invalid username! ' . $user); - } - //Failure: Return random data. - $this->view->salt1 = sprintf('$2a$%02d$', FreshRSS_password_Util::BCRYPT_COST); - $alphabet = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - for ($i = 22; $i > 0; $i--) { - $this->view->salt1 .= $alphabet[random_int(0, 63)]; - } - $this->view->nonce = hash('sha256', 'failure' . rand()); - Minz_Session::_param('nonce', $this->view->nonce); - } } diff --git a/app/Controllers/userController.php b/app/Controllers/userController.php index 916c14e3f..7fd45af96 100644 --- a/app/Controllers/userController.php +++ b/app/Controllers/userController.php @@ -91,6 +91,9 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController { } if ($passwordPlain != '') { + if (!FreshRSS_FormAuth::passwordRequirementsMet($passwordPlain, ['enforceMinLength' => true])) { + return false; + } $passwordHash = FreshRSS_password_Util::hash($passwordPlain); $userConfig->passwordHash = $passwordHash; if ($user === Minz_User::name()) { @@ -119,7 +122,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController { } $username = Minz_Request::paramString('username'); - $newPasswordPlain = Minz_User::name() !== $username ? Minz_Request::paramString('newPasswordPlain', true) : ''; + $newPasswordPlain = Minz_User::name() !== $username ? Minz_Request::paramString('newPasswordPlain', plaintext: true, trim: false) : ''; $ok = self::updateUser($username, null, $newPasswordPlain, [ 'token' => Minz_Request::paramString('token') ?: null, @@ -162,26 +165,28 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController { FreshRSS_View::prependTitle(_t('conf.profile.title') . ' · '); - FreshRSS_View::appendScript(Minz_Url::display('/scripts/vendor/bcrypt.js?' . @filemtime(PUBLIC_PATH . '/scripts/vendor/bcrypt.js'))); - if (Minz_Request::isPost() && Minz_User::name() != null) { $old_email = FreshRSS_Context::userConf()->mail_login; $email = Minz_Request::paramString('email'); - $challenge = Minz_Request::paramString('challenge'); - $newPasswordPlain = ''; - if ($challenge !== '') { + $passwordPlain = Minz_Request::paramString('passwordPlain', plaintext: true, trim: false); + $newPasswordPlain = Minz_Request::paramString('newPasswordPlain', plaintext: true, trim: false); + $confirmPasswordPlain = Minz_Request::paramString('confirmPasswordPlain', plaintext: true, trim: false); + if ($passwordPlain !== '') { $username = Minz_User::name(); - $nonce = Minz_Session::paramString('nonce'); - Minz_Session::_param('nonce', false); // One-time: consume the challenge so it cannot be replayed. - $newPasswordPlain = Minz_Request::paramString('newPasswordPlain', plaintext: true); - $confirmPasswordPlain = Minz_Request::paramString('confirmPasswordPlain', plaintext: true); + $passwordRequirementsMet = FreshRSS_FormAuth::passwordRequirementsMet( + $newPasswordPlain, + ['enforceMinLength' => true] + ); - if (!FreshRSS_FormAuth::checkCredentials( - $username, FreshRSS_Context::userConf()->passwordHash, $nonce, $challenge - ) || strlen($newPasswordPlain) < 7) { + // Currently the only requirement is checking whether the password length is within the range of 7-72 bytes. + // This limit is enforced in the form using a custom JS validator as well, so no custom error message is needed so far + // on the server side. + if (!$passwordRequirementsMet || !FreshRSS_FormAuth::checkCredentials( + $username, FreshRSS_Context::userConf()->passwordHash, $passwordPlain + )) { Minz_Session::_param('open', true); // Auto-expand `change password` section Minz_Request::bad( _t('feedback.auth.login.invalid'), @@ -190,7 +195,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController { return; } - if ($newPasswordPlain !== $confirmPasswordPlain) { + if ($confirmPasswordPlain !== $newPasswordPlain) { Minz_Session::_param('open', true); // Auto-expand `change password` section Minz_Request::bad( _t('feedback.profile.passwords_dont_match'), @@ -436,7 +441,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController { if (Minz_Request::isPost()) { $new_user_name = Minz_Request::paramString('new_user_name'); $email = Minz_Request::paramString('new_user_email'); - $passwordPlain = Minz_Request::paramString('new_user_passwordPlain', true); + $passwordPlain = Minz_Request::paramString('new_user_passwordPlain', plaintext: true, trim: false); $badRedirectUrl = [ 'c' => Minz_Request::paramString('originController') ?: 'auth', 'a' => Minz_Request::paramString('originAction') ?: 'register', @@ -456,7 +461,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController { ); } - if (!FreshRSS_password_Util::check($passwordPlain)) { + if (!FreshRSS_FormAuth::passwordRequirementsMet($passwordPlain)) { Minz_Request::bad( _t('user.password.invalid'), $badRedirectUrl @@ -708,13 +713,10 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController { $ok = true; if ($self_deletion) { // We check the password if it’s a self-destruction - $nonce = Minz_Session::paramString('nonce'); - Minz_Session::_param('nonce', false); // One-time: consume the challenge so it cannot be replayed. - $challenge = Minz_Request::paramString('challenge'); + $passwordPlain = Minz_Request::paramString('passwordPlain', plaintext: true, trim: false); $ok &= FreshRSS_FormAuth::checkCredentials( - $username, FreshRSS_Context::userConf()->passwordHash, - $nonce, $challenge + $username, FreshRSS_Context::userConf()->passwordHash, $passwordPlain ); if (!$ok) { Minz_Request::bad(_t('feedback.auth.login.invalid'), ['c' => 'user', 'a' => 'profile']); diff --git a/app/FreshRSS.php b/app/FreshRSS.php index 1376285e8..220fce2f5 100644 --- a/app/FreshRSS.php +++ b/app/FreshRSS.php @@ -158,7 +158,6 @@ class FreshRSS extends Minz_FrontController { Minz_Request::is('user', 'delete') || Minz_Request::is('auth', 'logout') || Minz_Request::is('feed', 'actualize') || - Minz_Request::is('javascript', 'nonce') || Minz_Request::is('error', 'index') ); if ($email_not_verified && !$action_is_allowed) { diff --git a/app/Models/FormAuth.php b/app/Models/FormAuth.php index 62bf43c62..15b8b3b18 100644 --- a/app/Models/FormAuth.php +++ b/app/Models/FormAuth.php @@ -2,23 +2,66 @@ declare(strict_types=1); class FreshRSS_FormAuth { - public static function checkCredentials(string $username, #[\SensitiveParameter] string $hash, - string $nonce, #[\SensitiveParameter] string $challenge): bool { - if (!FreshRSS_user_Controller::checkUsername($username) || - !ctype_graph($hash) || - !ctype_graph($challenge) || - !ctype_alnum($nonce)) { - Minz_Log::debug("Invalid credential parameters: user={$username}, challenge={$challenge}, nonce={$nonce}"); + public const MIN_PASSWORD_LENGTH = 7; // Only to be enforced when setting new passwords. + public const MAX_PASSWORD_LENGTH = 72; // Bcrypt limits password lengths to 72 bytes. + + /** + * @param array{enforceMinLength?: bool} $options + */ + public static function passwordRequirementsMet(#[\SensitiveParameter] string $passwordPlain, array $options = []): bool { + $enforceMinLength = ($options['enforceMinLength'] ?? true) === true; + if (strlen($passwordPlain) > self::MAX_PASSWORD_LENGTH) { + return false; + } + if ($enforceMinLength && strlen($passwordPlain) < self::MIN_PASSWORD_LENGTH) { + return false; + } + if (trim($passwordPlain) === '') { + return false; + } + return true; + } + + public static function checkCredentials(string $username, #[\SensitiveParameter] string $hash, #[\SensitiveParameter] string $passwordPlain): bool { + if (!FreshRSS_user_Controller::checkUsername($username) || !ctype_graph($hash)) { + Minz_Log::debug("Invalid credential parameters: user={$username}"); return false; } // Expecting bcrypt format, see: https://en.wikipedia.org/wiki/Bcrypt#Description - if (!preg_match('/^\$2[aby]\$(0[4-9]|10)\$[.\/0-9A-Za-z]{53}$/', $challenge)) { - Minz_Log::debug("Invalid challenge format: user={$username}, challenge={$challenge}, nonce={$nonce}"); + // FreshRSS has not historically used any other algorithm for password hash generation, + // so no upgrades are required. + if (!preg_match('/^\$2[aby]\$(0[4-9]|10)\$[.\/0-9A-Za-z]{53}$/', $hash)) { + Minz_Log::debug("Invalid hash format: user={$username}"); return false; } - return password_verify($hash . $nonce, $challenge); + $byteLen = strlen($passwordPlain); + $utf8Prefix = mb_check_encoding($passwordPlain, 'UTF-8') + ? mb_strcut($passwordPlain, 0, self::MAX_PASSWORD_LENGTH, 'UTF-8') + : ''; + + $cutOffUtf8Character = $byteLen > self::MAX_PASSWORD_LENGTH && + strlen($utf8Prefix) < self::MAX_PASSWORD_LENGTH && + mb_strlen($passwordPlain, 'UTF-8') === mb_strlen($utf8Prefix, 'UTF-8') + 1; + + // Only reject logging in with a password longer than 72 bytes if the limit does not + // cut off a valid UTF-8 character, since this may be an existing bcrypt password. + if (!$cutOffUtf8Character && $byteLen > self::MAX_PASSWORD_LENGTH) { + Minz_Log::warning("Exceeded maximum allowed password length during authentication: user={$username}"); + return false; + } + + // https://www.php.net/manual/function.password-verify.php + if (password_verify($passwordPlain, $hash)) { + if (trim($passwordPlain) === '') { + Minz_Log::warning("Refusing authentication with empty zero-length password: user={$username}"); + return false; + } + return true; + } + + return false; } /** diff --git a/app/Models/ViewJavascript.php b/app/Models/ViewJavascript.php index d7a8ca714..83628fc7a 100644 --- a/app/Models/ViewJavascript.php +++ b/app/Models/ViewJavascript.php @@ -2,14 +2,10 @@ declare(strict_types=1); final class FreshRSS_ViewJavascript extends FreshRSS_View { - /** @var array where the key is the category ID */ public array $categories; /** @var array where the key is the feed ID */ public array $feeds; /** @var array where the key is the label ID */ public array $tags; - - public string $nonce; - public string $salt1; } diff --git a/app/Utils/passwordUtil.php b/app/Utils/passwordUtil.php index 4f13a57fe..ecc45465d 100644 --- a/app/Utils/passwordUtil.php +++ b/app/Utils/passwordUtil.php @@ -18,17 +18,6 @@ class FreshRSS_password_Util { return $passwordHash; } - /** - * Verify the given password is valid. - * - * A valid password is a string of at least 7 characters. - * - * @return bool True if the password is valid, false otherwise - */ - public static function check(#[\SensitiveParameter] string $password): bool { - return strlen($password) >= 7; - } - public static function cryptAvailable(): bool { $hash = '$2y$04$usesomesillystringfore7hnbRJHxXVLeakoG8K30oukPsA.ztMG'; return $hash === @crypt('password', $hash); diff --git a/app/i18n/ar/admin.php b/app/i18n/ar/admin.php index e863d6ff2..532442f14 100644 --- a/app/i18n/ar/admin.php +++ b/app/i18n/ar/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => 'السماح بالقراءة المجهولة لمقالات المستخدم الافتراضي (%s)', 'allow_anonymous_refresh' => 'السماح بالتحديث المجهول للمقالات', 'api_enabled' => 'السماح بالوصول إلى API (مطلوب لتطبيقات الجوال ومشاركة استعلامات المستخدم)', - 'form' => 'نموذج الويب (تقليدي، يتطلب JavaScript)', + 'form' => 'نموذج الويب (تقليدي، يتطلب JavaScript)', // DIRTY 'http' => 'HTTP (متقدم: يديره خادم الويب، OIDC، SSO…)', 'none' => 'لا شيء (خطير)', 'title' => 'المصادقة', diff --git a/app/i18n/ar/user.php b/app/i18n/ar/user.php index 14e7e8ad9..fa7ec0e14 100644 --- a/app/i18n/ar/user.php +++ b/app/i18n/ar/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => 'كلمة المرور غير صالحة.', + 'provided_password_too_long' => 'The provided password is too long.', // TODO ), 'tos' => array( 'feedback' => array( diff --git a/app/i18n/az/admin.php b/app/i18n/az/admin.php index 985f8e05f..dc6ea1696 100644 --- a/app/i18n/az/admin.php +++ b/app/i18n/az/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => 'Standart istifadəçinin məqalələrinin anonim oxunmasına icazə ver (%s)', 'allow_anonymous_refresh' => 'Məqalələrin anonim yenilənməsinə icazə ver', 'api_enabled' => 'API girişinə icazə ver (mobil tətbiqlər və istifadəçi sorğularının paylaşılması üçün tələb olunur)', - 'form' => 'Veb forma (ənənəvi, JavaScript tələb edir)', + 'form' => 'Veb forma (ənənəvi)', 'http' => 'HTTP (qabaqcıl: veb server, OIDC, SSO… tərəfindən idarə olunur)', 'none' => 'Yoxdur (təhlükəlidir)', 'title' => 'Kimlik doğrulama', diff --git a/app/i18n/az/user.php b/app/i18n/az/user.php index a0ae82b7e..0230d68a4 100644 --- a/app/i18n/az/user.php +++ b/app/i18n/az/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => 'Parol yanlışdır.', + 'provided_password_too_long' => 'The provided password is too long.', // TODO ), 'tos' => array( 'feedback' => array( diff --git a/app/i18n/be/admin.php b/app/i18n/be/admin.php index c77c029fc..40f5ec2fc 100644 --- a/app/i18n/be/admin.php +++ b/app/i18n/be/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => 'Дазволіць ананімнае чытанне артыкулаў прадвызначанага карыстальніка (%s)', 'allow_anonymous_refresh' => 'Дазволіць ананімнае абнаўленне артыкулаў', 'api_enabled' => 'Дазволіць доступ да API (патрабуецца для мабільных праграм і абагульвання карыстальніцкіх запытаў)', - 'form' => 'Вэб-форма (традыцыйны спосаб, патрабуе JavaScript)', + 'form' => 'Вэб-форма (традыцыйны спосаб)', 'http' => 'HTTP (для дасведчаных: кіруецца вэб-серверам, OIDC, SSO…)', 'none' => 'Няма (небяспечна)', 'title' => 'Аўтэнтыфікацыя', diff --git a/app/i18n/be/user.php b/app/i18n/be/user.php index 8ec7f8e12..9ffbbf0ac 100644 --- a/app/i18n/be/user.php +++ b/app/i18n/be/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => 'Няправільны пароль.', + 'provided_password_too_long' => 'The provided password is too long.', // TODO ), 'tos' => array( 'feedback' => array( diff --git a/app/i18n/cs/admin.php b/app/i18n/cs/admin.php index 5cfe6c81b..3c559c76e 100644 --- a/app/i18n/cs/admin.php +++ b/app/i18n/cs/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => 'Povolit anonymní čtení článků výchozího uživatele (%s)', 'allow_anonymous_refresh' => 'Povolit anonymní obnovení článků', 'api_enabled' => 'Povolit přístup k API (vyžadováno pro mobilní aplikace and sharing user queries)', // DIRTY - 'form' => 'Webový formulář (tradiční, vyžaduje JavaScript)', + 'form' => 'Webový formulář (tradiční)', 'http' => 'HTTP (advanced: managed by Web server, OIDC, SSO…)', // TODO 'none' => 'Žádný (nebezpečné)', 'title' => 'Ověřování', diff --git a/app/i18n/cs/user.php b/app/i18n/cs/user.php index cd218ad43..51c9de842 100644 --- a/app/i18n/cs/user.php +++ b/app/i18n/cs/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => 'Heslo je neplatné.', + 'provided_password_too_long' => 'The provided password is too long.', // TODO ), 'tos' => array( 'feedback' => array( diff --git a/app/i18n/de/admin.php b/app/i18n/de/admin.php index 54564b391..11bca7cd0 100644 --- a/app/i18n/de/admin.php +++ b/app/i18n/de/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => 'Anonymes Lesen der Artikel des Standardbenutzers (%s) erlauben', 'allow_anonymous_refresh' => 'Anonymes Aktualisieren der Artikel erlauben', 'api_enabled' => 'API-Zugriff erlauben (für mobile Apps und zum Teilen von Benutzerabfragen erforderlich)', - 'form' => 'Webformular (traditionell, benötigt JavaScript)', + 'form' => 'Webformular (traditionell)', 'http' => 'HTTP (fortgeschritten: vom Webserver verwaltet, OIDC, SSO…)', 'none' => 'Keine (gefährlich)', 'title' => 'Authentifizierung', diff --git a/app/i18n/de/user.php b/app/i18n/de/user.php index 628c960f9..5fe6bcf73 100644 --- a/app/i18n/de/user.php +++ b/app/i18n/de/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => 'Das Passwort ist ungültig.', + 'provided_password_too_long' => 'The provided password is too long.', // TODO ), 'tos' => array( 'feedback' => array( diff --git a/app/i18n/el/admin.php b/app/i18n/el/admin.php index 41dce38f2..296f6c237 100644 --- a/app/i18n/el/admin.php +++ b/app/i18n/el/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => 'Επιτρέψτε την ανώνυμη ανάγνωση των άρθρων του προεπιλεγμένου χρήστη (%s)', 'allow_anonymous_refresh' => 'Επιτρέψτε την ανώνυμη ανανέωση των άρθρων', 'api_enabled' => 'Επιτρέψτε την πρόσβαση API (απαιτείται για εφαρμογές κινητών και κοινοποίηση ερωτημάτων χρηστών)', - 'form' => 'Φόρμα ιστού (σύνηθες, απαιτεί JavaScript)', + 'form' => 'Φόρμα ιστού (σύνηθες)', 'http' => 'HTTP (για προχωρημένους: διαχειρίζεται από τον Web server, OIDC, SSO…)', 'none' => 'Καμία (ριψοκίνδυνο)', 'title' => 'Πιστοποίηση', diff --git a/app/i18n/el/user.php b/app/i18n/el/user.php index 9d3ec5dd0..7939c9362 100644 --- a/app/i18n/el/user.php +++ b/app/i18n/el/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => 'Ο κωδικός πρόσβασης δεν είναι έγκυρος.', + 'provided_password_too_long' => 'The provided password is too long.', // TODO ), 'tos' => array( 'feedback' => array( diff --git a/app/i18n/en-US/admin.php b/app/i18n/en-US/admin.php index 66612544d..05f32acb9 100644 --- a/app/i18n/en-US/admin.php +++ b/app/i18n/en-US/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => 'Allow anonymous reading of the default user’s articles (%s)', // IGNORE 'allow_anonymous_refresh' => 'Allow anonymous refresh of the articles', // IGNORE 'api_enabled' => 'Allow API access (required for mobile apps and sharing user queries)', // IGNORE - 'form' => 'Web form (traditional, requires JavaScript)', // IGNORE + 'form' => 'Web form (traditional)', // IGNORE 'http' => 'HTTP (advanced: managed by Web server, OIDC, SSO…)', // IGNORE 'none' => 'None (dangerous)', // IGNORE 'title' => 'Authentication', // IGNORE diff --git a/app/i18n/en-US/user.php b/app/i18n/en-US/user.php index abae569f8..6ac603451 100644 --- a/app/i18n/en-US/user.php +++ b/app/i18n/en-US/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => 'The password is invalid.', // IGNORE + 'provided_password_too_long' => 'The provided password is too long.', // IGNORE ), 'tos' => array( 'feedback' => array( diff --git a/app/i18n/en/admin.php b/app/i18n/en/admin.php index 0d2a3dd5e..35832983c 100644 --- a/app/i18n/en/admin.php +++ b/app/i18n/en/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => 'Allow anonymous reading of the default user’s articles (%s)', 'allow_anonymous_refresh' => 'Allow anonymous refresh of the articles', 'api_enabled' => 'Allow API access (required for mobile apps and sharing user queries)', - 'form' => 'Web form (traditional, requires JavaScript)', + 'form' => 'Web form (traditional)', 'http' => 'HTTP (advanced: managed by Web server, OIDC, SSO…)', 'none' => 'None (dangerous)', 'title' => 'Authentication', diff --git a/app/i18n/en/user.php b/app/i18n/en/user.php index b41ec5aee..e2331103f 100644 --- a/app/i18n/en/user.php +++ b/app/i18n/en/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => 'The password is invalid.', + 'provided_password_too_long' => 'The provided password is too long.', ), 'tos' => array( 'feedback' => array( diff --git a/app/i18n/es/admin.php b/app/i18n/es/admin.php index 6d1501ed2..be04f005a 100644 --- a/app/i18n/es/admin.php +++ b/app/i18n/es/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => 'Permitir la lectura anónima de los artículos del usuario por defecto (%s)', 'allow_anonymous_refresh' => 'Permitir la actualización anónima de los artículos', 'api_enabled' => 'Concederle acceso a la API (necesario para apps de móvil y compartir vistas de usuario)', - 'form' => 'Formulario Web (el más habitual, requiere JavaScript)', + 'form' => 'Formulario Web (el más habitual)', 'http' => 'HTTP (avanzado: gestionado por el servidor, OIDC, SSO…)', 'none' => 'Ninguno (peligroso)', 'title' => 'Identificación', diff --git a/app/i18n/es/user.php b/app/i18n/es/user.php index c48b00401..d753b1ae9 100644 --- a/app/i18n/es/user.php +++ b/app/i18n/es/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => 'La contraseña no es válida.', + 'provided_password_too_long' => 'The provided password is too long.', // TODO ), 'tos' => array( 'feedback' => array( diff --git a/app/i18n/fa/admin.php b/app/i18n/fa/admin.php index 5fa772e88..d1469edab 100644 --- a/app/i18n/fa/admin.php +++ b/app/i18n/fa/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => ' اجازه خواندن ناشناس مقالات کاربر پیش‌فرض (%s)', 'allow_anonymous_refresh' => ' اجازه بازخوانی ناشناس مقالات را بدهید', 'api_enabled' => 'اجازه دسترسی به API (برای برنامه‌های موبایل و اشتراک‌گذاری پرس‌وجوهای کاربر لازم است)', - 'form' => 'فرم وب (سنتی، نیازمند JavaScript)', + 'form' => 'فرم وب (سنتی، نیازمند JavaScript)', // DIRTY 'http' => 'HTTP (پیشرفته: مدیریت‌شده توسط وب‌سرور، OIDC، SSO و...)', 'none' => ' هیچ (خطرناک)', 'title' => ' احراز هویت', diff --git a/app/i18n/fa/user.php b/app/i18n/fa/user.php index 409f583c8..b84a01d9a 100644 --- a/app/i18n/fa/user.php +++ b/app/i18n/fa/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => ' رمز عبور نامعتبر است.', + 'provided_password_too_long' => 'The provided password is too long.', // TODO ), 'tos' => array( 'feedback' => array( diff --git a/app/i18n/fi/admin.php b/app/i18n/fi/admin.php index 117d3f898..5b75306a2 100644 --- a/app/i18n/fi/admin.php +++ b/app/i18n/fi/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => 'Salli kirjautumattomien käyttäjien lukea oletuskäyttäjän artikkeleita (%s)', 'allow_anonymous_refresh' => 'Salli kirjautumattomien käyttäjien päivittää artikkelit', 'api_enabled' => 'Salli API-käyttö (pakollinen kännykkäsovelluksille ja käyttäjän kyselyjen jakamiselle)', - 'form' => 'Web-lomake (perinteinen, käyttää JavaScriptiä)', + 'form' => 'Web-lomake (perinteinen)', 'http' => 'HTTP (lisäasetukset: web-palvelin, OIDC, SSO…)', 'none' => 'Ei mitään (vaarallinen)', 'title' => 'Todentaminen', diff --git a/app/i18n/fi/user.php b/app/i18n/fi/user.php index a26f2ad18..431cd3ff8 100644 --- a/app/i18n/fi/user.php +++ b/app/i18n/fi/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => 'Salasana on väärä.', + 'provided_password_too_long' => 'The provided password is too long.', // TODO ), 'tos' => array( 'feedback' => array( diff --git a/app/i18n/fr/admin.php b/app/i18n/fr/admin.php index cd13f097b..dfe267d55 100644 --- a/app/i18n/fr/admin.php +++ b/app/i18n/fr/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => 'Autoriser la lecture anonyme des articles de l’utilisateur par défaut (%s)', 'allow_anonymous_refresh' => 'Autoriser le rafraîchissement anonyme des flux', 'api_enabled' => 'Autoriser l’accès par API (nécessaire pour les applications mobiles et pour partager les filtres utilisateurs)', - 'form' => 'Formulaire (traditionnel, requiert JavaScript)', + 'form' => 'Formulaire (traditionnel)', 'http' => 'HTTP (avancé : contrôlé par serveur Web, OIDC, SSO…)', 'none' => 'Aucune (dangereux)', 'title' => 'Authentification', diff --git a/app/i18n/fr/user.php b/app/i18n/fr/user.php index 8f2241ed5..57b22d6f7 100644 --- a/app/i18n/fr/user.php +++ b/app/i18n/fr/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => 'Le mot de passe est invalide.', + 'provided_password_too_long' => 'Le mot de passe fourni est trop long !', ), 'tos' => array( 'feedback' => array( diff --git a/app/i18n/he/admin.php b/app/i18n/he/admin.php index f5b0e2fdf..be6130480 100644 --- a/app/i18n/he/admin.php +++ b/app/i18n/he/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => 'הרשאה לאנונימיים לקרוא את מאמרי משתמש ברירת המחדל (%s)', 'allow_anonymous_refresh' => 'הרשאה לאנונימיים לרענן את רשימת המאמרים', 'api_enabled' => 'הרשאת גישה ל API (נדרש ליישומים סלולריים and sharing user queries)', // DIRTY - 'form' => 'טופס אינטרנטי (מסורתי, דורש JavaScript)', + 'form' => 'טופס אינטרנטי (מסורתי)', 'http' => 'HTTP (advanced: managed by Web server, OIDC, SSO…)', // TODO 'none' => 'ללא (מסוכן)', 'title' => 'Authentication', // TODO diff --git a/app/i18n/he/user.php b/app/i18n/he/user.php index 907fd99b2..88db6bcf8 100644 --- a/app/i18n/he/user.php +++ b/app/i18n/he/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => 'The password is invalid.', // TODO + 'provided_password_too_long' => 'The provided password is too long.', // TODO ), 'tos' => array( 'feedback' => array( diff --git a/app/i18n/hu/admin.php b/app/i18n/hu/admin.php index 2aafe22b8..7580446a9 100644 --- a/app/i18n/hu/admin.php +++ b/app/i18n/hu/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => 'Névtelen felhasználók olvashatják az alapértelmezett felhasználó cikkeit (%s)', 'allow_anonymous_refresh' => 'Névtelen felhasználók frissíthetik a cikkeket', 'api_enabled' => ' API elérés engedélyezése (mobilalkalmazásokhoz és felhasználói lekérdezés megosztásához szükséges )', - 'form' => 'Webes űrlap (hagyományos, JavaScript szükséges hozzá)', + 'form' => 'Webes űrlap (hagyományos)', 'http' => 'HTTP (haladó: Web szerver kezeli, OIDC, SSO…)', 'none' => 'nincs (veszélyes)', 'title' => 'Hitelesítés', diff --git a/app/i18n/hu/user.php b/app/i18n/hu/user.php index 6f9aad567..5983d5584 100644 --- a/app/i18n/hu/user.php +++ b/app/i18n/hu/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => 'A jelszó érvénytelen.', + 'provided_password_too_long' => 'The provided password is too long.', // TODO ), 'tos' => array( 'feedback' => array( diff --git a/app/i18n/id/admin.php b/app/i18n/id/admin.php index 09e481bb1..e68d49709 100644 --- a/app/i18n/id/admin.php +++ b/app/i18n/id/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => 'Izinkan pembacaan artikel pengguna default (%s) secara awanama', 'allow_anonymous_refresh' => 'Izinkan untuk memuat ulang artikel awanama', 'api_enabled' => 'Izinkan akses API (Diperlukan untuk aplikasi seluler dan untuk membagikan pencarian pengguna)', - 'form' => 'Formulir Web (klasik, membutuhkan JavaScript)', + 'form' => 'Formulir Web (klasik)', 'http' => 'HTTP (tingkat lanjut: dikelola oleh server Web, OIDC, SSO, dll.)', 'none' => 'Tidak ada (berbahaya)', 'title' => 'Autentikasi', diff --git a/app/i18n/id/user.php b/app/i18n/id/user.php index 49c09460c..670aa7b71 100644 --- a/app/i18n/id/user.php +++ b/app/i18n/id/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => 'Kata sandi tidak valid.', + 'provided_password_too_long' => 'The provided password is too long.', // TODO ), 'tos' => array( 'feedback' => array( diff --git a/app/i18n/it/admin.php b/app/i18n/it/admin.php index 912d2bc04..e1f8bdb85 100644 --- a/app/i18n/it/admin.php +++ b/app/i18n/it/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => 'Consenti la lettura agli utenti anonimi degli articoli dell utente predefinito (%s)', 'allow_anonymous_refresh' => 'Consenti agli utenti anonimi di aggiornare gli articoli', 'api_enabled' => 'Consentire l’accesso all’API (necessario per le app e per la condivisione delle query degli utenti)', - 'form' => 'Web form (tradizionale, richiede JavaScript)', + 'form' => 'Web form (tradizionale)', 'http' => 'HTTP (avanzato: gestito da server Web, OIDC, SSO…)', 'none' => 'Nessuno (pericoloso)', 'title' => 'Autenticazione', diff --git a/app/i18n/it/user.php b/app/i18n/it/user.php index 636dc5e8b..dfc825d90 100644 --- a/app/i18n/it/user.php +++ b/app/i18n/it/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => 'La password non è valida.', + 'provided_password_too_long' => 'The provided password is too long.', // TODO ), 'tos' => array( 'feedback' => array( diff --git a/app/i18n/ja/admin.php b/app/i18n/ja/admin.php index 88f33283f..3cf229022 100644 --- a/app/i18n/ja/admin.php +++ b/app/i18n/ja/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => 'ログインしていないユーザーにもデフォルトユーザー(%s)の記事の閲覧を許可する', 'allow_anonymous_refresh' => 'ログインしていないユーザーにも記事の更新を許可する', 'api_enabled' => 'APIアクセスを許可する(モバイルアプリやユーザークエリの共有に必要)', - 'form' => 'ウェブフォーム(JavaScriptが必要です)', + 'form' => 'ウェブフォーム(JavaScriptが必要です)', // DIRTY 'http' => 'HTTP(上級者向け:Webサーバー、OIDC、SSOなどで管理)', 'none' => 'なし(危険)', 'title' => '認証', diff --git a/app/i18n/ja/user.php b/app/i18n/ja/user.php index fbbae565f..0a1556dcb 100644 --- a/app/i18n/ja/user.php +++ b/app/i18n/ja/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => 'このパスワードは無効です。', + 'provided_password_too_long' => 'The provided password is too long.', // TODO ), 'tos' => array( 'feedback' => array( diff --git a/app/i18n/ko/admin.php b/app/i18n/ko/admin.php index aa5ee55c1..40a9afd20 100644 --- a/app/i18n/ko/admin.php +++ b/app/i18n/ko/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => '누구나 기본 사용자의 글을 읽을 수 있도록 합니다(%s)', 'allow_anonymous_refresh' => '누구나 피드를 갱신할 수 있도록 합니다', 'api_enabled' => 'API 사용을 허가합니다(모바일 애플리케이션을 사용할 때 필요합니다 and sharing user queries)', // DIRTY - 'form' => '웹폼 (전통적인 방식, 자바스크립트 필요)', + 'form' => '웹폼 (전통적인 방식)', 'http' => 'HTTP (advanced: managed by Web server, OIDC, SSO…)', // TODO 'none' => '사용하지 않음 (위험)', 'title' => '인증', diff --git a/app/i18n/ko/user.php b/app/i18n/ko/user.php index 65cb4aa5c..bcbd460dd 100644 --- a/app/i18n/ko/user.php +++ b/app/i18n/ko/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => '이 비밀번호는 유효하지 않습니다.', + 'provided_password_too_long' => 'The provided password is too long.', // TODO ), 'tos' => array( 'feedback' => array( diff --git a/app/i18n/lt/admin.php b/app/i18n/lt/admin.php index c7bb6afa4..5859cde52 100644 --- a/app/i18n/lt/admin.php +++ b/app/i18n/lt/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => 'Leisti anoniminį numatytojo naudotojo straipsnių skaitymą (%s)', 'allow_anonymous_refresh' => 'Leisti anoniminį straipsnių atnaujinimą', 'api_enabled' => 'Leisti API prieigą (reikalinga mobiliosioms programoms ir naudotojo užklausų bendrinimui)', - 'form' => 'Žiniatinklio forma (tradicinė, reikia JavaScript)', + 'form' => 'Žiniatinklio forma (tradicinė)', 'http' => 'HTTP (pažengusiems: valdo žiniatinklio serveris, OIDC, SSO…)', 'none' => 'Joks (pavojinga)', 'title' => 'Tapatybės nustatymas', diff --git a/app/i18n/lt/user.php b/app/i18n/lt/user.php index e916999e6..313f36575 100644 --- a/app/i18n/lt/user.php +++ b/app/i18n/lt/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => 'Slaptažodis netinkamas.', + 'provided_password_too_long' => 'The provided password is too long.', // TODO ), 'tos' => array( 'feedback' => array( diff --git a/app/i18n/lv/admin.php b/app/i18n/lv/admin.php index 50371ff62..f4c01a2c4 100644 --- a/app/i18n/lv/admin.php +++ b/app/i18n/lv/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => 'Ļaut anonīmi lasīt noklusējuma lietotāja rakstus (%s)', 'allow_anonymous_refresh' => 'Atļaut anonīmu rakstu atsvaidzināšanu', 'api_enabled' => 'Atļaut API piekļuvi (nepieciešams mobilajām lietotnēm and sharing user queries)', // DIRTY - 'form' => 'Tīmekļa veidlapa (tradicionālā, nepieciešams JavaScript)', + 'form' => 'Tīmekļa veidlapa (tradicionālā)', 'http' => 'HTTP (advanced: managed by Web server, OIDC, SSO…)', // TODO 'none' => 'Nav (bīstami)', 'title' => 'Autentifikācija', diff --git a/app/i18n/lv/user.php b/app/i18n/lv/user.php index 75e719e1e..2ecb23c02 100644 --- a/app/i18n/lv/user.php +++ b/app/i18n/lv/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => 'Parole ir nederīga.', + 'provided_password_too_long' => 'The provided password is too long.', // TODO ), 'tos' => array( 'feedback' => array( diff --git a/app/i18n/nl/admin.php b/app/i18n/nl/admin.php index 0a101c5f8..03d6582ee 100644 --- a/app/i18n/nl/admin.php +++ b/app/i18n/nl/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => 'Sta bezoekers toe om artikelen te lezen van de standaard gebruiker (%s)', 'allow_anonymous_refresh' => 'Sta bezoekers toe om de artikelen te vernieuwen', 'api_enabled' => 'Sta API toegang toe (nodig voor mobiele apps en het delen van gebruikersqueries)', - 'form' => 'Web formulier (traditioneel, JavaScript vereist)', + 'form' => 'Web formulier (traditioneel)', 'http' => 'HTTP (geavanceerd: beheerd door webserver, OIDC, SSO…)', 'none' => 'Geen (gevaarlijk)', 'title' => 'Authenticatie', diff --git a/app/i18n/nl/user.php b/app/i18n/nl/user.php index fd49f13f6..eac6f3687 100644 --- a/app/i18n/nl/user.php +++ b/app/i18n/nl/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => 'Het wachtwoord is ongeldig.', + 'provided_password_too_long' => 'The provided password is too long.', // TODO ), 'tos' => array( 'feedback' => array( diff --git a/app/i18n/oc/admin.php b/app/i18n/oc/admin.php index 4ea10fb4e..f6767b854 100644 --- a/app/i18n/oc/admin.php +++ b/app/i18n/oc/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => 'Autorizar la lectura anonima dels articles de l’utilizaire per defaut (%s)', 'allow_anonymous_refresh' => 'Autorizar l’actualizacion anonime dels fluxes', 'api_enabled' => 'Autorizar l’accès per API(necessari per las aplicacions mobil and sharing user queries)', // DIRTY - 'form' => 'Formulari (tradicional, demanda JavaScript)', + 'form' => 'Formulari (tradicional)', 'http' => 'HTTP (advanced: managed by Web server, OIDC, SSO…)', // TODO 'none' => 'Cap (perilhós)', 'title' => 'Autentificacion', diff --git a/app/i18n/oc/user.php b/app/i18n/oc/user.php index 4b8a52b7a..b173e39f1 100644 --- a/app/i18n/oc/user.php +++ b/app/i18n/oc/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => 'Lo senhal es invalid.', + 'provided_password_too_long' => 'The provided password is too long.', // TODO ), 'tos' => array( 'feedback' => array( diff --git a/app/i18n/pl/admin.php b/app/i18n/pl/admin.php index efe826963..463bb13c3 100644 --- a/app/i18n/pl/admin.php +++ b/app/i18n/pl/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => 'Pozwól na anonimowy odczyt wiadomości domyślnego użytkownika (%s)', 'allow_anonymous_refresh' => 'Pozwól na anonimowe odświeżanie wiadomości', 'api_enabled' => 'Pozwól na dostęp przez API (wymagane dla aplikacji na telefon i udostępniania zapytań użytkownika)', - 'form' => 'Formularz na stronie (tradycyjna, wymagany JavaScript)', + 'form' => 'Formularz na stronie (tradycyjna)', 'http' => 'HTTP (zaawansowane: zarządzane przez serwer WWW, OIDC, SSO…)', 'none' => 'Brak (niebezpieczna)', 'title' => 'Uwierzytelnianie', diff --git a/app/i18n/pl/user.php b/app/i18n/pl/user.php index c1bd1b269..ad6c17d7f 100644 --- a/app/i18n/pl/user.php +++ b/app/i18n/pl/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => 'Hasło nie jest prawidłowe.', + 'provided_password_too_long' => 'Podane hasło jest zbyt długie.', ), 'tos' => array( 'feedback' => array( diff --git a/app/i18n/pt-BR/admin.php b/app/i18n/pt-BR/admin.php index a67c4140c..696a108cd 100644 --- a/app/i18n/pt-BR/admin.php +++ b/app/i18n/pt-BR/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => 'Permitir a leitura anônima dos artigos pelo usuário padrão (%s)', 'allow_anonymous_refresh' => 'Permitir atualização anônima dos artigos', 'api_enabled' => 'Permitir acesso à API (Necessário para aplicativos móveis e compartilhamento de consultas de usuários)', - 'form' => 'Formulário Web(tradicional, Necessita de JavaScript)', + 'form' => 'Formulário Web(tradicional)', 'http' => 'HTTP (avançado: gerenciado por servidor web, OIDC, SSO…)', 'none' => 'Nenhum (Perigoso)', 'title' => 'Autenticação', diff --git a/app/i18n/pt-BR/user.php b/app/i18n/pt-BR/user.php index e1655313c..dfcd31610 100644 --- a/app/i18n/pt-BR/user.php +++ b/app/i18n/pt-BR/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => 'Senha incorreta', + 'provided_password_too_long' => 'The provided password is too long.', // TODO ), 'tos' => array( 'feedback' => array( diff --git a/app/i18n/pt-PT/admin.php b/app/i18n/pt-PT/admin.php index 08cf2f8dc..49bcf07ec 100644 --- a/app/i18n/pt-PT/admin.php +++ b/app/i18n/pt-PT/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => 'Permitir a leitura anónima dos artigos pelo utilizador predefinido (%s)', 'allow_anonymous_refresh' => 'Permitir atualização anónima dos artigos', 'api_enabled' => 'Permitir acesso à API (Necessário para aplicações móveis e partilha de pesquisas personalizadas)', - 'form' => 'Formulário web (tradicional, requer JavaScript)', + 'form' => 'Formulário web (tradicional)', 'http' => 'HTTP (avançado: gerido pelo servidor web, OIDC, SSO…)', 'none' => 'Nenhum (Perigoso)', 'title' => 'Autenticação', diff --git a/app/i18n/pt-PT/user.php b/app/i18n/pt-PT/user.php index 85ba94d76..307ee1c80 100644 --- a/app/i18n/pt-PT/user.php +++ b/app/i18n/pt-PT/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => 'Palavra-passe incorreta', + 'provided_password_too_long' => 'The provided password is too long.', // TODO ), 'tos' => array( 'feedback' => array( diff --git a/app/i18n/ru/admin.php b/app/i18n/ru/admin.php index 051121148..db3f1edea 100644 --- a/app/i18n/ru/admin.php +++ b/app/i18n/ru/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => 'Разрешить анонимное чтение статей пользователя по умолчанию (%s)', 'allow_anonymous_refresh' => 'Разрешить анонимное обновление статей', 'api_enabled' => 'Позволить API доступ (необходимо для мобильных приложений and sharing user queries)', - 'form' => 'Веб-форма (традиционный, необходим JavaScript)', + 'form' => 'Веб-форма (традиционный)', 'http' => 'HTTP (продвинутый: управляется веб-сервером, OIDC, SSO…)', 'none' => 'Без аутентификации (небезопасно)', 'title' => 'Аутентификации', diff --git a/app/i18n/ru/user.php b/app/i18n/ru/user.php index 76221de29..d4eb576e1 100644 --- a/app/i18n/ru/user.php +++ b/app/i18n/ru/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => 'Неверный пароль.', + 'provided_password_too_long' => 'The provided password is too long.', // TODO ), 'tos' => array( 'feedback' => array( diff --git a/app/i18n/sk/admin.php b/app/i18n/sk/admin.php index 63f2ec5b1..6dbff2055 100644 --- a/app/i18n/sk/admin.php +++ b/app/i18n/sk/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => 'Povoliť čítanie článkov prednastaveného používateľa (%s) bez prihlásenia.', 'allow_anonymous_refresh' => 'Povoliť obnovenie článkov bez prihlásenia', 'api_enabled' => 'Povoliť prístup cez API (vyžadujú mobilné aplikácie and sharing user queries)', // DIRTY - 'form' => 'Webový formulár (traditičný, vyžaduje JavaScript)', + 'form' => 'Webový formulár (traditičný)', 'http' => 'HTTP (advanced: managed by Web server, OIDC, SSO…)', // TODO 'none' => 'Žiadny (nebezpečné)', 'title' => 'Prihlásenie', diff --git a/app/i18n/sk/user.php b/app/i18n/sk/user.php index 61eea785c..75e3f6da8 100644 --- a/app/i18n/sk/user.php +++ b/app/i18n/sk/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => 'Neplatné heslo.', + 'provided_password_too_long' => 'The provided password is too long.', // TODO ), 'tos' => array( 'feedback' => array( diff --git a/app/i18n/tr/admin.php b/app/i18n/tr/admin.php index 0897e4292..a5f8d6cd9 100644 --- a/app/i18n/tr/admin.php +++ b/app/i18n/tr/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => 'Varsayılan kullanıcının makalelerinin (%s) anonim okunmasına izin ver', 'allow_anonymous_refresh' => 'Makalelerin anonim olarak yenilenmesine izin ver', 'api_enabled' => 'API erişimine izin ver (mobil uygulamalar ve kullanıcı sorgularını paylaşmak için gereklidir)', - 'form' => 'Web formu (geleneksel, JavaScript gerektirir)', + 'form' => 'Web formu (geleneksel)', 'http' => 'HTTP (gelişmiş: Web sunucusu, OIDC, SSO vb. tarafından yönetilir)', 'none' => 'Yok (tehlikeli)', 'title' => 'Kimlik Doğrulama', diff --git a/app/i18n/tr/user.php b/app/i18n/tr/user.php index bb568442c..fea614492 100644 --- a/app/i18n/tr/user.php +++ b/app/i18n/tr/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => 'Parola geçersiz.', + 'provided_password_too_long' => 'The provided password is too long.', // TODO ), 'tos' => array( 'feedback' => array( diff --git a/app/i18n/uk/admin.php b/app/i18n/uk/admin.php index 645af3a33..71dc567d1 100644 --- a/app/i18n/uk/admin.php +++ b/app/i18n/uk/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => 'Дозволити анонімне читання статей типового користувача (%s)', 'allow_anonymous_refresh' => 'Дозволити анонімне оновлення статей', 'api_enabled' => 'Дозволити доступ через API (треба для мобільних програм і поширення користувацьких запитів)', - 'form' => 'Вебформа (традиційно, треба JavaScript)', + 'form' => 'Вебформа (традиційно)', 'http' => 'HTTP (складно: за допомогою вебсервера, OIDC, SSO…)', 'none' => 'Нема (небезпечно)', 'title' => 'Вхід', diff --git a/app/i18n/uk/user.php b/app/i18n/uk/user.php index 8e544629a..ae4098675 100644 --- a/app/i18n/uk/user.php +++ b/app/i18n/uk/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => 'Хибний пароль.', + 'provided_password_too_long' => 'The provided password is too long.', // TODO ), 'tos' => array( 'feedback' => array( diff --git a/app/i18n/zh-CN/admin.php b/app/i18n/zh-CN/admin.php index f870258ab..c6c709dab 100644 --- a/app/i18n/zh-CN/admin.php +++ b/app/i18n/zh-CN/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => '允许匿名阅读默认用户(%s)的文章', 'allow_anonymous_refresh' => '允许匿名刷新文章', 'api_enabled' => '允许 API 访问 (用于手机应用和分享用户查询)', - 'form' => '网页表单(传统方式, 需要 JavaScript)', + 'form' => '网页表单(传统方式)', 'http' => 'HTTP(高级:由 Web 服务器、OIDC、SSO 管理…)', 'none' => '无(危险)', 'title' => '认证', diff --git a/app/i18n/zh-CN/user.php b/app/i18n/zh-CN/user.php index 45d5d1ffe..0347908aa 100644 --- a/app/i18n/zh-CN/user.php +++ b/app/i18n/zh-CN/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => '无效密码', + 'provided_password_too_long' => 'The provided password is too long.', // TODO ), 'tos' => array( 'feedback' => array( diff --git a/app/i18n/zh-TW/admin.php b/app/i18n/zh-TW/admin.php index 2bceccf6c..a27e3b300 100644 --- a/app/i18n/zh-TW/admin.php +++ b/app/i18n/zh-TW/admin.php @@ -15,7 +15,7 @@ return array( 'allow_anonymous' => '允許匿名閱讀預設使用者的文章 (%s)', 'allow_anonymous_refresh' => '允許匿名刷新文章', 'api_enabled' => '允許 API 存取 (行動應用程式與分享使用者查詢需要)', - 'form' => 'Web 表單 (傳統方式,需要 JavaScript)', + 'form' => 'Web 表單 (傳統方式,需要 JavaScript)', // DIRTY 'http' => 'HTTP (進階: 由 Web 伺服器、OIDC、SSO 等管理)', 'none' => '無 (危險)', 'title' => '身份驗證', diff --git a/app/i18n/zh-TW/user.php b/app/i18n/zh-TW/user.php index 4d7149829..7a680ee58 100644 --- a/app/i18n/zh-TW/user.php +++ b/app/i18n/zh-TW/user.php @@ -41,6 +41,7 @@ return array( ), 'password' => array( 'invalid' => '密碼無效', + 'provided_password_too_long' => 'The provided password is too long.', // TODO ), 'tos' => array( 'feedback' => array( diff --git a/app/install.php b/app/install.php index ee9b28efb..c9751ee88 100644 --- a/app/install.php +++ b/app/install.php @@ -212,7 +212,7 @@ function saveStep3(): bool { } $password_plain = param('passwordPlain', ''); - if (FreshRSS_Context::systemConf()->auth_type === 'form' && $password_plain == '') { + if (FreshRSS_Context::systemConf()->auth_type === 'form' && !FreshRSS_FormAuth::passwordRequirementsMet($password_plain)) { return false; } @@ -613,7 +613,7 @@ function printStep2(): void {
- +
@@ -694,12 +694,15 @@ function printStep3(): void {
- tabindex="3" /> - + tabindex="3" + minlength="" + pattern=".{,}" + data-max-bytes="" /> +

-
@@ -774,7 +777,13 @@ if (_t('gen.dir') === 'rtl') { - + <?= _t('install.title') ?>: <?= _t('install.step', STEP + 1) ?> @@ -874,5 +883,6 @@ if (_t('gen.dir') === 'rtl') { + diff --git a/app/views/auth/formLogin.phtml b/app/views/auth/formLogin.phtml index 00db222a8..03c722f86 100644 --- a/app/views/auth/formLogin.phtml +++ b/app/views/auth/formLogin.phtml @@ -18,7 +18,7 @@ -
+ @@ -32,12 +32,10 @@
- auth_autofills['password'] !== '' ? ' value="' . $this->auth_autofills['password'] . '"' : '' ?> - class="passwordPlain" required="required" /> + auth_autofills['password'] !== '' ? ' value="' . $this->auth_autofills['password'] . '"' : '' ?> + id="passwordPlain" name="passwordPlain" class="passwordPlain" required="required" />
- -
@@ -50,7 +48,7 @@
-
diff --git a/app/views/auth/reauth.phtml b/app/views/auth/reauth.phtml index 11d00a8da..9e6701912 100644 --- a/app/views/auth/reauth.phtml +++ b/app/views/auth/reauth.phtml @@ -6,25 +6,23 @@

- +
- auth_autofills['password'] !== '' ? ' value="' . $this->auth_autofills['password'] . '"' : '' ?> - class="passwordPlain" required="required" /> + auth_autofills['password'] !== '' ? ' value="' . $this->auth_autofills['password'] . '"' : '' ?> + id="passwordPlain" name="passwordPlain" class="passwordPlain" required="required" />
- -
reauth_time; ?>

-
diff --git a/app/views/auth/register.phtml b/app/views/auth/register.phtml index 9d82a3d8a..78f058886 100644 --- a/app/views/auth/register.phtml +++ b/app/views/auth/register.phtml @@ -50,11 +50,14 @@
- auth_autofills['password'] !== '' ? ' value="' . $this->auth_autofills['password'] . '"' : '' ?> - required="required" autocomplete="new-password" pattern=".{7,}" /> + auth_autofills['password'] !== '' ? ' value="' . $this->auth_autofills['password'] . '"' : '' ?> + required="required" autocomplete="new-password" + minlength="" + pattern=".{,}" + data-max-bytes="" />
-

diff --git a/app/views/helpers/javascript_vars.phtml b/app/views/helpers/javascript_vars.phtml index 822d1011c..58079b16e 100644 --- a/app/views/helpers/javascript_vars.phtml +++ b/app/views/helpers/javascript_vars.phtml @@ -89,6 +89,7 @@ echo json_encode([ 'favicon_size_exceeded' => _t('feedback.sub.feed.favicon.too_large', format_bytes(FreshRSS_Context::systemConf()->limits['max_favicon_upload_size'])), 'language' => FreshRSS_Context::userConf()->language, 'unsafe_csp_header' => _t('gen.js.unsafe_csp_header'), + 'provided_password_too_long' => _t('user.password.provided_password_too_long'), ], 'icons' => [ 'read' => rawurlencode(_i('read')), diff --git a/app/views/javascript/nonce.phtml b/app/views/javascript/nonce.phtml deleted file mode 100644 index ee1e3ecb5..000000000 --- a/app/views/javascript/nonce.phtml +++ /dev/null @@ -1,4 +0,0 @@ - $this->salt1, 'nonce' => $this->nonce]); diff --git a/app/views/user/details.phtml b/app/views/user/details.phtml index ef0c5809c..9364ebfaa 100644 --- a/app/views/user/details.phtml +++ b/app/views/user/details.phtml @@ -59,8 +59,12 @@
- username ? '' : 'disabled="disabled" ' ?>/> + username ? '' : 'disabled="disabled" ' ?> + minlength="" + pattern=".{,}" + data-max-bytes="" />

@@ -68,7 +72,6 @@
- diff --git a/app/views/user/manage.phtml b/app/views/user/manage.phtml index a3936e78d..8799f8564 100644 --- a/app/views/user/manage.phtml +++ b/app/views/user/manage.phtml @@ -70,11 +70,14 @@
- +

-
diff --git a/app/views/user/profile.phtml b/app/views/user/profile.phtml index 85a2b27d6..c4a5a3759 100644 --- a/app/views/user/profile.phtml +++ b/app/views/user/profile.phtml @@ -7,7 +7,7 @@ ?>
- +

@@ -57,28 +57,30 @@ Minz_Session::_param('open', false); ?> -
> +
>
- +
- -
- +

@@ -90,7 +92,12 @@

- +
@@ -149,7 +156,7 @@

- +

@@ -158,11 +165,10 @@
- +
-
- +
diff --git a/cli/create-user.php b/cli/create-user.php index cc4f44ab4..c82a2701f 100755 --- a/cli/create-user.php +++ b/cli/create-user.php @@ -86,6 +86,7 @@ $ok = FreshRSS_user_Controller::createUser( ); if (!$ok) { + // TODO: Refactor with exceptions in updateUser to be able to show the exact reason. fail('FreshRSS could not create user!'); } diff --git a/cli/update-user.php b/cli/update-user.php index e046109b3..2eb6a8fa7 100755 --- a/cli/update-user.php +++ b/cli/update-user.php @@ -75,6 +75,7 @@ $ok = FreshRSS_user_Controller::updateUser( $values); if (!$ok) { + // TODO: Refactor with exceptions in updateUser to be able to show the exact reason. fail('FreshRSS could not update user!'); } diff --git a/docs/en/admins/09_AccessControl.md b/docs/en/admins/09_AccessControl.md index 036d38035..4ddcb3113 100644 --- a/docs/en/admins/09_AccessControl.md +++ b/docs/en/admins/09_AccessControl.md @@ -1,6 +1,6 @@ # Access control -FreshRSS offers three methods of Access control: Form Authentication using JavaScript, HTTP based Authentication, or an uncontrolled state with no authentication required. +FreshRSS offers three methods of Access control: Form Authentication using a standard HTML form, HTTP based Authentication, or an uncontrolled state with no authentication required. ## Server-side feed fetching & security considerations @@ -40,9 +40,9 @@ These steps are not necessary for trusted, single-user deployments, but are stro ## Form Authentication -Form Authentication requires the use of JavaScript. It will work on any supported version of PHP. +Form Authentication is using a standard HTML form, it does not require any JavaScript since FreshRSS v1.31.0. It will work on any supported version of PHP (>=8.1). -This option requires nothing more than selecting Form Authentication during installation. +This option requires nothing more than selecting Form Authentication during installation, and properly configured PHP sessions. ## HTTP Authentication diff --git a/lib/Minz/Log.php b/lib/Minz/Log.php index cae4e5c9e..564c36b20 100644 --- a/lib/Minz/Log.php +++ b/lib/Minz/Log.php @@ -23,6 +23,8 @@ class Minz_Log { 'debug' => LOG_DEBUG, ]; + private static string $lastLog = ''; + /** * Records a message in a specific log file * The message is not logged if @@ -56,6 +58,8 @@ class Minz_Log { }; } + self::$lastLog = $information; + if (! ($env === 'silent' || $level > self::LOG_LEVELS[$log_level])) { $username = Minz_User::name() ?? Minz_User::INTERNAL_USER; if ($file_name == null) { @@ -82,6 +86,20 @@ class Minz_Log { } } + /** + * Returns the most recently logged message as a string value, and clears it. + * Mainly for use in PHPUnit tests. + * + * @phpstan-impure The return value might change, even though input arguments are the same between calls. + */ + public static function getLastLog(bool $clear_last_log = true): string { + $lastLog = self::$lastLog; + if ($clear_last_log) { + self::$lastLog = ''; + } + return $lastLog; + } + /** * Make sure we do not waste a huge amount of disk space with old log messages. * diff --git a/lib/Minz/Request.php b/lib/Minz/Request.php index c671baba5..9b2a53ac9 100644 --- a/lib/Minz/Request.php +++ b/lib/Minz/Request.php @@ -145,12 +145,15 @@ class Minz_Request { /** * @param bool $plaintext `true` to return special characters without any escaping (unsafe), `false` (default) to XML-encode them + * @param bool $trim `false` to skip trimming whitespace surrounding the parameter, `true` (default) to trim whitespace */ - public static function paramStringNull(string $key, bool $plaintext = false): ?string { + public static function paramStringNull(string $key, bool $plaintext = false, bool $trim = true): ?string { if (isset(self::$params[$key])) { $s = self::$params[$key]; if (is_string($s)) { - $s = trim($s); + if ($trim) { + $s = trim($s); + } return $plaintext ? $s : htmlspecialchars($s, ENT_COMPAT, 'UTF-8'); } if (is_int($s) || is_bool($s)) { @@ -162,9 +165,10 @@ class Minz_Request { /** * @param bool $plaintext `true` to return special characters without any escaping (unsafe), `false` (default) to XML-encode them + * @param bool $trim `false` to skip trimming whitespace surrounding the parameter, `true` (default) to trim whitespace */ - public static function paramString(string $key, bool $plaintext = false): string { - return self::paramStringNull($key, $plaintext) ?? ''; + public static function paramString(string $key, bool $plaintext = false, bool $trim = true): string { + return self::paramStringNull($key, $plaintext, $trim) ?? ''; } /** diff --git a/p/scripts/extra.js b/p/scripts/extra.js index fe63eef7e..e7ed3f822 100644 --- a/p/scripts/extra.js +++ b/p/scripts/extra.js @@ -1,106 +1,11 @@ // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0 'use strict'; -/* globals context, notifs_html5_is_supported, openNotification, xmlHttpRequestJson */ - -// -function poormanSalt() { // If crypto.getRandomValues is not available - const base = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789/abcdefghijklmnopqrstuvwxyz'; - let text = '$2a$04$'; - for (let i = 22; i > 0; i--) { - text += base.charAt(Math.floor(Math.random() * 64)); - } - return text; -} +/* globals context, notifs_html5_is_supported */ function forgetOpenCategories() { localStorage.removeItem('FreshRSS_open_categories'); } -function init_crypto_forms() { - const crypto_forms = document.querySelectorAll('.crypto-form'); - if (crypto_forms.length === 0) { - return; - } - - if (!(window.bcrypt)) { - if (window.console) { - console.log('FreshRSS waiting for bcrypt.js…'); - } - setTimeout(init_crypto_forms, 100); - return; - } - - /* globals bcrypt */ - crypto_forms.forEach(crypto_form => { - const submit_button = crypto_form.querySelector('[type="submit"]'); - if (submit_button) { - submit_button.disabled = false; - } - - crypto_form.onsubmit = function (e) { - let challenge = crypto_form.querySelector('#challenge'); - if (!challenge) { - crypto_form.querySelectorAll('details[data-challenge-if-open]').forEach(el => { - if (el.open && !challenge) { - crypto_form.insertAdjacentHTML('beforeend', ''); - challenge = crypto_form.querySelector('#challenge'); - } - }); - if (!challenge) { - return true; - } - } - - e.preventDefault(); - - if (!submit_button) { - return false; - } - submit_button.disabled = true; - - const req = new XMLHttpRequest(); - req.open('GET', './?c=javascript&a=nonce&user=' + crypto_form.querySelector('#username').value, true); - - req.onerror = function () { - openNotification('Communication error!', 'bad'); - submit_button.disabled = false; - }; - - req.onload = function () { - if (req.status == 200) { - const json = xmlHttpRequestJson(req); - if (!json.salt1 || !json.nonce) { - openNotification('Invalid user!', 'bad'); - } else { - try { - const strong = window.Uint32Array && window.crypto && (typeof window.crypto.getRandomValues === 'function'); - const s = bcrypt.hashSync(crypto_form.querySelector('.passwordPlain').value, json.salt1); - const c = bcrypt.hashSync(s + json.nonce, strong ? bcrypt.genSaltSync(4) : poormanSalt()); - challenge.value = c; - if (!s || !c) { - openNotification('Crypto error!', 'bad'); - } else { - crypto_form.removeEventListener('submit', crypto_form.onsubmit); - crypto_form.submit(); - } - } catch (ex) { - openNotification('Crypto exception! ' + ex, 'bad'); - } - } - } else { - req.onerror(); - } - submit_button.disabled = false; - }; - - req.send(); - }; - }); -} -// - -// - function init_display(parent) { const theme = parent.querySelector('select#theme'); if (!theme) { @@ -113,6 +18,7 @@ function init_display(parent) { }); } +// function togglePW(btn) { if (btn.classList.contains('active')) { hidePW(btn); @@ -144,7 +50,29 @@ function init_password_observers(parent) { btn.onclick = () => togglePW(btn); }); } -// + +function init_password_byte_length_validation(parent) { + parent.querySelectorAll('input[type="password"][data-max-bytes]').forEach(input => { + function validate() { + // Display custom validation error for the password input or unset any custom errors. + if (new TextEncoder().encode(input.value).length > +input.dataset.maxBytes) { + input.setCustomValidity(context.i18n.provided_password_too_long); + } else { + input.setCustomValidity(''); + } + + if (input.validity.customError) { + // Update the displayed validation status immediately before + // the user attempts to submit the form. + input.reportValidity(); + } + } + + input.addEventListener('input', validate); + validate(); + }); +} +// function init_archiving(parent) { parent.addEventListener('change', function (e) { @@ -646,8 +574,8 @@ function init_extra_afterDOM() { if (loginButton) { loginButton.addEventListener('click', forgetOpenCategories); } - init_crypto_forms(); init_password_observers(document.body); + init_password_byte_length_validation(document.body); init_select_observers(); init_configuration_alert(); init_2stateButton(); @@ -662,6 +590,7 @@ function init_extra_afterDOM() { if (slider) { slider.addEventListener('freshrss:slider-load', function (e) { init_password_observers(slider); + init_password_byte_length_validation(slider); }); init_slider(slider); init_archiving(slider); diff --git a/p/scripts/install.js b/p/scripts/install.js index bab481538..8fc3c9278 100644 --- a/p/scripts/install.js +++ b/p/scripts/install.js @@ -1,42 +1,12 @@ // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0 'use strict'; -let timeoutHide; - -function showPW_this() { - const id_passwordField = this.getAttribute('data-toggle'); - if (this.classList.contains('active')) { - hidePW(id_passwordField); - } else { - showPW(id_passwordField); - } - return false; -} - -function showPW(id_passwordField) { - const passwordField = document.getElementById(id_passwordField); - passwordField.setAttribute('type', 'text'); - passwordField.nextElementSibling.classList.add('active'); - clearTimeout(timeoutHide); - timeoutHide = setTimeout(function () { hidePW(id_passwordField); }, 5000); - return false; -} - -function hidePW(id_passwordField) { - clearTimeout(timeoutHide); - const passwordField = document.getElementById(id_passwordField); - passwordField.setAttribute('type', 'password'); - passwordField.nextElementSibling.classList.remove('active'); - return false; -} - -function init_password_observers(parent) { - parent.querySelectorAll('.toggle-password').forEach(function (btn) { - btn.addEventListener('click', showPW_this); - }); -} - -init_password_observers(document.body); +(function parseJsonVars() { + const jsonVars = document.getElementById('jsonVars'); + const json = JSON.parse(jsonVars.innerHTML); + jsonVars.outerHTML = ''; + window.context = { 'i18n': json.i18n }; +}()); const auth_type = document.getElementById('auth_type'); function auth_type_change() { diff --git a/p/scripts/vendor/bcrypt.js b/p/scripts/vendor/bcrypt.js deleted file mode 100644 index 50f716a4c..000000000 --- a/p/scripts/vendor/bcrypt.js +++ /dev/null @@ -1,1221 +0,0 @@ -// GENERATED FILE. DO NOT EDIT. -(function (global, factory) { - function preferDefault(exports) { - return exports.default || exports; - } - if (typeof define === "function" && define.amd) { - define(["crypto"], function (_crypto) { - var exports = {}; - factory(exports, _crypto); - return preferDefault(exports); - }); - } else if (typeof exports === "object") { - factory(exports, require("crypto")); - if (typeof module === "object") module.exports = preferDefault(exports); - } else { - (function () { - var exports = {}; - factory(exports, global.crypto); - global.bcrypt = preferDefault(exports); - })(); - } -})( - typeof globalThis !== "undefined" - ? globalThis - : typeof self !== "undefined" - ? self - : this, - function (_exports, _crypto) { - "use strict"; - - Object.defineProperty(_exports, "__esModule", { - value: true, - }); - _exports.compare = compare; - _exports.compareSync = compareSync; - _exports.decodeBase64 = decodeBase64; - _exports.default = void 0; - _exports.encodeBase64 = encodeBase64; - _exports.genSalt = genSalt; - _exports.genSaltSync = genSaltSync; - _exports.getRounds = getRounds; - _exports.getSalt = getSalt; - _exports.hash = hash; - _exports.hashSync = hashSync; - _exports.setRandomFallback = setRandomFallback; - _exports.truncates = truncates; - _crypto = _interopRequireDefault(_crypto); - function _interopRequireDefault(e) { - return e && e.__esModule ? e : { default: e }; - } - /* - Copyright (c) 2012 Nevins Bartolomeo - Copyright (c) 2012 Shane Girish - Copyright (c) 2025 Daniel Wirtz - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions - are met: - 1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - 3. The name of the author may not be used to endorse or promote products - derived from this software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR - IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, - INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF - THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - - // The Node.js crypto module is used as a fallback for the Web Crypto API. When - // building for the browser, inclusion of the crypto module should be disabled, - // which the package hints at in its package.json for bundlers that support it. - - /** - * The random implementation to use as a fallback. - * @type {?function(number):!Array.} - * @inner - */ - var randomFallback = null; - - /** - * Generates cryptographically secure random bytes. - * @function - * @param {number} len Bytes length - * @returns {!Array.} Random bytes - * @throws {Error} If no random implementation is available - * @inner - */ - function randomBytes(len) { - // Web Crypto API. Globally available in the browser and in Node.js >=23. - try { - return crypto.getRandomValues(new Uint8Array(len)); - } catch {} - // Node.js crypto module for non-browser environments. - try { - return _crypto.default.randomBytes(len); - } catch {} - // Custom fallback specified with `setRandomFallback`. - if (!randomFallback) { - throw Error( - "Neither WebCryptoAPI nor a crypto module is available. Use bcrypt.setRandomFallback to set an alternative", - ); - } - return randomFallback(len); - } - - /** - * Sets the pseudo random number generator to use as a fallback if neither node's `crypto` module nor the Web Crypto - * API is available. Please note: It is highly important that the PRNG used is cryptographically secure and that it - * is seeded properly! - * @param {?function(number):!Array.} random Function taking the number of bytes to generate as its - * sole argument, returning the corresponding array of cryptographically secure random byte values. - * @see http://nodejs.org/api/crypto.html - * @see http://www.w3.org/TR/WebCryptoAPI/ - */ - function setRandomFallback(random) { - randomFallback = random; - } - - /** - * Synchronously generates a salt. - * @param {number=} rounds Number of rounds to use, defaults to 10 if omitted - * @param {number=} seed_length Not supported. - * @returns {string} Resulting salt - * @throws {Error} If a random fallback is required but not set - */ - function genSaltSync(rounds, seed_length) { - rounds = rounds || GENSALT_DEFAULT_LOG2_ROUNDS; - if (typeof rounds !== "number") - throw Error( - "Illegal arguments: " + typeof rounds + ", " + typeof seed_length, - ); - if (rounds < 4) rounds = 4; - else if (rounds > 31) rounds = 31; - var salt = []; - salt.push("$2b$"); - if (rounds < 10) salt.push("0"); - salt.push(rounds.toString()); - salt.push("$"); - salt.push(base64_encode(randomBytes(BCRYPT_SALT_LEN), BCRYPT_SALT_LEN)); // May throw - return salt.join(""); - } - - /** - * Asynchronously generates a salt. - * @param {(number|function(Error, string=))=} rounds Number of rounds to use, defaults to 10 if omitted - * @param {(number|function(Error, string=))=} seed_length Not supported. - * @param {function(Error, string=)=} callback Callback receiving the error, if any, and the resulting salt - * @returns {!Promise} If `callback` has been omitted - * @throws {Error} If `callback` is present but not a function - */ - function genSalt(rounds, seed_length, callback) { - if (typeof seed_length === "function") - (callback = seed_length), (seed_length = undefined); // Not supported. - if (typeof rounds === "function") - (callback = rounds), (rounds = undefined); - if (typeof rounds === "undefined") rounds = GENSALT_DEFAULT_LOG2_ROUNDS; - else if (typeof rounds !== "number") - throw Error("illegal arguments: " + typeof rounds); - function _async(callback) { - nextTick(function () { - // Pretty thin, but salting is fast enough - try { - callback(null, genSaltSync(rounds)); - } catch (err) { - callback(err); - } - }); - } - if (callback) { - if (typeof callback !== "function") - throw Error("Illegal callback: " + typeof callback); - _async(callback); - } else - return new Promise(function (resolve, reject) { - _async(function (err, res) { - if (err) { - reject(err); - return; - } - resolve(res); - }); - }); - } - - /** - * Synchronously generates a hash for the given password. - * @param {string} password Password to hash - * @param {(number|string)=} salt Salt length to generate or salt to use, default to 10 - * @returns {string} Resulting hash - */ - function hashSync(password, salt) { - if (typeof salt === "undefined") salt = GENSALT_DEFAULT_LOG2_ROUNDS; - if (typeof salt === "number") salt = genSaltSync(salt); - if (typeof password !== "string" || typeof salt !== "string") - throw Error( - "Illegal arguments: " + typeof password + ", " + typeof salt, - ); - return _hash(password, salt); - } - - /** - * Asynchronously generates a hash for the given password. - * @param {string} password Password to hash - * @param {number|string} salt Salt length to generate or salt to use - * @param {function(Error, string=)=} callback Callback receiving the error, if any, and the resulting hash - * @param {function(number)=} progressCallback Callback successively called with the percentage of rounds completed - * (0.0 - 1.0), maximally once per `MAX_EXECUTION_TIME = 100` ms. - * @returns {!Promise} If `callback` has been omitted - * @throws {Error} If `callback` is present but not a function - */ - function hash(password, salt, callback, progressCallback) { - function _async(callback) { - if (typeof password === "string" && typeof salt === "number") - genSalt(salt, function (err, salt) { - _hash(password, salt, callback, progressCallback); - }); - else if (typeof password === "string" && typeof salt === "string") - _hash(password, salt, callback, progressCallback); - else - nextTick( - callback.bind( - this, - Error( - "Illegal arguments: " + typeof password + ", " + typeof salt, - ), - ), - ); - } - if (callback) { - if (typeof callback !== "function") - throw Error("Illegal callback: " + typeof callback); - _async(callback); - } else - return new Promise(function (resolve, reject) { - _async(function (err, res) { - if (err) { - reject(err); - return; - } - resolve(res); - }); - }); - } - - /** - * Compares two strings of the same length in constant time. - * @param {string} known Must be of the correct length - * @param {string} unknown Must be the same length as `known` - * @returns {boolean} - * @inner - */ - function safeStringCompare(known, unknown) { - var diff = known.length ^ unknown.length; - for (var i = 0; i < known.length; ++i) { - diff |= known.charCodeAt(i) ^ unknown.charCodeAt(i); - } - return diff === 0; - } - - /** - * Synchronously tests a password against a hash. - * @param {string} password Password to compare - * @param {string} hash Hash to test against - * @returns {boolean} true if matching, otherwise false - * @throws {Error} If an argument is illegal - */ - function compareSync(password, hash) { - if (typeof password !== "string" || typeof hash !== "string") - throw Error( - "Illegal arguments: " + typeof password + ", " + typeof hash, - ); - if (hash.length !== 60) return false; - return safeStringCompare( - hashSync(password, hash.substring(0, hash.length - 31)), - hash, - ); - } - - /** - * Asynchronously tests a password against a hash. - * @param {string} password Password to compare - * @param {string} hashValue Hash to test against - * @param {function(Error, boolean)=} callback Callback receiving the error, if any, otherwise the result - * @param {function(number)=} progressCallback Callback successively called with the percentage of rounds completed - * (0.0 - 1.0), maximally once per `MAX_EXECUTION_TIME = 100` ms. - * @returns {!Promise} If `callback` has been omitted - * @throws {Error} If `callback` is present but not a function - */ - function compare(password, hashValue, callback, progressCallback) { - function _async(callback) { - if (typeof password !== "string" || typeof hashValue !== "string") { - nextTick( - callback.bind( - this, - Error( - "Illegal arguments: " + - typeof password + - ", " + - typeof hashValue, - ), - ), - ); - return; - } - if (hashValue.length !== 60) { - nextTick(callback.bind(this, null, false)); - return; - } - hash( - password, - hashValue.substring(0, 29), - function (err, comp) { - if (err) callback(err); - else callback(null, safeStringCompare(comp, hashValue)); - }, - progressCallback, - ); - } - if (callback) { - if (typeof callback !== "function") - throw Error("Illegal callback: " + typeof callback); - _async(callback); - } else - return new Promise(function (resolve, reject) { - _async(function (err, res) { - if (err) { - reject(err); - return; - } - resolve(res); - }); - }); - } - - /** - * Gets the number of rounds used to encrypt the specified hash. - * @param {string} hash Hash to extract the used number of rounds from - * @returns {number} Number of rounds used - * @throws {Error} If `hash` is not a string - */ - function getRounds(hash) { - if (typeof hash !== "string") - throw Error("Illegal arguments: " + typeof hash); - return parseInt(hash.split("$")[2], 10); - } - - /** - * Gets the salt portion from a hash. Does not validate the hash. - * @param {string} hash Hash to extract the salt from - * @returns {string} Extracted salt part - * @throws {Error} If `hash` is not a string or otherwise invalid - */ - function getSalt(hash) { - if (typeof hash !== "string") - throw Error("Illegal arguments: " + typeof hash); - if (hash.length !== 60) - throw Error("Illegal hash length: " + hash.length + " != 60"); - return hash.substring(0, 29); - } - - /** - * Tests if a password will be truncated when hashed, that is its length is - * greater than 72 bytes when converted to UTF-8. - * @param {string} password The password to test - * @returns {boolean} `true` if truncated, otherwise `false` - */ - function truncates(password) { - if (typeof password !== "string") - throw Error("Illegal arguments: " + typeof password); - return utf8Length(password) > 72; - } - - /** - * Continues with the callback on the next tick. - * @function - * @param {function(...[*])} callback Callback to execute - * @inner - */ - var nextTick = - typeof process !== "undefined" && - process && - typeof process.nextTick === "function" - ? typeof setImmediate === "function" - ? setImmediate - : process.nextTick - : setTimeout; - - /** Calculates the byte length of a string encoded as UTF8. */ - function utf8Length(string) { - var len = 0, - c = 0; - for (var i = 0; i < string.length; ++i) { - c = string.charCodeAt(i); - if (c < 128) len += 1; - else if (c < 2048) len += 2; - else if ( - (c & 0xfc00) === 0xd800 && - (string.charCodeAt(i + 1) & 0xfc00) === 0xdc00 - ) { - ++i; - len += 4; - } else len += 3; - } - return len; - } - - /** Converts a string to an array of UTF8 bytes. */ - function utf8Array(string) { - var offset = 0, - c1, - c2; - var buffer = new Array(utf8Length(string)); - for (var i = 0, k = string.length; i < k; ++i) { - c1 = string.charCodeAt(i); - if (c1 < 128) { - buffer[offset++] = c1; - } else if (c1 < 2048) { - buffer[offset++] = (c1 >> 6) | 192; - buffer[offset++] = (c1 & 63) | 128; - } else if ( - (c1 & 0xfc00) === 0xd800 && - ((c2 = string.charCodeAt(i + 1)) & 0xfc00) === 0xdc00 - ) { - c1 = 0x10000 + ((c1 & 0x03ff) << 10) + (c2 & 0x03ff); - ++i; - buffer[offset++] = (c1 >> 18) | 240; - buffer[offset++] = ((c1 >> 12) & 63) | 128; - buffer[offset++] = ((c1 >> 6) & 63) | 128; - buffer[offset++] = (c1 & 63) | 128; - } else { - buffer[offset++] = (c1 >> 12) | 224; - buffer[offset++] = ((c1 >> 6) & 63) | 128; - buffer[offset++] = (c1 & 63) | 128; - } - } - return buffer; - } - - // A base64 implementation for the bcrypt algorithm. This is partly non-standard. - - /** - * bcrypt's own non-standard base64 dictionary. - * @type {!Array.} - * @const - * @inner - **/ - var BASE64_CODE = - "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".split( - "", - ); - - /** - * @type {!Array.} - * @const - * @inner - **/ - var BASE64_INDEX = [ - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 54, 55, 56, 57, 58, 59, 60, - 61, 62, 63, -1, -1, -1, -1, -1, -1, -1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, - 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, -1, -1, - -1, -1, -1, -1, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, - 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, -1, -1, -1, -1, -1, - ]; - - /** - * Encodes a byte array to base64 with up to len bytes of input. - * @param {!Array.} b Byte array - * @param {number} len Maximum input length - * @returns {string} - * @inner - */ - function base64_encode(b, len) { - var off = 0, - rs = [], - c1, - c2; - if (len <= 0 || len > b.length) throw Error("Illegal len: " + len); - while (off < len) { - c1 = b[off++] & 0xff; - rs.push(BASE64_CODE[(c1 >> 2) & 0x3f]); - c1 = (c1 & 0x03) << 4; - if (off >= len) { - rs.push(BASE64_CODE[c1 & 0x3f]); - break; - } - c2 = b[off++] & 0xff; - c1 |= (c2 >> 4) & 0x0f; - rs.push(BASE64_CODE[c1 & 0x3f]); - c1 = (c2 & 0x0f) << 2; - if (off >= len) { - rs.push(BASE64_CODE[c1 & 0x3f]); - break; - } - c2 = b[off++] & 0xff; - c1 |= (c2 >> 6) & 0x03; - rs.push(BASE64_CODE[c1 & 0x3f]); - rs.push(BASE64_CODE[c2 & 0x3f]); - } - return rs.join(""); - } - - /** - * Decodes a base64 encoded string to up to len bytes of output. - * @param {string} s String to decode - * @param {number} len Maximum output length - * @returns {!Array.} - * @inner - */ - function base64_decode(s, len) { - var off = 0, - slen = s.length, - olen = 0, - rs = [], - c1, - c2, - c3, - c4, - o, - code; - if (len <= 0) throw Error("Illegal len: " + len); - while (off < slen - 1 && olen < len) { - code = s.charCodeAt(off++); - c1 = code < BASE64_INDEX.length ? BASE64_INDEX[code] : -1; - code = s.charCodeAt(off++); - c2 = code < BASE64_INDEX.length ? BASE64_INDEX[code] : -1; - if (c1 == -1 || c2 == -1) break; - o = (c1 << 2) >>> 0; - o |= (c2 & 0x30) >> 4; - rs.push(String.fromCharCode(o)); - if (++olen >= len || off >= slen) break; - code = s.charCodeAt(off++); - c3 = code < BASE64_INDEX.length ? BASE64_INDEX[code] : -1; - if (c3 == -1) break; - o = ((c2 & 0x0f) << 4) >>> 0; - o |= (c3 & 0x3c) >> 2; - rs.push(String.fromCharCode(o)); - if (++olen >= len || off >= slen) break; - code = s.charCodeAt(off++); - c4 = code < BASE64_INDEX.length ? BASE64_INDEX[code] : -1; - o = ((c3 & 0x03) << 6) >>> 0; - o |= c4; - rs.push(String.fromCharCode(o)); - ++olen; - } - var res = []; - for (off = 0; off < olen; off++) res.push(rs[off].charCodeAt(0)); - return res; - } - - /** - * @type {number} - * @const - * @inner - */ - var BCRYPT_SALT_LEN = 16; - - /** - * @type {number} - * @const - * @inner - */ - var GENSALT_DEFAULT_LOG2_ROUNDS = 10; - - /** - * @type {number} - * @const - * @inner - */ - var BLOWFISH_NUM_ROUNDS = 16; - - /** - * @type {number} - * @const - * @inner - */ - var MAX_EXECUTION_TIME = 100; - - /** - * @type {Array.} - * @const - * @inner - */ - var P_ORIG = [ - 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, 0xa4093822, 0x299f31d0, - 0x082efa98, 0xec4e6c89, 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, - 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, 0x9216d5d9, 0x8979fb1b, - ]; - - /** - * @type {Array.} - * @const - * @inner - */ - var S_ORIG = [ - 0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, 0xb8e1afed, 0x6a267e96, - 0xba7c9045, 0xf12c7f99, 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, - 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e, 0x0d95748f, 0x728eb658, - 0x718bcd58, 0x82154aee, 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, - 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, 0x8e79dcb0, 0x603a180e, - 0x6c9e0e8b, 0xb01e8a3e, 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, - 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, 0x55ca396a, 0x2aab10b6, - 0xb4cc5c34, 0x1141e8ce, 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, - 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, 0xafd6ba33, 0x6c24cf5c, - 0x7a325381, 0x28958677, 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, - 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, 0xef845d5d, 0xe98575b1, - 0xdc262302, 0xeb651b88, 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, - 0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, 0x21c66842, 0xf6e96c9a, - 0x670c9c61, 0xabd388f0, 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, - 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, 0xa1f1651d, 0x39af0176, - 0x66ca593e, 0x82430e88, 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, - 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, 0x4ed3aa62, 0x363f7706, - 0x1bfedf72, 0x429b023d, 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, - 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, 0xe3fe501a, 0xb6794c3b, - 0x976ce0bd, 0x04c006ba, 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463, - 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, 0x6dfc511f, 0x9b30952c, - 0xcc814544, 0xaf5ebd09, 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, - 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, 0x5579c0bd, 0x1a60320a, - 0xd6a100c6, 0x402c7279, 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, - 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, 0x323db5fa, 0xfd238760, - 0x53317b48, 0x3e00df82, 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, - 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, 0x695b27b0, 0xbbca58c8, - 0xe1ffa35d, 0xb8f011a0, 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, - 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, 0xe1ddf2da, 0xa4cb7e33, - 0x62fb1341, 0xcee4c6e8, 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, - 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, 0xd08ed1d0, 0xafc725e0, - 0x8e3c5b2f, 0x8e7594b7, 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, - 0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, 0x2f2f2218, 0xbe0e1777, - 0xea752dfe, 0x8b021fa1, 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, - 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, 0x165fa266, 0x80957705, - 0x93cc7314, 0x211a1477, 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, - 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, 0x00250e2d, 0x2071b35e, - 0x226800bb, 0x57b8e0af, 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, - 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, 0x83260376, 0x6295cfa9, - 0x11c81968, 0x4e734a41, 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915, - 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, 0x08ba6fb5, 0x571be91f, - 0xf296ec6b, 0x2a0dd915, 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, - 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a, 0x4b7a70e9, 0xb5b32944, - 0xdb75092e, 0xc4192623, 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266, - 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, 0x193602a5, 0x75094c29, - 0xa0591340, 0xe4183a3e, 0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6, - 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, 0x4cdd2086, 0x8470eb26, - 0x6382e9c6, 0x021ecc5e, 0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1, - 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, 0x3e07841c, 0x7fdeae5c, - 0x8e7d44ec, 0x5716f2b8, 0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff, - 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, 0xd19113f9, 0x7ca92ff6, - 0x94324773, 0x22f54701, 0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7, - 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, 0xe238cd99, 0x3bea0e2f, - 0x3280bba1, 0x183eb331, 0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf, - 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, 0xde9a771f, 0xd9930810, - 0xb38bae12, 0xdccf3f2e, 0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87, - 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, 0xec7aec3a, 0xdb851dfa, - 0x63094366, 0xc464c3d2, 0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16, - 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, 0x71dff89e, 0x10314e55, - 0x81ac77d6, 0x5f11199b, 0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509, - 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, 0x86e34570, 0xeae96fb1, - 0x860e5e0a, 0x5a3e2ab3, 0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f, - 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a, 0xc6150eba, 0x94e2ea78, - 0xa5fc3c53, 0x1e0a2df4, 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960, - 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, 0xe3bc4595, 0xa67bc883, - 0xb17f37d1, 0x018cff28, 0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802, - 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, 0x1521b628, 0x29076170, - 0xecdd4775, 0x619f1510, 0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf, - 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, 0xeecc86bc, 0x60622ca7, - 0x9cab5cab, 0xb2f3846e, 0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50, - 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, 0x9b540b19, 0x875fa099, - 0x95f7997e, 0x623d7da8, 0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281, - 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, 0x57f584a5, 0x1b227263, - 0x9b83c3ff, 0x1ac24696, 0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128, - 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, 0x5d4a14d9, 0xe864b7e3, - 0x42105d14, 0x203e13e0, 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0, - 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, 0xd81e799e, 0x86854dc7, - 0xe44b476a, 0x3d816250, 0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3, - 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, 0x095bbf00, 0xad19489d, - 0x1462b174, 0x23820e00, 0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061, - 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, 0x7cde3759, 0xcbee7460, - 0x4085f2a7, 0xce77326e, 0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735, - 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc, 0x9e447a2e, 0xc3453484, - 0xfdd56705, 0x0e1e9ec9, 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340, - 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, 0x153e21e7, 0x8fb03d4a, - 0xe6e39f2b, 0xdb83adf7, 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, - 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068, 0xd4082471, 0x3320f46a, - 0x43b7d4b7, 0x500061af, 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, - 0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45, 0xbfbc09ec, 0x03bd9785, - 0x7fac6dd0, 0x31cb8504, 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, - 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb, 0x68dc1462, 0xd7486900, - 0x680ec0a4, 0x27a18dee, 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, - 0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42, 0x20fe9e35, 0xd9f385b9, - 0xee39d7ab, 0x3b124e8b, 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, - 0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb, 0xfb0af54e, 0xd8feb397, - 0x454056ac, 0xba489527, 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, - 0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33, 0xa62a4a56, 0x3f3125f9, - 0x5ef47e1c, 0x9029317c, 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, - 0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc, 0x07f9c9ee, 0x41041f0f, - 0x404779a4, 0x5d886e17, 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, - 0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b, 0x0e12b4c2, 0x02e1329e, - 0xaf664fd1, 0xcad18115, 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, - 0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728, 0xd0127845, 0x95b794fd, - 0x647d0862, 0xe7ccf5f0, 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, - 0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37, 0xa812dc60, 0xa1ebddf8, - 0x991be14c, 0xdb6e6b0d, 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, - 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b, 0x667b9ffb, 0xcedb7d9c, - 0xa091cf0b, 0xd9155ea3, 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, - 0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d, 0x6842ada7, 0xc66a2b3b, - 0x12754ccc, 0x782ef11c, 0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, - 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9, 0x44421659, 0x0a121386, - 0xd90cec6e, 0xd5abea2a, 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, - 0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d, 0xd1fd8346, 0xf6381fb0, - 0x7745ae04, 0xd736fccc, 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, - 0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61, 0x4e58f48f, 0xf2ddfda2, - 0xf474ef38, 0x8789bdc2, 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, - 0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2, 0x466e598e, 0x20b45770, - 0x8cd55591, 0xc902de4c, 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, - 0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633, 0xe85a1f02, 0x09f0be8c, - 0x4a99a025, 0x1d6efe10, 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, - 0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52, 0x50115e01, 0xa70683fa, - 0xa002b5c4, 0x0de6d027, 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, - 0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62, 0x11e69ed7, 0x2338ea63, - 0x53c2dd94, 0xc2c21634, 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, - 0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24, 0x86e3725f, 0x724d9db9, - 0x1ac15bb4, 0xd39eb8fc, 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, - 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c, 0x6fd5c7e7, 0x56e14ec4, - 0x362abfce, 0xddc6c837, 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0, - 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, 0x5cb0679e, 0x4fa33742, - 0xd3822740, 0x99bc9bbe, 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, - 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, 0x5748ab2f, 0xbc946e79, - 0xc6a376d2, 0x6549c2c8, 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, - 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, 0xa1fad5f0, 0x6a2d519a, - 0x63ef8ce2, 0x9a86ee22, 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, - 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, 0x2826a2f9, 0xa73a3ae1, - 0x4ba99586, 0xef5562e9, 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, - 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, 0xe990fd5a, 0x9e34d797, - 0x2cf0b7d9, 0x022b8b51, 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, - 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, 0xe029ac71, 0xe019a5e6, - 0x47b0acfd, 0xed93fa9b, 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28, - 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, 0x15056dd4, 0x88f46dba, - 0x03a16125, 0x0564f0bd, 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, - 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, 0x7533d928, 0xb155fdf5, - 0x03563482, 0x8aba3cbb, 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, - 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, 0xea7a90c2, 0xfb3e7bce, - 0x5121ce64, 0x774fbe32, 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, - 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, 0xb39a460a, 0x6445c0dd, - 0x586cdecf, 0x1c20c8ae, 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, - 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, 0x72eacea8, 0xfa6484bb, - 0x8d6612ae, 0xbf3c6f47, 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, - 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, 0x4040cb08, 0x4eb4e2cc, - 0x34d2466a, 0x0115af84, 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, - 0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, 0x611560b1, 0xe7933fdc, - 0xbb3a792b, 0x344525bd, 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, - 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, 0x1a908749, 0xd44fbd9a, - 0xd0dadecb, 0xd50ada38, 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, - 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, 0xbf97222c, 0x15e6fc2a, - 0x0f91fc71, 0x9b941525, 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, - 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, 0xe0ec6e0e, 0x1698db3b, - 0x4c98a0be, 0x3278e964, 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e, - 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, 0xdf359f8d, 0x9b992f2e, - 0xe60b6f47, 0x0fe3f11d, 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, - 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, 0xf523f357, 0xa6327623, - 0x93a83531, 0x56cccd02, 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, - 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, 0xe6c6c7bd, 0x327a140a, - 0x45e1d006, 0xc3f27b9a, 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, - 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, 0x53113ec0, 0x1640e3d3, - 0x38abbd60, 0x2547adf0, 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, - 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, 0x1948c25c, 0x02fb8a8c, - 0x01c36ae4, 0xd6ebe1f9, 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, - 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6, - ]; - - /** - * @type {Array.} - * @const - * @inner - */ - var C_ORIG = [ - 0x4f727068, 0x65616e42, 0x65686f6c, 0x64657253, 0x63727944, 0x6f756274, - ]; - - /** - * @param {Array.} lr - * @param {number} off - * @param {Array.} P - * @param {Array.} S - * @returns {Array.} - * @inner - */ - function _encipher(lr, off, P, S) { - // This is our bottleneck: 1714/1905 ticks / 90% - see profile.txt - var n, - l = lr[off], - r = lr[off + 1]; - l ^= P[0]; - - /* - for (var i=0, k=BLOWFISH_NUM_ROUNDS-2; i<=k;) - // Feistel substitution on left word - n = S[l >>> 24], - n += S[0x100 | ((l >> 16) & 0xff)], - n ^= S[0x200 | ((l >> 8) & 0xff)], - n += S[0x300 | (l & 0xff)], - r ^= n ^ P[++i], - // Feistel substitution on right word - n = S[r >>> 24], - n += S[0x100 | ((r >> 16) & 0xff)], - n ^= S[0x200 | ((r >> 8) & 0xff)], - n += S[0x300 | (r & 0xff)], - l ^= n ^ P[++i]; - */ - - //The following is an unrolled version of the above loop. - //Iteration 0 - n = S[l >>> 24]; - n += S[0x100 | ((l >> 16) & 0xff)]; - n ^= S[0x200 | ((l >> 8) & 0xff)]; - n += S[0x300 | (l & 0xff)]; - r ^= n ^ P[1]; - n = S[r >>> 24]; - n += S[0x100 | ((r >> 16) & 0xff)]; - n ^= S[0x200 | ((r >> 8) & 0xff)]; - n += S[0x300 | (r & 0xff)]; - l ^= n ^ P[2]; - //Iteration 1 - n = S[l >>> 24]; - n += S[0x100 | ((l >> 16) & 0xff)]; - n ^= S[0x200 | ((l >> 8) & 0xff)]; - n += S[0x300 | (l & 0xff)]; - r ^= n ^ P[3]; - n = S[r >>> 24]; - n += S[0x100 | ((r >> 16) & 0xff)]; - n ^= S[0x200 | ((r >> 8) & 0xff)]; - n += S[0x300 | (r & 0xff)]; - l ^= n ^ P[4]; - //Iteration 2 - n = S[l >>> 24]; - n += S[0x100 | ((l >> 16) & 0xff)]; - n ^= S[0x200 | ((l >> 8) & 0xff)]; - n += S[0x300 | (l & 0xff)]; - r ^= n ^ P[5]; - n = S[r >>> 24]; - n += S[0x100 | ((r >> 16) & 0xff)]; - n ^= S[0x200 | ((r >> 8) & 0xff)]; - n += S[0x300 | (r & 0xff)]; - l ^= n ^ P[6]; - //Iteration 3 - n = S[l >>> 24]; - n += S[0x100 | ((l >> 16) & 0xff)]; - n ^= S[0x200 | ((l >> 8) & 0xff)]; - n += S[0x300 | (l & 0xff)]; - r ^= n ^ P[7]; - n = S[r >>> 24]; - n += S[0x100 | ((r >> 16) & 0xff)]; - n ^= S[0x200 | ((r >> 8) & 0xff)]; - n += S[0x300 | (r & 0xff)]; - l ^= n ^ P[8]; - //Iteration 4 - n = S[l >>> 24]; - n += S[0x100 | ((l >> 16) & 0xff)]; - n ^= S[0x200 | ((l >> 8) & 0xff)]; - n += S[0x300 | (l & 0xff)]; - r ^= n ^ P[9]; - n = S[r >>> 24]; - n += S[0x100 | ((r >> 16) & 0xff)]; - n ^= S[0x200 | ((r >> 8) & 0xff)]; - n += S[0x300 | (r & 0xff)]; - l ^= n ^ P[10]; - //Iteration 5 - n = S[l >>> 24]; - n += S[0x100 | ((l >> 16) & 0xff)]; - n ^= S[0x200 | ((l >> 8) & 0xff)]; - n += S[0x300 | (l & 0xff)]; - r ^= n ^ P[11]; - n = S[r >>> 24]; - n += S[0x100 | ((r >> 16) & 0xff)]; - n ^= S[0x200 | ((r >> 8) & 0xff)]; - n += S[0x300 | (r & 0xff)]; - l ^= n ^ P[12]; - //Iteration 6 - n = S[l >>> 24]; - n += S[0x100 | ((l >> 16) & 0xff)]; - n ^= S[0x200 | ((l >> 8) & 0xff)]; - n += S[0x300 | (l & 0xff)]; - r ^= n ^ P[13]; - n = S[r >>> 24]; - n += S[0x100 | ((r >> 16) & 0xff)]; - n ^= S[0x200 | ((r >> 8) & 0xff)]; - n += S[0x300 | (r & 0xff)]; - l ^= n ^ P[14]; - //Iteration 7 - n = S[l >>> 24]; - n += S[0x100 | ((l >> 16) & 0xff)]; - n ^= S[0x200 | ((l >> 8) & 0xff)]; - n += S[0x300 | (l & 0xff)]; - r ^= n ^ P[15]; - n = S[r >>> 24]; - n += S[0x100 | ((r >> 16) & 0xff)]; - n ^= S[0x200 | ((r >> 8) & 0xff)]; - n += S[0x300 | (r & 0xff)]; - l ^= n ^ P[16]; - lr[off] = r ^ P[BLOWFISH_NUM_ROUNDS + 1]; - lr[off + 1] = l; - return lr; - } - - /** - * @param {Array.} data - * @param {number} offp - * @returns {{key: number, offp: number}} - * @inner - */ - function _streamtoword(data, offp) { - for (var i = 0, word = 0; i < 4; ++i) - (word = (word << 8) | (data[offp] & 0xff)), - (offp = (offp + 1) % data.length); - return { - key: word, - offp: offp, - }; - } - - /** - * @param {Array.} key - * @param {Array.} P - * @param {Array.} S - * @inner - */ - function _key(key, P, S) { - var offset = 0, - lr = [0, 0], - plen = P.length, - slen = S.length, - sw; - for (var i = 0; i < plen; i++) - (sw = _streamtoword(key, offset)), - (offset = sw.offp), - (P[i] = P[i] ^ sw.key); - for (i = 0; i < plen; i += 2) - (lr = _encipher(lr, 0, P, S)), (P[i] = lr[0]), (P[i + 1] = lr[1]); - for (i = 0; i < slen; i += 2) - (lr = _encipher(lr, 0, P, S)), (S[i] = lr[0]), (S[i + 1] = lr[1]); - } - - /** - * Expensive key schedule Blowfish. - * @param {Array.} data - * @param {Array.} key - * @param {Array.} P - * @param {Array.} S - * @inner - */ - function _ekskey(data, key, P, S) { - var offp = 0, - lr = [0, 0], - plen = P.length, - slen = S.length, - sw; - for (var i = 0; i < plen; i++) - (sw = _streamtoword(key, offp)), - (offp = sw.offp), - (P[i] = P[i] ^ sw.key); - offp = 0; - for (i = 0; i < plen; i += 2) - (sw = _streamtoword(data, offp)), - (offp = sw.offp), - (lr[0] ^= sw.key), - (sw = _streamtoword(data, offp)), - (offp = sw.offp), - (lr[1] ^= sw.key), - (lr = _encipher(lr, 0, P, S)), - (P[i] = lr[0]), - (P[i + 1] = lr[1]); - for (i = 0; i < slen; i += 2) - (sw = _streamtoword(data, offp)), - (offp = sw.offp), - (lr[0] ^= sw.key), - (sw = _streamtoword(data, offp)), - (offp = sw.offp), - (lr[1] ^= sw.key), - (lr = _encipher(lr, 0, P, S)), - (S[i] = lr[0]), - (S[i + 1] = lr[1]); - } - - /** - * Internaly crypts a string. - * @param {Array.} b Bytes to crypt - * @param {Array.} salt Salt bytes to use - * @param {number} rounds Number of rounds - * @param {function(Error, Array.=)=} callback Callback receiving the error, if any, and the resulting bytes. If - * omitted, the operation will be performed synchronously. - * @param {function(number)=} progressCallback Callback called with the current progress - * @returns {!Array.|undefined} Resulting bytes if callback has been omitted, otherwise `undefined` - * @inner - */ - function _crypt(b, salt, rounds, callback, progressCallback) { - var cdata = C_ORIG.slice(), - clen = cdata.length, - err; - - // Validate - if (rounds < 4 || rounds > 31) { - err = Error("Illegal number of rounds (4-31): " + rounds); - if (callback) { - nextTick(callback.bind(this, err)); - return; - } else throw err; - } - if (salt.length !== BCRYPT_SALT_LEN) { - err = Error( - "Illegal salt length: " + salt.length + " != " + BCRYPT_SALT_LEN, - ); - if (callback) { - nextTick(callback.bind(this, err)); - return; - } else throw err; - } - rounds = (1 << rounds) >>> 0; - var P, - S, - i = 0, - j; - - //Use typed arrays when available - huge speedup! - if (typeof Int32Array === "function") { - P = new Int32Array(P_ORIG); - S = new Int32Array(S_ORIG); - } else { - P = P_ORIG.slice(); - S = S_ORIG.slice(); - } - _ekskey(salt, b, P, S); - - /** - * Calcualtes the next round. - * @returns {Array.|undefined} Resulting array if callback has been omitted, otherwise `undefined` - * @inner - */ - function next() { - if (progressCallback) progressCallback(i / rounds); - if (i < rounds) { - var start = Date.now(); - for (; i < rounds; ) { - i = i + 1; - _key(b, P, S); - _key(salt, P, S); - if (Date.now() - start > MAX_EXECUTION_TIME) break; - } - } else { - for (i = 0; i < 64; i++) - for (j = 0; j < clen >> 1; j++) _encipher(cdata, j << 1, P, S); - var ret = []; - for (i = 0; i < clen; i++) - ret.push(((cdata[i] >> 24) & 0xff) >>> 0), - ret.push(((cdata[i] >> 16) & 0xff) >>> 0), - ret.push(((cdata[i] >> 8) & 0xff) >>> 0), - ret.push((cdata[i] & 0xff) >>> 0); - if (callback) { - callback(null, ret); - return; - } else return ret; - } - if (callback) nextTick(next); - } - - // Async - if (typeof callback !== "undefined") { - next(); - - // Sync - } else { - var res; - while (true) - if (typeof (res = next()) !== "undefined") return res || []; - } - } - - /** - * Internally hashes a password. - * @param {string} password Password to hash - * @param {?string} salt Salt to use, actually never null - * @param {function(Error, string=)=} callback Callback receiving the error, if any, and the resulting hash. If omitted, - * hashing is performed synchronously. - * @param {function(number)=} progressCallback Callback called with the current progress - * @returns {string|undefined} Resulting hash if callback has been omitted, otherwise `undefined` - * @inner - */ - function _hash(password, salt, callback, progressCallback) { - var err; - if (typeof password !== "string" || typeof salt !== "string") { - err = Error("Invalid string / salt: Not a string"); - if (callback) { - nextTick(callback.bind(this, err)); - return; - } else throw err; - } - - // Validate the salt - var minor, offset; - if (salt.charAt(0) !== "$" || salt.charAt(1) !== "2") { - err = Error("Invalid salt version: " + salt.substring(0, 2)); - if (callback) { - nextTick(callback.bind(this, err)); - return; - } else throw err; - } - if (salt.charAt(2) === "$") - (minor = String.fromCharCode(0)), (offset = 3); - else { - minor = salt.charAt(2); - if ( - (minor !== "a" && minor !== "b" && minor !== "y") || - salt.charAt(3) !== "$" - ) { - err = Error("Invalid salt revision: " + salt.substring(2, 4)); - if (callback) { - nextTick(callback.bind(this, err)); - return; - } else throw err; - } - offset = 4; - } - - // Extract number of rounds - if (salt.charAt(offset + 2) > "$") { - err = Error("Missing salt rounds"); - if (callback) { - nextTick(callback.bind(this, err)); - return; - } else throw err; - } - var r1 = parseInt(salt.substring(offset, offset + 1), 10) * 10, - r2 = parseInt(salt.substring(offset + 1, offset + 2), 10), - rounds = r1 + r2, - real_salt = salt.substring(offset + 3, offset + 25); - password += minor >= "a" ? "\x00" : ""; - var passwordb = utf8Array(password), - saltb = base64_decode(real_salt, BCRYPT_SALT_LEN); - - /** - * Finishes hashing. - * @param {Array.} bytes Byte array - * @returns {string} - * @inner - */ - function finish(bytes) { - var res = []; - res.push("$2"); - if (minor >= "a") res.push(minor); - res.push("$"); - if (rounds < 10) res.push("0"); - res.push(rounds.toString()); - res.push("$"); - res.push(base64_encode(saltb, saltb.length)); - res.push(base64_encode(bytes, C_ORIG.length * 4 - 1)); - return res.join(""); - } - - // Sync - if (typeof callback == "undefined") - return finish(_crypt(passwordb, saltb, rounds)); - // Async - else { - _crypt( - passwordb, - saltb, - rounds, - function (err, bytes) { - if (err) callback(err, null); - else callback(null, finish(bytes)); - }, - progressCallback, - ); - } - } - - /** - * Encodes a byte array to base64 with up to len bytes of input, using the custom bcrypt alphabet. - * @function - * @param {!Array.} bytes Byte array - * @param {number} length Maximum input length - * @returns {string} - */ - function encodeBase64(bytes, length) { - return base64_encode(bytes, length); - } - - /** - * Decodes a base64 encoded string to up to len bytes of output, using the custom bcrypt alphabet. - * @function - * @param {string} string String to decode - * @param {number} length Maximum output length - * @returns {!Array.} - */ - function decodeBase64(string, length) { - return base64_decode(string, length); - } - var _default = (_exports.default = { - setRandomFallback, - genSaltSync, - genSalt, - hashSync, - hash, - compareSync, - compare, - getRounds, - getSalt, - truncates, - encodeBase64, - decodeBase64, - }); - }, -); diff --git a/phpcs.xml b/phpcs.xml index 1ab113986..57b8d9c7a 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -5,7 +5,7 @@ /\.git/* /data/config.php /data/update.php - /data/users/*/config.php + /data/users/* /(?-i:extensions)/* /lib/http-conditional.php /lib/marienfressinaud/ @@ -32,6 +32,7 @@ + diff --git a/tests/app/Models/FormAuthTest.php b/tests/app/Models/FormAuthTest.php new file mode 100644 index 000000000..968f8149a --- /dev/null +++ b/tests/app/Models/FormAuthTest.php @@ -0,0 +1,339 @@ + false]); + self::assertTrue($ok); + } + + public function testReqsCheckReturnsTrueIfLessThan7Utf8BytesAndWithNoMinLengthEnforcement(): void { + $password = str_repeat('é', 3); + self::assertSame(6, strlen($password)); + + $ok = FreshRSS_FormAuth::passwordRequirementsMet($password, ['enforceMinLength' => false]); + self::assertTrue($ok); + } + + public function testAuthWithInvalidUsernameAndCorrectCredentialsFail(): void { + $username = ':-)'; + $password = '1234567'; + $hash = FreshRSS_password_Util::hash($password); + $ok = FreshRSS_FormAuth::checkCredentials( + $username, $hash, $password + ); + self::assertFalse($ok); + self::assertSame("Invalid credential parameters: user={$username}", Minz_Log::getLastLog()); + } + + public function testAuthWithValidUsernameAndWrongCredentialsFail(): void { + $username = 'admin'; + $password = 'correctpassword'; + $hash = FreshRSS_password_Util::hash('badpassword'); + $ok = FreshRSS_FormAuth::checkCredentials( + $username, $hash, $password + ); + self::assertFalse($ok); + self::assertSame('', Minz_Log::getLastLog()); + } + + public function testAuthWithValidUsernameAndCorrectCredentialsOk(): void { + $username = 'admin'; + $password = 'correctpassword'; + $hash = FreshRSS_password_Util::hash($password); + $ok = FreshRSS_FormAuth::checkCredentials( + $username, $hash, $password + ); + self::assertTrue($ok); + self::assertSame('', Minz_Log::getLastLog()); + } + + public function testAuthWithValidUsernameAndCorrectCredentialsAndTooLongUtf8PasswordOk(): void { + $username = 'admin'; + $password = str_repeat('a', 71) . 'é'; + self::assertSame(73, strlen($password)); + $hash = FreshRSS_password_Util::hash($password); + $ok = FreshRSS_FormAuth::checkCredentials( + $username, $hash, $password + ); + self::assertTrue($ok); // Login is successful with a cut-off UTF-8 character at the end + self::assertSame('', Minz_Log::getLastLog()); + + $password .= 'é'; + $hash = FreshRSS_password_Util::hash($password); + $ok = FreshRSS_FormAuth::checkCredentials( + $username, $hash, $password + ); + self::assertFalse($ok); + self::assertSame("Exceeded maximum allowed password length during authentication: user={$username}", Minz_Log::getLastLog()); + } + + public function testAuthWithValidUsernameAndCorrectCredentialsAndTooLongUtf8PasswordWithCutOffCharacterFollowedByAnotherCharacterFail(): void { + $username = 'admin'; + $password = str_repeat('a', 71) . 'éa'; + self::assertSame(74, strlen($password)); + $hash = FreshRSS_password_Util::hash($password); + $ok = FreshRSS_FormAuth::checkCredentials( + $username, $hash, $password + ); + self::assertFalse($ok); + self::assertSame("Exceeded maximum allowed password length during authentication: user={$username}", Minz_Log::getLastLog()); + } + + public function testAuthWithValidUsernameAndCorrectCredentialsAndTooLongUtf8PasswordWithThreeByteCharacterOk(): void { + $username = 'admin'; + $password = str_repeat('a', 71) . '€'; + self::assertSame(74, strlen($password)); + $hash = FreshRSS_password_Util::hash($password); + $ok = FreshRSS_FormAuth::checkCredentials( + $username, $hash, $password + ); + self::assertTrue($ok); // Login is successful with a cut-off UTF-8 character at the end + self::assertSame('', Minz_Log::getLastLog()); + + $password .= '€'; + $hash = FreshRSS_password_Util::hash($password); + $ok = FreshRSS_FormAuth::checkCredentials( + $username, $hash, $password + ); + self::assertFalse($ok); + self::assertSame("Exceeded maximum allowed password length during authentication: user={$username}", Minz_Log::getLastLog()); + } + + public function testAuthWithValidUsernameAndCorrectCredentialsAndTooLongUtf8PasswordWithFourByteCharacterOk(): void { + $username = 'admin'; + $password = str_repeat('a', 71) . '😀'; + self::assertSame(75, strlen($password)); + $hash = FreshRSS_password_Util::hash($password); + $ok = FreshRSS_FormAuth::checkCredentials( + $username, $hash, $password + ); + self::assertTrue($ok); // Login is successful with a cut-off UTF-8 character at the end + self::assertSame('', Minz_Log::getLastLog()); + + $password .= '😀'; + $hash = FreshRSS_password_Util::hash($password); + $ok = FreshRSS_FormAuth::checkCredentials( + $username, $hash, $password + ); + self::assertFalse($ok); + self::assertSame("Exceeded maximum allowed password length during authentication: user={$username}", Minz_Log::getLastLog()); + } + + public function testAuthWithValidUsernameAndCorrectCredentialsAndTooLongUtf8PasswordWithThreeByteCharacterCutOffAfterTwoBytesOk(): void { + $username = 'admin'; + $password = str_repeat('a', 70) . '€'; + self::assertSame(73, strlen($password)); + $hash = FreshRSS_password_Util::hash($password); + $ok = FreshRSS_FormAuth::checkCredentials( + $username, $hash, $password + ); + self::assertTrue($ok); + self::assertSame('', Minz_Log::getLastLog()); + } + + public function testAuthWithValidUsernameAndCorrectCredentialsAndTooLongUtf8PasswordWithFourByteCharacterCutOffAfterTwoBytesOk(): void { + $username = 'admin'; + $password = str_repeat('a', 70) . '😀'; + self::assertSame(74, strlen($password)); + $hash = FreshRSS_password_Util::hash($password); + $ok = FreshRSS_FormAuth::checkCredentials( + $username, $hash, $password + ); + self::assertTrue($ok); + self::assertSame('', Minz_Log::getLastLog()); + } + + public function testAuthWithValidUsernameAndCorrectCredentialsAndTooLongUtf8PasswordWithFourByteCharacterCutOffAfterThreeBytesOk(): void { + $username = 'admin'; + $password = str_repeat('a', 69) . '😀'; + self::assertSame(73, strlen($password)); + $hash = FreshRSS_password_Util::hash($password); + $ok = FreshRSS_FormAuth::checkCredentials( + $username, $hash, $password + ); + self::assertTrue($ok); + self::assertSame('', Minz_Log::getLastLog()); + } + + public function testAuthWithValidUsernameAndCorrectCredentialsAndTooLongUtf8PasswordWithoutCutOffCharacter(): void { + $username = 'admin'; + $password = str_repeat('a', 72) . 'é'; + self::assertSame(74, strlen($password)); + $hash = FreshRSS_password_Util::hash($password); + $ok = FreshRSS_FormAuth::checkCredentials( + $username, $hash, $password + ); + self::assertFalse($ok); + self::assertSame("Exceeded maximum allowed password length during authentication: user={$username}", Minz_Log::getLastLog()); + } + + public function testAuthWithValidUsernameAndCorrectCredentialsAndHugeUtf8Password(): void { + $username = 'admin'; + $password = str_repeat('a', 10000) . 'é'; + $hash = FreshRSS_password_Util::hash($password); + $ok = FreshRSS_FormAuth::checkCredentials( + $username, $hash, $password + ); + self::assertFalse($ok); + self::assertSame("Exceeded maximum allowed password length during authentication: user={$username}", Minz_Log::getLastLog()); + } + + public function testAuthWithValidUsernameAndCorrectCredentialsAndEmptyPasswordFail(): void { + $username = 'admin'; + $password = ''; + $hash = FreshRSS_password_Util::hash($password); + $ok = FreshRSS_FormAuth::checkCredentials( + $username, $hash, $password + ); + self::assertFalse($ok); + self::assertSame("Refusing authentication with empty zero-length password: user={$username}", Minz_Log::getLastLog()); + } + + public function testAuthWithValidUsernameAndCorrectCredentialsAndTooLongPasswordFail(): void { + $username = 'admin'; + $password = str_repeat('a', 73); + $hash = FreshRSS_password_Util::hash($password); + $ok = FreshRSS_FormAuth::checkCredentials( + $username, $hash, $password + ); + self::assertFalse($ok); + self::assertSame("Exceeded maximum allowed password length during authentication: user={$username}", Minz_Log::getLastLog()); + + // It's fine if the user truncates their own password though + // Note: There are separate tests for UTF-8 passwords to test the case + // of a cut-off UTF-8 character at the end, which can't easily be truncated + // by a user. + $password = str_repeat('a', 72); + $ok = FreshRSS_FormAuth::checkCredentials( + $username, $hash, $password + ); + self::assertTrue($ok); + self::assertSame('', Minz_Log::getLastLog()); + + // If truncated down to less than 72 characters, login should fail + $password = str_repeat('a', 71); + $ok = FreshRSS_FormAuth::checkCredentials( + $username, $hash, $password + ); + self::assertFalse($ok); + self::assertSame('', Minz_Log::getLastLog()); + } + + public function testAuthWithValidUsernameAndCorrectCredentialsAndWrongPassAlgorithmFail(): void { + $username = 'admin'; + $password = '1234567'; + $hash = crypt($password, 'ab'); // Uses `password_verify()` compatible algorithm + $ok = FreshRSS_FormAuth::checkCredentials( + $username, $hash, $password + ); + self::assertFalse($ok); + self::assertSame("Invalid hash format: user={$username}", Minz_Log::getLastLog()); + } + + public function testAuthWithValidUsernameAndAnyCredentialsAndMalformedBcryptHashFail(): void { + $username = 'admin'; + $password = 'whatever'; + $hash = '$2y$04$00000000$'; // See: https://github.com/php/php-src/security/advisories/GHSA-7fj2-8x79-rjf4 + $ok = FreshRSS_FormAuth::checkCredentials( + $username, $hash, $password + ); + self::assertFalse($ok); + self::assertSame("Invalid hash format: user={$username}", Minz_Log::getLastLog()); + } + + public function testAuthWithValidUsernameAndAnyCredentialsAndEmptyHashFail(): void { + $username = 'admin'; + $password = 'whatever'; + $hash = ''; + $ok = FreshRSS_FormAuth::checkCredentials( + $username, $hash, $password + ); + self::assertFalse($ok); + self::assertSame("Invalid credential parameters: user={$username}", Minz_Log::getLastLog()); // `ctype_graph($hash)` returns false on empty string values + } + + public function testAuthWithValidUsernameAndCorrectHashAsPasswordFail(): void { + $username = 'admin'; + $password = 'whatever'; + $hash = FreshRSS_password_Util::hash($password); + $ok = FreshRSS_FormAuth::checkCredentials( + $username, $hash, $hash + ); + self::assertFalse($ok); + self::assertSame('', Minz_Log::getLastLog()); + } +} diff --git a/tests/app/Utils/passwordUtilTest.php b/tests/app/Utils/passwordUtilTest.php deleted file mode 100644 index 2d98c5051..000000000 --- a/tests/app/Utils/passwordUtilTest.php +++ /dev/null @@ -1,28 +0,0 @@ -