@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Simplify and correct some challenge TTL lockout code

Summary:
Depends on D19889. Ref T13222. Some of this logic is either not-quite-right or a little more complicated than it needs to be.

Currently, we TTL TOTP challenges after three timesteps -- once the current code could no longer be used. But we actually have to TTL it after five timesteps -- once the most-future acceptable code could no longer be used. Otherwise, you can enter the most-future code now (perhaps the attacker compromises NTP and skews the server clock back by 75 seconds) and then an attacker can re-use it in three timesteps.

Generally, simplify things a bit and trust TTLs more. This also makes the "wait" dialog friendlier since we can give users an exact number of seconds.

The overall behavior here is still a little odd because we don't actually require you to respond to the challenge you were issued (right now, we check that the response is valid whenever you submit it, not that it's a valid response to the challenge we issued), but that will change in a future diff. This is just moving us generally in the right direction, and doesn't yet lock everything down properly.

Test Plan:
- Added a little snippet to the control caption to list all the valid codes to make this easier:

```
$key = new PhutilOpaqueEnvelope($config->getFactorSecret());
$valid = array();
foreach ($this->getAllowedTimesteps() as $step) {
$valid[] = self::getTOTPCode($key, $step);
}

$control->setCaption(
pht(
'Valid Codes: '.implode(', ', $valid)));
```

- Used the most-future code to sign `L3`.
- Verified that `L4` did not unlock until the code for `L3` left the activation window.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13222

Differential Revision: https://secure.phabricator.com/D19890

+37 -30
+1 -1
src/applications/auth/action/PhabricatorAuthTryFactorAction.php
··· 9 9 } 10 10 11 11 public function getScoreThreshold() { 12 - return 10 / phutil_units('1 hour in seconds'); 12 + return 100 / phutil_units('1 hour in seconds'); 13 13 } 14 14 15 15 public function getLimitExplanation() {
+36 -29
src/applications/auth/factor/PhabricatorTOTPAuthFactor.php
··· 155 155 PhabricatorUser $viewer, 156 156 array $challenges) { 157 157 158 - $now = $this->getCurrentTimestep(); 158 + $current_step = $this->getCurrentTimestep(); 159 159 160 160 // If we already issued a valid challenge, don't issue a new one. 161 161 if ($challenges) { 162 162 return array(); 163 163 } 164 164 165 - // Otherwise, generate a new challenge for the current timestep. It TTLs 166 - // after it would fall off the bottom of the window. 167 - $timesteps = $this->getAllowedTimesteps(); 168 - $min_step = min($timesteps); 165 + // Otherwise, generate a new challenge for the current timestep and compute 166 + // the TTL. 169 167 168 + // When computing the TTL, note that we accept codes within a certain 169 + // window of the challenge timestep to account for clock skew and users 170 + // needing time to enter codes. 171 + 172 + // We don't want this challenge to expire until after all valid responses 173 + // to it are no longer valid responses to any other challenge we might 174 + // issue in the future. If the challenge expires too quickly, we may issue 175 + // a new challenge which can accept the same TOTP code response. 176 + 177 + // This means that we need to keep this challenge alive for double the 178 + // window size: if we're currently at timestep 3, the user might respond 179 + // with the code for timestep 5. This is valid, since timestep 5 is within 180 + // the window for timestep 3. 181 + 182 + // But the code for timestep 5 can be used to respond at timesteps 3, 4, 5, 183 + // 6, and 7. To prevent any valid response to this challenge from being 184 + // used again, we need to keep this challenge active until timestep 8. 185 + 186 + $window_size = $this->getTimestepWindowSize(); 170 187 $step_duration = $this->getTimestepDuration(); 171 - $ttl_steps = ($now - $min_step) + 1; 188 + 189 + $ttl_steps = ($window_size * 2) + 1; 172 190 $ttl_seconds = ($ttl_steps * $step_duration); 173 191 174 192 return array( 175 193 $this->newChallenge($config, $viewer) 176 - ->setChallengeKey($now) 194 + ->setChallengeKey($current_step) 177 195 ->setChallengeTTL(PhabricatorTime::getNow() + $ttl_seconds), 178 196 ); 179 197 } ··· 216 234 // nearby timestep, require that it was issued to the current session. 217 235 // This is defusing attacks where you (broadly) look at someone's phone 218 236 // and type the code in more quickly than they do. 219 - 220 - $step_duration = $this->getTimestepDuration(); 221 - $now = $this->getCurrentTimestep(); 222 - $timesteps = $this->getAllowedTimesteps(); 223 - $timesteps = array_fuse($timesteps); 224 - $min_step = min($timesteps); 225 - 226 237 $session_phid = $viewer->getSession()->getPHID(); 238 + $now = PhabricatorTime::getNow(); 227 239 228 240 $engine = $config->getSessionEngine(); 229 241 $workflow_key = $engine->getWorkflowKey(); 230 242 231 243 foreach ($challenges as $challenge) { 232 244 $challenge_timestep = (int)$challenge->getChallengeKey(); 233 - 234 - // This challenge isn't for one of the timesteps you'd be able to respond 235 - // to if you submitted the form right now, so we're good to keep going. 236 - if (!isset($timesteps[$challenge_timestep])) { 237 - continue; 238 - } 239 - 240 - // This is the number of timesteps you need to wait for the problem 241 - // timestep to leave the window, rounded up. 242 - $wait_steps = ($challenge_timestep - $min_step) + 1; 243 - $wait_duration = ($wait_steps * $step_duration); 245 + $wait_duration = ($challenge->getChallengeTTL() - $now) + 1; 244 246 245 247 if ($challenge->getSessionPHID() !== $session_phid) { 246 248 return $this->newResult() ··· 248 250 ->setErrorMessage( 249 251 pht( 250 252 'This factor recently issued a challenge to a different login '. 251 - 'session. Wait %s seconds for the code to cycle, then try '. 253 + 'session. Wait %s second(s) for the code to cycle, then try '. 252 254 'again.', 253 255 new PhutilNumber($wait_duration))); 254 256 } ··· 259 261 ->setErrorMessage( 260 262 pht( 261 263 'This factor recently issued a challenge for a different '. 262 - 'workflow. Wait %s seconds for the code to cycle, then try '. 264 + 'workflow. Wait %s second(s) for the code to cycle, then try '. 263 265 'again.', 264 266 new PhutilNumber($wait_duration))); 265 267 } ··· 423 425 } 424 426 425 427 private function getAllowedTimesteps() { 426 - $now = $this->getCurrentTimestep(); 427 - return range($now - 2, $now + 2); 428 + $current_step = $this->getCurrentTimestep(); 429 + $window = $this->getTimestepWindowSize(); 430 + return range($current_step - $window, $current_step + $window); 431 + } 432 + 433 + private function getTimestepWindowSize() { 434 + return 2; 428 435 } 429 436 430 437