]> git.mxchange.org Git - friendica.git/commitdiff
Add password length limit if using the Blowfish hashing algorithm
authorHypolite Petovan <hypolite@mrpetovan.com>
Mon, 1 Aug 2022 15:42:10 +0000 (11:42 -0400)
committerHypolite Petovan <hypolite@mrpetovan.com>
Mon, 1 Aug 2022 16:12:18 +0000 (12:12 -0400)
- Add new page to reset a password that would be too long
- Add support for pattern parameter in field_password

src/Model/User.php
src/Module/Security/PasswordTooLong.php [new file with mode: 0644]
src/Module/Settings/Account.php
src/Security/Authentication.php
static/routes.config.php
view/templates/field_password.tpl
view/templates/security/password_too_long.tpl [new file with mode: 0644]
view/theme/frio/templates/field_password.tpl

index 1e002aa3f895a1aaaa0b7738e7507ccf690fdf8d..574830603e816395364903e16db9c68b9fef18fc 100644 (file)
@@ -735,6 +735,29 @@ class User
                return password_hash($password, PASSWORD_DEFAULT);
        }
 
+       /**
+        * Allowed characters are a-z, A-Z, 0-9 and special characters except white spaces, accentuated letters and colon (:).
+        *
+        * Password length is limited to 72 characters if the current default password hashing algorithm is Blowfish.
+        * From the manual: "Using the PASSWORD_BCRYPT as the algorithm, will result in the password parameter being
+        * truncated to a maximum length of 72 bytes."
+        *
+        * @see https://www.php.net/manual/en/function.password-hash.php#refsect1-function.password-hash-parameters
+        *
+        * @param string|null $delimiter Whether the regular expression is meant to be wrapper in delimiter characters
+        * @return string
+        */
+       public static function getPasswordRegExp(string $delimiter = null): string
+       {
+               $allowed_characters = '!"#$%&\'()*+,-./;<=>?@[\]^_`{|}~';
+
+               if ($delimiter) {
+                       $allowed_characters = preg_quote($allowed_characters, $delimiter);
+               }
+
+               return '^[a-zA-Z0-9' . $allowed_characters . ']' . (PASSWORD_DEFAULT !== PASSWORD_BCRYPT ? '{1,72}' : '+') . '$';
+       }
+
        /**
         * Updates a user row with a new plaintext password
         *
@@ -755,9 +778,11 @@ class User
                        throw new Exception(DI::l10n()->t('The new password has been exposed in a public data dump, please choose another.'));
                }
 
-               $allowed_characters = '!"#$%&\'()*+,-./;<=>?@[\]^_`{|}~';
+               if (PASSWORD_DEFAULT === PASSWORD_BCRYPT && strlen($password) > 72) {
+                       throw new Exception(DI::l10n()->t('The password length is limited to 72 characters.'));
+               }
 
-               if (!preg_match('/^[a-z0-9' . preg_quote($allowed_characters, '/') . ']+$/i', $password)) {
+               if (!preg_match('/' . self::getPasswordRegExp('/') . '/', $password)) {
                        throw new Exception(DI::l10n()->t('The password can\'t contain accentuated letters, white spaces or colons (:)'));
                }
 
diff --git a/src/Module/Security/PasswordTooLong.php b/src/Module/Security/PasswordTooLong.php
new file mode 100644 (file)
index 0000000..9de4a34
--- /dev/null
@@ -0,0 +1,103 @@
+<?php
+/**
+ * @copyright Copyright (C) 2010-2022, the Friendica project
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace Friendica\Module\Security;
+
+use Friendica\App;
+use Friendica\Core\L10n;
+use Friendica\Core\Renderer;
+use Friendica\Database\DBA;
+use Friendica\Model\User;
+use Friendica\Module\Response;
+use Friendica\Navigation\SystemMessages;
+use Friendica\Util\Profiler;
+use Psr\Log\LoggerInterface;
+
+class PasswordTooLong extends \Friendica\BaseModule
+{
+       /** @var SystemMessages */
+       private $sysmsg;
+
+       public function __construct(SystemMessages $sysmsg, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = [])
+       {
+               parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
+
+               $this->sysmsg = $sysmsg;
+       }
+
+       protected function post(array $request = [])
+       {
+               $newpass = $request['password'];
+               $confirm = $request['password_confirm'];
+
+               try {
+                       if ($newpass != $confirm) {
+                               throw new \Exception($this->l10n->t('Passwords do not match.'));
+                       }
+
+                       //  check if the old password was supplied correctly before changing it to the new value
+                       User::getIdFromPasswordAuthentication(local_user(), $request['password_current']);
+
+                       if (strlen($request['password_current']) <= 72) {
+                               throw new \Exception($this->l10n->t('Password does not need changing.'));
+                       }
+
+                       $result = User::updatePassword(local_user(), $newpass);
+                       if (!DBA::isResult($result)) {
+                               throw new \Exception($this->l10n->t('Password update failed. Please try again.'));
+                       }
+
+                       $this->sysmsg->addInfo($this->l10n->t('Password changed.'));
+
+                       $this->baseUrl->redirect($request['return_url'] ?? '');
+               } catch (\Exception $e) {
+                       $this->sysmsg->addNotice($e->getMessage());
+                       $this->sysmsg->addNotice($this->l10n->t('Password unchanged.'));
+               }
+       }
+
+       protected function content(array $request = []): string
+       {
+               // Nothing to do here
+               if (PASSWORD_DEFAULT !== PASSWORD_BCRYPT) {
+                       $this->baseUrl->redirect();
+               }
+
+               $tpl = Renderer::getMarkupTemplate('security/password_too_long.tpl');
+               $o   = Renderer::replaceMacros($tpl, [
+                       '$l10n' => [
+                               'ptitle' => $this->l10n->t('Password Too Long'),
+                               'desc'   => $this->l10n->t('Since version 2022.09, we\'ve realized that any password longer than 72 characters is truncated during hashing. To prevent any confusion about this behavior, please update your password to be fewer or equal to 72 characters.'),
+                               'submit' => $this->l10n->t('Update Password'),
+                       ],
+
+                       '$baseurl'             => $this->baseUrl->get(true),
+                       '$form_security_token' => self::getFormSecurityToken('security/password_too_long'),
+                       '$return_url'          => $request['return_url'] ?? '',
+
+                       '$password_current' => ['password_current', $this->l10n->t('Current Password:'), '', $this->l10n->t('Your current password to confirm the changes'), 'required', 'autocomplete="off"'],
+                       '$password'         => ['password', $this->l10n->t('New Password:'), '', $this->l10n->t('Allowed characters are a-z, A-Z, 0-9 and special characters except white spaces, accentuated letters and colon (:).') . ' ' . $this->l10n->t('Password length is limited to 72 characters.'), 'required', 'autocomplete="off"', User::getPasswordRegExp()],
+                       '$password_confirm' => ['password_confirm', $this->l10n->t('Confirm:'), '', '', 'required', 'autocomplete="off"'],
+               ]);
+
+               return $o;
+       }
+}
index 33a5feff492d3fba72db77146aac34dd03c16cb5..d20827e6ab7b669b10dfeb392f7208cd7fd377c8 100644 (file)
@@ -551,6 +551,9 @@ class Account extends BaseSettings
 
                $notify_type = DI::pConfig()->get(local_user(), 'system', 'notify_type');
 
+               $passwordRules = DI::l10n()->t('Allowed characters are a-z, A-Z, 0-9 and special characters except white spaces, accentuated letters and colon (:).')
+                       . (PASSWORD_DEFAULT === PASSWORD_BCRYPT ? ' ' . DI::l10n()->t('Password length is limited to 72 characters.') : '');
+
                $tpl = Renderer::getMarkupTemplate('settings/account.tpl');
                $o   = Renderer::replaceMacros($tpl, [
                        '$ptitle' => DI::l10n()->t('Account Settings'),
@@ -563,7 +566,7 @@ class Account extends BaseSettings
                        '$open'                => $this->parameters['open'] ?? 'password',
 
                        '$h_pass'        => DI::l10n()->t('Password Settings'),
-                       '$password1'     => ['password', DI::l10n()->t('New Password:'), '', DI::l10n()->t('Allowed characters are a-z, A-Z, 0-9 and special characters except white spaces, accentuated letters and colon (:).'), false, 'autocomplete="off"'],
+                       '$password1'     => ['password', DI::l10n()->t('New Password:'), '', $passwordRules, false, 'autocomplete="off"', User::getPasswordRegExp()],
                        '$password2'     => ['confirm', DI::l10n()->t('Confirm:'), '', DI::l10n()->t('Leave password fields blank unless changing'), false, 'autocomplete="off"'],
                        '$password3'     => ['opassword', DI::l10n()->t('Current Password:'), '', DI::l10n()->t('Your current password to confirm the changes'), false, 'autocomplete="off"'],
                        '$password4'     => ['mpassword', DI::l10n()->t('Password:'), '', DI::l10n()->t('Your current password to confirm the changes of the email address'), false, 'autocomplete="off"'],
index d1e64534c6bb176aba00d329255d32faf896ec54..42dc02340490b397864a73a74fc97ee97ea4fd36 100644 (file)
@@ -291,8 +291,14 @@ class Authentication
                        $this->dba->update('user', ['openid' => $openid_identity, 'openidserver' => $openid_server], ['uid' => $record['uid']]);
                }
 
-               $this->setForUser($a, $record, true, true);
+               /**
+                * @see User::getPasswordRegExp()
+                */
+               if (PASSWORD_DEFAULT === PASSWORD_BCRYPT && strlen($password) > 72) {
+                       $return_path = '/security/password_too_long?' . http_build_query(['return_path' => $return_path]);
+               }
 
+               $this->setForUser($a, $record, true, true);
 
                $this->baseUrl->redirect($return_path);
        }
