[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Sep 7 05:41:13 2022 | Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer |