public function validateSender(PhabricatorMetaMTAReceivedMail $mail, PhabricatorUser $sender)
 {
     parent::validateSender($mail, $sender);
     $parts = $this->matchObjectAddressInMail($mail);
     $pattern = $parts['pattern'];
     try {
         $object = $this->loadObjectFromMail($mail, $sender);
     } catch (PhabricatorPolicyException $policy_exception) {
         throw new PhabricatorMetaMTAReceivedMailProcessingException(MetaMTAReceivedMailStatus::STATUS_POLICY_PROBLEM, pht('This mail is addressed to an object ("%s") you do not have ' . 'permission to see: %s', $pattern, $policy_exception->getMessage()));
     }
     if (!$object) {
         throw new PhabricatorMetaMTAReceivedMailProcessingException(MetaMTAReceivedMailStatus::STATUS_NO_SUCH_OBJECT, pht('This mail is addressed to an object ("%s"), but that object ' . 'does not exist.', $pattern));
     }
     $sender_identifier = $parts['sender'];
     if ($sender_identifier === 'public') {
         if (!PhabricatorEnv::getEnvConfig('metamta.public-replies')) {
             throw new PhabricatorMetaMTAReceivedMailProcessingException(MetaMTAReceivedMailStatus::STATUS_NO_PUBLIC_MAIL, pht('This mail is addressed to the public email address of an object ' . '("%s"), but public replies are not enabled on this Phabricator ' . 'install. An administrator may have recently disabled this ' . 'setting, or you may have replied to an old message. Try ' . 'replying to a more recent message instead.', $pattern));
         }
         $check_phid = $object->getPHID();
     } else {
         if ($sender_identifier != $sender->getID()) {
             throw new PhabricatorMetaMTAReceivedMailProcessingException(MetaMTAReceivedMailStatus::STATUS_USER_MISMATCH, pht('This mail is addressed to the private email address of an object ' . '("%s"), but you are not the user who is authorized to use the ' . 'address you sent mail to. Each private address is unique to the ' . 'user who received the original mail. Try replying to a message ' . 'which was sent directly to you instead.', $pattern));
         }
         $check_phid = $sender->getPHID();
     }
     $expect_hash = self::computeMailHash($object->getMailKey(), $check_phid);
     if (!phutil_hashes_are_identical($expect_hash, $parts['hash'])) {
         throw new PhabricatorMetaMTAReceivedMailProcessingException(MetaMTAReceivedMailStatus::STATUS_HASH_MISMATCH, pht('This mail is addressed to an object ("%s"), but the address is ' . 'not correct (the security hash is wrong). Check that the address ' . 'is correct.', $pattern));
     }
 }
 protected function execute(ConduitAPIRequest $request)
 {
     $client = $request->getValue('client');
     $client_version = (int) $request->getValue('clientVersion');
     $client_description = (string) $request->getValue('clientDescription');
     $client_description = id(new PhutilUTF8StringTruncator())->setMaximumBytes(255)->truncateString($client_description);
     $username = (string) $request->getValue('user');
     // Log the connection, regardless of the outcome of checks below.
     $connection = new PhabricatorConduitConnectionLog();
     $connection->setClient($client);
     $connection->setClientVersion($client_version);
     $connection->setClientDescription($client_description);
     $connection->setUsername($username);
     $connection->save();
     switch ($client) {
         case 'arc':
             $server_version = 6;
             $supported_versions = array($server_version => true, 4 => true, 5 => true);
             if (empty($supported_versions[$client_version])) {
                 if ($server_version < $client_version) {
                     $ex = new ConduitException('ERR-BAD-VERSION');
                     $ex->setErrorDescription(pht("Your '%s' client version is '%d', which is newer than the " . "server version, '%d'. Upgrade your Phabricator install.", 'arc', $client_version, $server_version));
                 } else {
                     $ex = new ConduitException('NEW-ARC-VERSION');
                     $ex->setErrorDescription(pht('A new version of arc is available! You need to upgrade ' . 'to connect to this server (you are running version ' . '%d, the server is running version %d).', $client_version, $server_version));
                 }
                 throw $ex;
             }
             break;
         default:
             // Allow new clients by default.
             break;
     }
     $token = $request->getValue('authToken');
     $signature = $request->getValue('authSignature');
     $user = id(new PhabricatorUser())->loadOneWhere('username = %s', $username);
     if (!$user) {
         throw new ConduitException('ERR-INVALID-USER');
     }
     $session_key = null;
     if ($token && $signature) {
         $threshold = 60 * 15;
         $now = time();
         if (abs($token - $now) > $threshold) {
             throw id(new ConduitException('ERR-INVALID-TOKEN'))->setErrorDescription(pht('The request you submitted is signed with a timestamp, but that ' . 'timestamp is not within %s of the current time. The ' . 'signed timestamp is %s (%s), and the current server time is ' . '%s (%s). This is a difference of %s seconds, but the ' . 'timestamp must differ from the server time by no more than ' . '%s seconds. Your client or server clock may not be set ' . 'correctly.', phutil_format_relative_time($threshold), $token, date('r', $token), $now, date('r', $now), $token - $now, $threshold));
         }
         $valid = sha1($token . $user->getConduitCertificate());
         if (!phutil_hashes_are_identical($valid, $signature)) {
             throw new ConduitException('ERR-INVALID-CERTIFICATE');
         }
         $session_key = id(new PhabricatorAuthSessionEngine())->establishSession(PhabricatorAuthSession::TYPE_CONDUIT, $user->getPHID(), $partial = false);
     } else {
         throw new ConduitException('ERR-NO-CERTIFICATE');
     }
     return array('connectionID' => $connection->getID(), 'sessionKey' => $session_key, 'userPHID' => $user->getPHID());
 }
 private function verifyMessage()
 {
     $api_key = PhabricatorEnv::getEnvConfig('mailgun.api-key');
     $request = $this->getRequest();
     $timestamp = $request->getStr('timestamp');
     $token = $request->getStr('token');
     $sig = $request->getStr('signature');
     $hash = hash_hmac('sha256', $timestamp . $token, $api_key);
     return phutil_hashes_are_identical($sig, $hash);
 }
 public function processRequest(AphrontRequest $request)
 {
     $viewer = $request->getUser();
     $accounts = id(new PhabricatorExternalAccountQuery())->setViewer($viewer)->withUserPHIDs(array($viewer->getPHID()))->requireCapabilities(array(PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT))->execute();
     $identity_phids = mpull($accounts, 'getPHID');
     $identity_phids[] = $viewer->getPHID();
     $sessions = id(new PhabricatorAuthSessionQuery())->setViewer($viewer)->withIdentityPHIDs($identity_phids)->execute();
     $handles = id(new PhabricatorHandleQuery())->setViewer($viewer)->withPHIDs($identity_phids)->execute();
     $current_key = PhabricatorHash::digest($request->getCookie(PhabricatorCookies::COOKIE_SESSION));
     $rows = array();
     $rowc = array();
     foreach ($sessions as $session) {
         $is_current = phutil_hashes_are_identical($session->getSessionKey(), $current_key);
         if ($is_current) {
             $rowc[] = 'highlighted';
             $button = phutil_tag('a', array('class' => 'small grey button disabled'), pht('Current'));
         } else {
             $rowc[] = null;
             $button = javelin_tag('a', array('href' => '/auth/session/terminate/' . $session->getID() . '/', 'class' => 'small grey button', 'sigil' => 'workflow'), pht('Terminate'));
         }
         $hisec = $session->getHighSecurityUntil() - time();
         $rows[] = array($handles[$session->getUserPHID()]->renderLink(), substr($session->getSessionKey(), 0, 6), $session->getType(), $hisec > 0 ? phutil_format_relative_time($hisec) : null, phabricator_datetime($session->getSessionStart(), $viewer), phabricator_date($session->getSessionExpires(), $viewer), $button);
     }
     $table = new AphrontTableView($rows);
     $table->setNoDataString(pht("You don't have any active sessions."));
     $table->setRowClasses($rowc);
     $table->setHeaders(array(pht('Identity'), pht('Session'), pht('Type'), pht('HiSec'), pht('Created'), pht('Expires'), pht('')));
     $table->setColumnClasses(array('wide', 'n', '', 'right', 'right', 'right', 'action'));
     $terminate_icon = id(new PHUIIconView())->setIconFont('fa-exclamation-triangle');
     $terminate_button = id(new PHUIButtonView())->setText(pht('Terminate All Sessions'))->setHref('/auth/session/terminate/all/')->setTag('a')->setWorkflow(true)->setIcon($terminate_icon);
     $header = id(new PHUIHeaderView())->setHeader(pht('Active Login Sessions'))->addActionLink($terminate_button);
     $hisec = $viewer->getSession()->getHighSecurityUntil() - time();
     if ($hisec > 0) {
         $hisec_icon = id(new PHUIIconView())->setIconFont('fa-lock');
         $hisec_button = id(new PHUIButtonView())->setText(pht('Leave High Security'))->setHref('/auth/session/downgrade/')->setTag('a')->setWorkflow(true)->setIcon($hisec_icon);
         $header->addActionLink($hisec_button);
     }
     $panel = id(new PHUIObjectBoxView())->setHeader($header)->setTable($table);
     return $panel;
 }
 public function handleRequest(AphrontRequest $request)
 {
     $viewer = $this->getViewer();
     $id = $request->getURIData('id');
     $is_all = $id === 'all';
     $query = id(new PhabricatorAuthSessionQuery())->setViewer($viewer)->withIdentityPHIDs(array($viewer->getPHID()));
     if (!$is_all) {
         $query->withIDs(array($id));
     }
     $current_key = PhabricatorHash::digest($request->getCookie(PhabricatorCookies::COOKIE_SESSION));
     $sessions = $query->execute();
     foreach ($sessions as $key => $session) {
         $is_current = phutil_hashes_are_identical($session->getSessionKey(), $current_key);
         if ($is_current) {
             // Don't terminate the current login session.
             unset($sessions[$key]);
         }
     }
     $panel_uri = '/settings/panel/sessions/';
     if (!$sessions) {
         return $this->newDialog()->setTitle(pht('No Matching Sessions'))->appendParagraph(pht('There are no matching sessions to terminate.'))->appendParagraph(pht('(You can not terminate your current login session. To ' . 'terminate it, log out.)'))->addCancelButton($panel_uri);
     }
     if ($request->isDialogFormPost()) {
         foreach ($sessions as $session) {
             $session->delete();
         }
         return id(new AphrontRedirectResponse())->setURI($panel_uri);
     }
     if ($is_all) {
         $title = pht('Terminate Sessions?');
         $short = pht('Terminate Sessions');
         $body = pht('Really terminate all sessions? (Your current login session will ' . 'not be terminated.)');
     } else {
         $title = pht('Terminate Session?');
         $short = pht('Terminate Session');
         $body = pht('Really terminate session %s?', phutil_tag('strong', array(), substr($session->getSessionKey(), 0, 6)));
     }
     return $this->newDialog()->setTitle($title)->setShortTitle($short)->appendParagraph($body)->addSubmitButton(pht('Terminate'))->addCancelButton($panel_uri);
 }
 /**
  * Authenticate the client making the request to a Phabricator user account.
  *
  * @param   ConduitAPIRequest Request being executed.
  * @param   dict              Request metadata.
  * @return  null|pair         Null to indicate successful authentication, or
  *                            an error code and error message pair.
  */
 private function authenticateUser(ConduitAPIRequest $api_request, array $metadata)
 {
     $request = $this->getRequest();
     if ($request->getUser()->getPHID()) {
         $request->validateCSRF();
         return $this->validateAuthenticatedUser($api_request, $request->getUser());
     }
     $auth_type = idx($metadata, 'auth.type');
     if ($auth_type === ConduitClient::AUTH_ASYMMETRIC) {
         $host = idx($metadata, 'auth.host');
         if (!$host) {
             return array('ERR-INVALID-AUTH', pht('Request is missing required "%s" parameter.', 'auth.host'));
         }
         // TODO: Validate that we are the host!
         $raw_key = idx($metadata, 'auth.key');
         $public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($raw_key);
         $ssl_public_key = $public_key->toPKCS8();
         // First, verify the signature.
         try {
             $protocol_data = $metadata;
             // TODO: We should stop writing this into the protocol data when
             // processing a request.
             unset($protocol_data['scope']);
             ConduitClient::verifySignature($this->method, $api_request->getAllParameters(), $protocol_data, $ssl_public_key);
         } catch (Exception $ex) {
             return array('ERR-INVALID-AUTH', pht('Signature verification failure. %s', $ex->getMessage()));
         }
         // If the signature is valid, find the user or device which is
         // associated with this public key.
         $stored_key = id(new PhabricatorAuthSSHKeyQuery())->setViewer(PhabricatorUser::getOmnipotentUser())->withKeys(array($public_key))->executeOne();
         if (!$stored_key) {
             return array('ERR-INVALID-AUTH', pht('No user or device is associated with that public key.'));
         }
         $object = $stored_key->getObject();
         if ($object instanceof PhabricatorUser) {
             $user = $object;
         } else {
             if (!$stored_key->getIsTrusted()) {
                 return array('ERR-INVALID-AUTH', pht('The key which signed this request is not trusted. Only ' . 'trusted keys can be used to sign API calls.'));
             }
             if (!PhabricatorEnv::isClusterRemoteAddress()) {
                 return array('ERR-INVALID-AUTH', pht('This request originates from outside of the Phabricator ' . 'cluster address range. Requests signed with trusted ' . 'device keys must originate from within the cluster.'));
             }
             $user = PhabricatorUser::getOmnipotentUser();
             // Flag this as an intracluster request.
             $api_request->setIsClusterRequest(true);
         }
         return $this->validateAuthenticatedUser($api_request, $user);
     } else {
         if ($auth_type === null) {
             // No specified authentication type, continue with other authentication
             // methods below.
         } else {
             return array('ERR-INVALID-AUTH', pht('Provided "%s" ("%s") is not recognized.', 'auth.type', $auth_type));
         }
     }
     $token_string = idx($metadata, 'token');
     if (strlen($token_string)) {
         if (strlen($token_string) != 32) {
             return array('ERR-INVALID-AUTH', pht('API token "%s" has the wrong length. API tokens should be ' . '32 characters long.', $token_string));
         }
         $type = head(explode('-', $token_string));
         $valid_types = PhabricatorConduitToken::getAllTokenTypes();
         $valid_types = array_fuse($valid_types);
         if (empty($valid_types[$type])) {
             return array('ERR-INVALID-AUTH', pht('API token "%s" has the wrong format. API tokens should be ' . '32 characters long and begin with one of these prefixes: %s.', $token_string, implode(', ', $valid_types)));
         }
         $token = id(new PhabricatorConduitTokenQuery())->setViewer(PhabricatorUser::getOmnipotentUser())->withTokens(array($token_string))->withExpired(false)->executeOne();
         if (!$token) {
             $token = id(new PhabricatorConduitTokenQuery())->setViewer(PhabricatorUser::getOmnipotentUser())->withTokens(array($token_string))->withExpired(true)->executeOne();
             if ($token) {
                 return array('ERR-INVALID-AUTH', pht('API token "%s" was previously valid, but has expired.', $token_string));
             } else {
                 return array('ERR-INVALID-AUTH', pht('API token "%s" is not valid.', $token_string));
             }
         }
         // If this is a "cli-" token, it expires shortly after it is generated
         // by default. Once it is actually used, we extend its lifetime and make
         // it permanent. This allows stray tokens to get cleaned up automatically
         // if they aren't being used.
         if ($token->getTokenType() == PhabricatorConduitToken::TYPE_COMMANDLINE) {
             if ($token->getExpires()) {
                 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
                 $token->setExpires(null);
                 $token->save();
                 unset($unguarded);
             }
         }
         // If this is a "clr-" token, Phabricator must be configured in cluster
         // mode and the remote address must be a cluster node.
         if ($token->getTokenType() == PhabricatorConduitToken::TYPE_CLUSTER) {
             if (!PhabricatorEnv::isClusterRemoteAddress()) {
                 return array('ERR-INVALID-AUTH', pht('This request originates from outside of the Phabricator ' . 'cluster address range. Requests signed with cluster API ' . 'tokens must originate from within the cluster.'));
             }
             // Flag this as an intracluster request.
             $api_request->setIsClusterRequest(true);
         }
         $user = $token->getObject();
         if (!$user instanceof PhabricatorUser) {
             return array('ERR-INVALID-AUTH', pht('API token is not associated with a valid user.'));
         }
         return $this->validateAuthenticatedUser($api_request, $user);
     }
     // handle oauth
     $access_token = idx($metadata, 'access_token');
     $method_scope = idx($metadata, 'scope');
     if ($access_token && $method_scope != PhabricatorOAuthServerScope::SCOPE_NOT_ACCESSIBLE) {
         $token = id(new PhabricatorOAuthServerAccessToken())->loadOneWhere('token = %s', $access_token);
         if (!$token) {
             return array('ERR-INVALID-AUTH', pht('Access token does not exist.'));
         }
         $oauth_server = new PhabricatorOAuthServer();
         $valid = $oauth_server->validateAccessToken($token, $method_scope);
         if (!$valid) {
             return array('ERR-INVALID-AUTH', pht('Access token is invalid.'));
         }
         // valid token, so let's log in the user!
         $user_phid = $token->getUserPHID();
         $user = id(new PhabricatorUser())->loadOneWhere('phid = %s', $user_phid);
         if (!$user) {
             return array('ERR-INVALID-AUTH', pht('Access token is for invalid user.'));
         }
         return $this->validateAuthenticatedUser($api_request, $user);
     }
     // Handle sessionless auth.
     // TODO: This is super messy.
     // TODO: Remove this in favor of token-based auth.
     if (isset($metadata['authUser'])) {
         $user = id(new PhabricatorUser())->loadOneWhere('userName = %s', $metadata['authUser']);
         if (!$user) {
             return array('ERR-INVALID-AUTH', pht('Authentication is invalid.'));
         }
         $token = idx($metadata, 'authToken');
         $signature = idx($metadata, 'authSignature');
         $certificate = $user->getConduitCertificate();
         $hash = sha1($token . $certificate);
         if (!phutil_hashes_are_identical($hash, $signature)) {
             return array('ERR-INVALID-AUTH', pht('Authentication is invalid.'));
         }
         return $this->validateAuthenticatedUser($api_request, $user);
     }
     // Handle session-based auth.
     // TODO: Remove this in favor of token-based auth.
     $session_key = idx($metadata, 'sessionKey');
     if (!$session_key) {
         return array('ERR-INVALID-SESSION', pht('Session key is not present.'));
     }
     $user = id(new PhabricatorAuthSessionEngine())->loadUserForSession(PhabricatorAuthSession::TYPE_CONDUIT, $session_key);
     if (!$user) {
         return array('ERR-INVALID-SESSION', pht('Session key is invalid.'));
     }
     return $this->validateAuthenticatedUser($api_request, $user);
 }
 protected function verifyAuthCSRFCode(AphrontRequest $request, $actual)
 {
     $expect = $this->getAuthCSRFCode($request);
     if (!strlen($actual)) {
         throw new Exception(pht('The authentication provider did not return a client state ' . 'parameter in its response, but one was expected. If this ' . 'problem persists, you may need to clear your cookies.'));
     }
     if (!phutil_hashes_are_identical($actual, $expect)) {
         throw new Exception(pht('The authentication provider did not return the correct client ' . 'state parameter in its response. If this problem persists, you may ' . 'need to clear your cookies.'));
     }
 }
 /**
  * Terminate all of a user's login sessions.
  *
  * This is used when users change passwords, linked accounts, or add
  * multifactor authentication.
  *
  * @param PhabricatorUser User whose sessions should be terminated.
  * @param string|null Optionally, one session to keep. Normally, the current
  *   login session.
  *
  * @return void
  */
 public function terminateLoginSessions(PhabricatorUser $user, $except_session = null)
 {
     $sessions = id(new PhabricatorAuthSessionQuery())->setViewer($user)->withIdentityPHIDs(array($user->getPHID()))->execute();
     if ($except_session !== null) {
         $except_session = PhabricatorHash::digest($except_session);
     }
     foreach ($sessions as $key => $session) {
         if ($except_session !== null) {
             $is_except = phutil_hashes_are_identical($session->getSessionKey(), $except_session);
             if ($is_except) {
                 continue;
             }
         }
         $session->delete();
     }
 }
 protected function loadAccountForRegistrationOrLinking($account_key)
 {
     $request = $this->getRequest();
     $viewer = $request->getUser();
     $account = null;
     $provider = null;
     $response = null;
     if (!$account_key) {
         $response = $this->renderError(pht('Request did not include account key.'));
         return array($account, $provider, $response);
     }
     // NOTE: We're using the omnipotent user because the actual user may not
     // be logged in yet, and because we want to tailor an error message to
     // distinguish between "not usable" and "does not exist". We do explicit
     // checks later on to make sure this account is valid for the intended
     // operation. This requires edit permission for completeness and consistency
     // but it won't actually be meaningfully checked because we're using the
     // ominpotent user.
     $account = id(new PhabricatorExternalAccountQuery())->setViewer(PhabricatorUser::getOmnipotentUser())->withAccountSecrets(array($account_key))->needImages(true)->requireCapabilities(array(PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT))->executeOne();
     if (!$account) {
         $response = $this->renderError(pht('No valid linkable account.'));
         return array($account, $provider, $response);
     }
     if ($account->getUserPHID()) {
         if ($account->getUserPHID() != $viewer->getPHID()) {
             $response = $this->renderError(pht('The account you are attempting to register or link is already ' . 'linked to another user.'));
         } else {
             $response = $this->renderError(pht('The account you are attempting to link is already linked ' . 'to your account.'));
         }
         return array($account, $provider, $response);
     }
     $registration_key = $request->getCookie(PhabricatorCookies::COOKIE_REGISTRATION);
     // NOTE: This registration key check is not strictly necessary, because
     // we're only creating new accounts, not linking existing accounts. It
     // might be more hassle than it is worth, especially for email.
     //
     // The attack this prevents is getting to the registration screen, then
     // copy/pasting the URL and getting someone else to click it and complete
     // the process. They end up with an account bound to credentials you
     // control. This doesn't really let you do anything meaningful, though,
     // since you could have simply completed the process yourself.
     if (!$registration_key) {
         $response = $this->renderError(pht('Your browser did not submit a registration key with the request. ' . 'You must use the same browser to begin and complete registration. ' . 'Check that cookies are enabled and try again.'));
         return array($account, $provider, $response);
     }
     // We store the digest of the key rather than the key itself to prevent a
     // theoretical attacker with read-only access to the database from
     // hijacking registration sessions.
     $actual = $account->getProperty('registrationKey');
     $expect = PhabricatorHash::digest($registration_key);
     if (!phutil_hashes_are_identical($actual, $expect)) {
         $response = $this->renderError(pht('Your browser submitted a different registration key than the one ' . 'associated with this account. You may need to clear your cookies.'));
         return array($account, $provider, $response);
     }
     $other_account = id(new PhabricatorExternalAccount())->loadAllWhere('accountType = %s AND accountDomain = %s AND accountID = %s
     AND id != %d', $account->getAccountType(), $account->getAccountDomain(), $account->getAccountID(), $account->getID());
     if ($other_account) {
         $response = $this->renderError(pht('The account you are attempting to register with already belongs ' . 'to another user.'));
         return array($account, $provider, $response);
     }
     $provider = PhabricatorAuthProvider::getEnabledProviderByKey($account->getProviderKey());
     if (!$provider) {
         $response = $this->renderError(pht('The account you are attempting to register with uses a nonexistent ' . 'or disabled authentication provider (with key "%s"). An ' . 'administrator may have recently disabled this provider.', $account->getProviderKey()));
         return array($account, $provider, $response);
     }
     return array($account, $provider, null);
 }
 /**
  * Verify that a password matches a hash.
  *
  * The default implementation checks for equality; if a hasher embeds salt in
  * hashes it should override this method and perform a salt-aware comparison.
  *
  * @param   PhutilOpaqueEnvelope  Password to compare.
  * @param   PhutilOpaqueEnvelope  Bare password hash.
  * @return  bool                  True if the passwords match.
  * @task hasher
  */
 protected function verifyPassword(PhutilOpaqueEnvelope $password, PhutilOpaqueEnvelope $hash)
 {
     $actual_hash = $this->getPasswordHash($password)->openEnvelope();
     $expect_hash = $hash->openEnvelope();
     return phutil_hashes_are_identical($actual_hash, $expect_hash);
 }
Beispiel #11
0
 public function validateCSRFToken($token)
 {
     // We expect a BREACH-mitigating token. See T3684.
     $breach_prefix = self::CSRF_BREACH_PREFIX;
     $breach_prelen = strlen($breach_prefix);
     if (strncmp($token, $breach_prefix, $breach_prelen) !== 0) {
         return false;
     }
     $salt = substr($token, $breach_prelen, self::CSRF_SALT_LENGTH);
     $token = substr($token, $breach_prelen + self::CSRF_SALT_LENGTH);
     // When the user posts a form, we check that it contains a valid CSRF token.
     // Tokens cycle each hour (every CSRF_CYLCE_FREQUENCY seconds) and we accept
     // either the current token, the next token (users can submit a "future"
     // token if you have two web frontends that have some clock skew) or any of
     // the last 6 tokens. This means that pages are valid for up to 7 hours.
     // There is also some Javascript which periodically refreshes the CSRF
     // tokens on each page, so theoretically pages should be valid indefinitely.
     // However, this code may fail to run (if the user loses their internet
     // connection, or there's a JS problem, or they don't have JS enabled).
     // Choosing the size of the window in which we accept old CSRF tokens is
     // an issue of balancing concerns between security and usability. We could
     // choose a very narrow (e.g., 1-hour) window to reduce vulnerability to
     // attacks using captured CSRF tokens, but it's also more likely that real
     // users will be affected by this, e.g. if they close their laptop for an
     // hour, open it back up, and try to submit a form before the CSRF refresh
     // can kick in. Since the user experience of submitting a form with expired
     // CSRF is often quite bad (you basically lose data, or it's a big pain to
     // recover at least) and I believe we gain little additional protection
     // by keeping the window very short (the overwhelming value here is in
     // preventing blind attacks, and most attacks which can capture CSRF tokens
     // can also just capture authentication information [sniffing networks]
     // or act as the user [xss]) the 7 hour default seems like a reasonable
     // balance. Other major platforms have much longer CSRF token lifetimes,
     // like Rails (session duration) and Django (forever), which suggests this
     // is a reasonable analysis.
     $csrf_window = 6;
     for ($ii = -$csrf_window; $ii <= 1; $ii++) {
         $valid = $this->getRawCSRFToken($ii);
         $digest = PhabricatorHash::digest($valid, $salt);
         $digest = substr($digest, 0, self::CSRF_TOKEN_LENGTH);
         if (phutil_hashes_are_identical($digest, $token)) {
             return true;
         }
     }
     return false;
 }
 public static function verifyTOTPCode(PhabricatorUser $user, PhutilOpaqueEnvelope $key, $code)
 {
     // TODO: This should use rate limiting to prevent multiple attempts in a
     // short period of time.
     $now = (int) (time() / 30);
     // Allow the user to enter a code a few minutes away on either side, in
     // case the server or client has some clock skew.
     for ($offset = -2; $offset <= 2; $offset++) {
         $real = self::getTOTPCode($key, $now + $offset);
         if (phutil_hashes_are_identical($real, $code)) {
             return true;
         }
     }
     // TODO: After validating a code, this should mark it as used and prevent
     // it from being reused.
     return false;
 }