index ce41c23d3a60c208f1fadf9f2178f623843cc2ab..f9935a53f40e3ea0f8ab6eeb5265e92d4ef7f33a 100644 (file)
@@ -549,6 +549,10 @@ return [
                '/{type:users}/{guid}' => [Module\Diaspora\Receive::class, [        R::POST]],
        ],
 
+       '/security' => [
+               '/password_too_long' => [Module\Security\PasswordTooLong::class, [R::GET, R::POST]],
+       ],
+
        '/settings' => [
                '[/]'         => [Module\Settings\Account::class,               [R::GET, R::POST]],
                '/account' => [
index 6295880621f3c747f48422ea5b90bcb792341a00..07241fb11baa988a73763a9777ffcde7b08af879 100644 (file)
@@ -1,7 +1,7 @@
        
        <div class="field password" id="wrapper_{{$field.0}}">
                <label for="id_{{$field.0}}">{{$field.1}}{{if $field.4}} <span class="required" title="{{$field.4}}">*</span>{{/if}}</label>
-               <input type="password" name="{{$field.0}}" id="id_{{$field.0}}" value="{{$field.2 nofilter}}"{{if $field.4}} required{{/if}}{{if $field.5 eq "autofocus"}} autofocus{{/if}} aria-describedby="{{$field.0}}_tip">
+               <input type="password" name="{{$field.0}}" id="id_{{$field.0}}" value="{{$field.2 nofilter}}"{{if $field.4}} required{{/if}}{{if $field.5 eq "autofocus"}} autofocus{{elseif $field.5}} {{$field.5}}{{/if}}{{if $field.6}} pattern="(($field.6}}"{{/if}} aria-describedby="{{$field.0}}_tip">
                {{if $field.3}}
                <span class="field_help" role="tooltip" id="{{$field.0}}_tip">{{$field.3 nofilter}}</span>
                {{/if}}
diff --git a/view/templates/security/password_too_long.tpl b/view/templates/security/password_too_long.tpl
new file mode 100644 (file)
index 0000000..81e6985
--- /dev/null
@@ -0,0 +1,22 @@
+<div class="generic-page-wrapper">
+       <h1>{{$l10n.ptitle}}</h1>
+
+       <div id="settings-nick-wrapper">
+               <div id="settings-nickname-desc" class="info-message">{{$l10n.desc}}</div>
+       </div>
+       <div id="settings-nick-end"></div>
+
+       <div id="settings-form">
+               <form class="settings-content-block" action="security/password_too_long" method="post" autocomplete="off" enctype="multipart/form-data">
+                       <input type="hidden" name="form_security_token" value="{{$form_security_token}}">
+                       <input type="hidden" name="return_url" value="{{$return_url}}">
+                       {{include file="field_password.tpl" field=$password_current}}
+                       {{include file="field_password.tpl" field=$password}}
+                       {{include file="field_password.tpl" field=$password_confirm}}
+
+                       <div class="settings-submit-wrapper">
+                               <button type="submit" name="password-submit" class="btn btn-primary" value="{{$l10n.submit}}">{{$l10n.submit}}</button>
+                       </div>
+               </form>
+       </div>
+</div>
index df29b2fda264bf62d2ed94949d00ca09373b1fa7..25a7d0c4cecfcbac625006c2adb42a0f907a9f7f 100644 (file)
@@ -1,7 +1,7 @@
 
 <div id="id_{{$field.0}}_wrapper" class="form-group field input password">
        <label for="id_{{$field.0}}" id="label_{{$field.0}}">{{$field.1}}{{if $field.4}} <span class="required" title="{{$field.4}}">*</span>{{/if}}</label>
-       <input class="form-control" name="{{$field.0}}" id="id_{{$field.0}}" type="password" value="{{$field.2 nofilter}}" {{if $field.4}} required{{/if}}{{if $field.5 eq "autofocus"}} autofocus{{elseif $field.5}} {{$field.5}}{{/if}} aria-describedby="{{$field.0}}_tip">
+       <input class="form-control" name="{{$field.0}}" id="id_{{$field.0}}" type="password" value="{{$field.2 nofilter}}" {{if $field.4}} required{{/if}}{{if $field.5 eq "autofocus"}} autofocus{{elseif $field.5}} {{$field.5}}{{/if}}{{if $field.6}} pattern="{{$field.6}}"{{/if}} aria-describedby="{{$field.0}}_tip">
        {{if $field.3}}
        <span class="help-block" id="{{$field.0}}_tip" role="tooltip">{{$field.3 nofilter}}</span>
        {{/if}}