[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/administrator/components/com_users/src/Model/ -> BackupcodesModel.php (source)

   1  <?php
   2  
   3  /**
   4   * @package    Joomla.Administrator
   5   * @subpackage com_users
   6   *
   7   * @copyright  (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
   8   * @license    GNU General Public License version 2 or later; see LICENSE.txt
   9   */
  10  
  11  namespace Joomla\Component\Users\Administrator\Model;
  12  
  13  use Joomla\CMS\Crypt\Crypt;
  14  use Joomla\CMS\Date\Date;
  15  use Joomla\CMS\Factory;
  16  use Joomla\CMS\Language\Text;
  17  use Joomla\CMS\MVC\Model\BaseDatabaseModel;
  18  use Joomla\CMS\User\User;
  19  use Joomla\CMS\User\UserFactoryInterface;
  20  use Joomla\Component\Users\Administrator\Table\MfaTable;
  21  
  22  // phpcs:disable PSR1.Files.SideEffects
  23  \defined('_JEXEC') or die;
  24  // phpcs:enable PSR1.Files.SideEffects
  25  
  26  /**
  27   * Model for managing backup codes
  28   *
  29   * @since 4.2.0
  30   */
  31  class BackupcodesModel extends BaseDatabaseModel
  32  {
  33      /**
  34       * Caches the backup codes per user ID
  35       *
  36       * @var  array
  37       * @since 4.2.0
  38       */
  39      protected $cache = [];
  40  
  41      /**
  42       * Get the backup codes record for the specified user
  43       *
  44       * @param   User|null   $user   The user in question. Use null for the currently logged in user.
  45       *
  46       * @return  MfaTable|null  Record object or null if none is found
  47       * @throws  \Exception
  48       * @since 4.2.0
  49       */
  50      public function getBackupCodesRecord(User $user = null): ?MfaTable
  51      {
  52          // Make sure I have a user
  53          if (empty($user)) {
  54              $user = Factory::getApplication()->getIdentity() ?:
  55                  Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0);
  56          }
  57  
  58          /** @var MfaTable $record */
  59          $record = $this->getTable('Mfa', 'Administrator');
  60          $loaded = $record->load(
  61              [
  62                  'user_id' => $user->id,
  63                  'method'  => 'backupcodes',
  64              ]
  65          );
  66  
  67          if (!$loaded) {
  68              $record = null;
  69          }
  70  
  71          return $record;
  72      }
  73  
  74      /**
  75       * Generate a new set of backup codes for the specified user. The generated codes are immediately saved to the
  76       * database and the internal cache is updated.
  77       *
  78       * @param   User|null   $user   Which user to generate codes for?
  79       *
  80       * @return void
  81       * @throws \Exception
  82       * @since 4.2.0
  83       */
  84      public function regenerateBackupCodes(User $user = null): void
  85      {
  86          // Make sure I have a user
  87          if (empty($user)) {
  88              $user = Factory::getApplication()->getIdentity() ?:
  89                  Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0);
  90          }
  91  
  92          // Generate backup codes
  93          $backupCodes = [];
  94  
  95          for ($i = 0; $i < 10; $i++) {
  96              // Each backup code is 2 groups of 4 digits
  97              $backupCodes[$i] = sprintf('%04u%04u', random_int(0, 9999), random_int(0, 9999));
  98          }
  99  
 100          // Save the backup codes to the database and update the cache
 101          $this->saveBackupCodes($backupCodes, $user);
 102      }
 103  
 104      /**
 105       * Saves the backup codes to the database
 106       *
 107       * @param   array       $codes   An array of exactly 10 elements
 108       * @param   User|null   $user    The user for which to save the backup codes
 109       *
 110       * @return  boolean
 111       * @throws  \Exception
 112       * @since 4.2.0
 113       */
 114      public function saveBackupCodes(array $codes, ?User $user = null): bool
 115      {
 116          // Make sure I have a user
 117          if (empty($user)) {
 118              $user = Factory::getApplication()->getIdentity() ?:
 119                  Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0);
 120          }
 121  
 122          // Try to load existing backup codes
 123          $existingCodes = $this->getBackupCodes($user);
 124          $jNow          = Date::getInstance();
 125  
 126          /** @var MfaTable $record */
 127          $record = $this->getTable('Mfa', 'Administrator');
 128  
 129          if (is_null($existingCodes)) {
 130              $record->reset();
 131  
 132              $newData = [
 133                  'user_id'    => $user->id,
 134                  'title'      => Text::_('COM_USERS_PROFILE_OTEPS'),
 135                  'method'     => 'backupcodes',
 136                  'default'    => 0,
 137                  'created_on' => $jNow->toSql(),
 138                  'options'    => $codes,
 139              ];
 140          } else {
 141              $record->load(
 142                  [
 143                      'user_id' => $user->id,
 144                      'method'  => 'backupcodes',
 145                  ]
 146              );
 147  
 148              $newData = [
 149                  'options' => $codes,
 150              ];
 151          }
 152  
 153          $saved = $record->save($newData);
 154  
 155          if (!$saved) {
 156              return false;
 157          }
 158  
 159          // Finally, update the cache
 160          $this->cache[$user->id] = $codes;
 161  
 162          return true;
 163      }
 164  
 165      /**
 166       * Returns the backup codes for the specified user. Cached values will be preferentially returned, therefore you
 167       * MUST go through this model's Methods ONLY when dealing with backup codes.
 168       *
 169       * @param   User|null   $user   The user for which you want the backup codes
 170       *
 171       * @return  array|null  The backup codes, or null if they do not exist
 172       * @throws  \Exception
 173       * @since 4.2.0
 174       */
 175      public function getBackupCodes(User $user = null): ?array
 176      {
 177          // Make sure I have a user
 178          if (empty($user)) {
 179              $user = Factory::getApplication()->getIdentity() ?:
 180                  Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0);
 181          }
 182  
 183          if (isset($this->cache[$user->id])) {
 184              return $this->cache[$user->id];
 185          }
 186  
 187          // If there is no cached record try to load it from the database
 188          $this->cache[$user->id] = null;
 189  
 190          // Try to load the record
 191          /** @var MfaTable $record */
 192          $record = $this->getTable('Mfa', 'Administrator');
 193          $loaded = $record->load(
 194              [
 195                  'user_id' => $user->id,
 196                  'method'  => 'backupcodes',
 197              ]
 198          );
 199  
 200          if ($loaded) {
 201              $this->cache[$user->id] = $record->options;
 202          }
 203  
 204          return $this->cache[$user->id];
 205      }
 206  
 207      /**
 208       * Check if the provided string is a backup code. If it is, it will be removed from the list (replaced with an empty
 209       * string) and the codes will be saved to the database. All comparisons are performed in a timing safe manner.
 210       *
 211       * @param   string      $code   The code to check
 212       * @param   User|null   $user   The user to check against
 213       *
 214       * @return  boolean
 215       * @throws  \Exception
 216       * @since 4.2.0
 217       */
 218      public function isBackupCode($code, ?User $user = null): bool
 219      {
 220          // Load the backup codes
 221          $codes = $this->getBackupCodes($user) ?: array_fill(0, 10, '');
 222  
 223          // Keep only the numbers in the provided $code
 224          $code = filter_var($code, FILTER_SANITIZE_NUMBER_INT);
 225          $code = trim($code);
 226  
 227          // Check if the code is in the array. We always check against ten codes to prevent timing attacks which
 228          // determine the amount of codes.
 229          $result = false;
 230  
 231          // The two arrays let us always add an element to an array, therefore having PHP expend the same amount of time
 232          // for the correct code, the incorrect codes and the fake codes.
 233          $newArray   = [];
 234          $dummyArray = [];
 235  
 236          $realLength = count($codes);
 237          $restLength = 10 - $realLength;
 238  
 239          for ($i = 0; $i < $realLength; $i++) {
 240              if (hash_equals($codes[$i], $code)) {
 241                  // This may seem redundant but makes sure both branches of the if-block are isochronous
 242                  $result       = $result || true;
 243                  $newArray[]   = '';
 244                  $dummyArray[] = $codes[$i];
 245              } else {
 246                  // This may seem redundant but makes sure both branches of the if-block are isochronous
 247                  $result       = $result || false;
 248                  $dummyArray[] = '';
 249                  $newArray[]   = $codes[$i];
 250              }
 251          }
 252  
 253          /**
 254           * This is an intentional waste of time, symmetrical to the code above, making sure
 255           * evaluating each of the total of ten elements takes the same time. This code should never
 256           * run UNLESS someone messed up with our backup codes array and it no longer contains 10
 257           * elements.
 258           */
 259          $otherResult = false;
 260  
 261          $temp1 = '';
 262  
 263          for ($i = 0; $i < 10; $i++) {
 264              $temp1[$i] = random_int(0, 99999999);
 265          }
 266  
 267          for ($i = 0; $i < $restLength; $i++) {
 268              if (Crypt::timingSafeCompare($temp1[$i], $code)) {
 269                  $otherResult  = $otherResult || true;
 270                  $newArray[]   = '';
 271                  $dummyArray[] = $temp1[$i];
 272              } else {
 273                  $otherResult  = $otherResult || false;
 274                  $newArray[]   = '';
 275                  $dummyArray[] = $temp1[$i];
 276              }
 277          }
 278  
 279          // This last check makes sure than an empty code does not validate
 280          $result = $result && !hash_equals('', $code);
 281  
 282          // Save the backup codes
 283          $this->saveBackupCodes($newArray, $user);
 284  
 285          // Finally return the result
 286          return $result;
 287      }
 288  }


Generated: Wed Sep 7 05:41:13 2022 Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer