[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/auth/factor/ -> PhabricatorAuthFactorTOTP.php (source)

   1  <?php
   2  
   3  final class PhabricatorAuthFactorTOTP extends PhabricatorAuthFactor {
   4  
   5    const TEMPORARY_TOKEN_TYPE = 'mfa:totp:key';
   6  
   7    public function getFactorKey() {
   8      return 'totp';
   9    }
  10  
  11    public function getFactorName() {
  12      return pht('Mobile Phone App (TOTP)');
  13    }
  14  
  15    public function getFactorDescription() {
  16      return pht(
  17        'Attach a mobile authenticator application (like Authy '.
  18        'or Google Authenticator) to your account. When you need to '.
  19        'authenticate, you will enter a code shown on your phone.');
  20    }
  21  
  22    public function processAddFactorForm(
  23      AphrontFormView $form,
  24      AphrontRequest $request,
  25      PhabricatorUser $user) {
  26  
  27      $key = $request->getStr('totpkey');
  28      if (strlen($key)) {
  29        // If the user is providing a key, make sure it's a key we generated.
  30        // This raises the barrier to theoretical attacks where an attacker might
  31        // provide a known key (such attacks are already prevented by CSRF, but
  32        // this is a second barrier to overcome).
  33  
  34        // (We store and verify the hash of the key, not the key itself, to limit
  35        // how useful the data in the table is to an attacker.)
  36  
  37        $temporary_token = id(new PhabricatorAuthTemporaryTokenQuery())
  38          ->setViewer($user)
  39          ->withObjectPHIDs(array($user->getPHID()))
  40          ->withTokenTypes(array(self::TEMPORARY_TOKEN_TYPE))
  41          ->withExpired(false)
  42          ->withTokenCodes(array(PhabricatorHash::digest($key)))
  43          ->executeOne();
  44        if (!$temporary_token) {
  45          // If we don't have a matching token, regenerate the key below.
  46          $key = null;
  47        }
  48      }
  49  
  50      if (!strlen($key)) {
  51        $key = self::generateNewTOTPKey();
  52  
  53        // Mark this key as one we generated, so the user is allowed to submit
  54        // a response for it.
  55  
  56        $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
  57          id(new PhabricatorAuthTemporaryToken())
  58            ->setObjectPHID($user->getPHID())
  59            ->setTokenType(self::TEMPORARY_TOKEN_TYPE)
  60            ->setTokenExpires(time() + phutil_units('1 hour in seconds'))
  61            ->setTokenCode(PhabricatorHash::digest($key))
  62            ->save();
  63        unset($unguarded);
  64      }
  65  
  66      $code = $request->getStr('totpcode');
  67  
  68      $e_code = true;
  69      if ($request->getExists('totp')) {
  70        $okay = self::verifyTOTPCode(
  71          $user,
  72          new PhutilOpaqueEnvelope($key),
  73          $code);
  74  
  75        if ($okay) {
  76          $config = $this->newConfigForUser($user)
  77            ->setFactorName(pht('Mobile App (TOTP)'))
  78            ->setFactorSecret($key);
  79  
  80          return $config;
  81        } else {
  82          if (!strlen($code)) {
  83            $e_code = pht('Required');
  84          } else {
  85            $e_code = pht('Invalid');
  86          }
  87        }
  88      }
  89  
  90      $form->addHiddenInput('totp', true);
  91      $form->addHiddenInput('totpkey', $key);
  92  
  93      $form->appendRemarkupInstructions(
  94        pht(
  95          'First, download an authenticator application on your phone. Two '.
  96          'applications which work well are **Authy** and **Google '.
  97          'Authenticator**, but any other TOTP application should also work.'));
  98  
  99      $form->appendInstructions(
 100        pht(
 101          'Launch the application on your phone, and add a new entry for '.
 102          'this Phabricator install. When prompted, scan the QR code or '.
 103          'manually enter the key shown below into the application.'));
 104  
 105      $prod_uri = new PhutilURI(PhabricatorEnv::getProductionURI('/'));
 106      $issuer = $prod_uri->getDomain();
 107  
 108      $uri = urisprintf(
 109        'otpauth://totp/%s:%s?secret=%s&issuer=%s',
 110        $issuer,
 111        $user->getUsername(),
 112        $key,
 113        $issuer);
 114  
 115      $qrcode = $this->renderQRCode($uri);
 116      $form->appendChild($qrcode);
 117  
 118      $form->appendChild(
 119        id(new AphrontFormStaticControl())
 120          ->setLabel(pht('Key'))
 121          ->setValue(phutil_tag('strong', array(), $key)));
 122  
 123      $form->appendInstructions(
 124        pht(
 125          '(If given an option, select that this key is "Time Based", not '.
 126          '"Counter Based".)'));
 127  
 128      $form->appendInstructions(
 129        pht(
 130          'After entering the key, the application should display a numeric '.
 131          'code. Enter that code below to confirm that you have configured '.
 132          'the authenticator correctly:'));
 133  
 134      $form->appendChild(
 135        id(new AphrontFormTextControl())
 136          ->setLabel(pht('TOTP Code'))
 137          ->setName('totpcode')
 138          ->setValue($code)
 139          ->setError($e_code));
 140  
 141    }
 142  
 143    public function renderValidateFactorForm(
 144      PhabricatorAuthFactorConfig $config,
 145      AphrontFormView $form,
 146      PhabricatorUser $viewer,
 147      $validation_result) {
 148  
 149      if (!$validation_result) {
 150        $validation_result = array();
 151      }
 152  
 153      $form->appendChild(
 154        id(new AphrontFormTextControl())
 155          ->setName($this->getParameterName($config, 'totpcode'))
 156          ->setLabel(pht('App Code'))
 157          ->setCaption(pht('Factor Name: %s', $config->getFactorName()))
 158          ->setValue(idx($validation_result, 'value'))
 159          ->setError(idx($validation_result, 'error', true)));
 160    }
 161  
 162    public function processValidateFactorForm(
 163      PhabricatorAuthFactorConfig $config,
 164      PhabricatorUser $viewer,
 165      AphrontRequest $request) {
 166  
 167      $code = $request->getStr($this->getParameterName($config, 'totpcode'));
 168      $key = new PhutilOpaqueEnvelope($config->getFactorSecret());
 169  
 170      if (self::verifyTOTPCode($viewer, $key, $code)) {
 171        return array(
 172          'error' => null,
 173          'value' => $code,
 174          'valid' => true,
 175        );
 176      } else {
 177        return array(
 178          'error' => strlen($code) ? pht('Invalid') : pht('Required'),
 179          'value' => $code,
 180          'valid' => false,
 181        );
 182      }
 183    }
 184  
 185  
 186    public static function generateNewTOTPKey() {
 187      return strtoupper(Filesystem::readRandomCharacters(16));
 188    }
 189  
 190    public static function verifyTOTPCode(
 191      PhabricatorUser $user,
 192      PhutilOpaqueEnvelope $key,
 193      $code) {
 194  
 195      // TODO: This should use rate limiting to prevent multiple attempts in a
 196      // short period of time.
 197  
 198      $now = (int)(time() / 30);
 199  
 200      // Allow the user to enter a code a few minutes away on either side, in
 201      // case the server or client has some clock skew.
 202      for ($offset = -2; $offset <= 2; $offset++) {
 203        $real = self::getTOTPCode($key, $now + $offset);
 204        if ($real === $code) {
 205          return true;
 206        }
 207      }
 208  
 209      // TODO: After validating a code, this should mark it as used and prevent
 210      // it from being reused.
 211  
 212      return false;
 213    }
 214  
 215  
 216    public static function base32Decode($buf) {
 217      $buf = strtoupper($buf);
 218  
 219      $map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
 220      $map = str_split($map);
 221      $map = array_flip($map);
 222  
 223      $out = '';
 224      $len = strlen($buf);
 225      $acc = 0;
 226      $bits = 0;
 227      for ($ii = 0; $ii < $len; $ii++) {
 228        $chr = $buf[$ii];
 229        $val = $map[$chr];
 230  
 231        $acc = $acc << 5;
 232        $acc = $acc + $val;
 233  
 234        $bits += 5;
 235        if ($bits >= 8) {
 236          $bits = $bits - 8;
 237          $out .= chr(($acc & (0xFF << $bits)) >> $bits);
 238        }
 239      }
 240  
 241      return $out;
 242    }
 243  
 244    public static function getTOTPCode(PhutilOpaqueEnvelope $key, $timestamp) {
 245      $binary_timestamp = pack('N*', 0).pack('N*', $timestamp);
 246      $binary_key = self::base32Decode($key->openEnvelope());
 247  
 248      $hash = hash_hmac('sha1', $binary_timestamp, $binary_key, true);
 249  
 250      // See RFC 4226.
 251  
 252      $offset = ord($hash[19]) & 0x0F;
 253  
 254      $code = ((ord($hash[$offset + 0]) & 0x7F) << 24) |
 255              ((ord($hash[$offset + 1]) & 0xFF) << 16) |
 256              ((ord($hash[$offset + 2]) & 0xFF) <<  8) |
 257              ((ord($hash[$offset + 3])       )      );
 258  
 259      $code = ($code % 1000000);
 260      $code = str_pad($code, 6, '0', STR_PAD_LEFT);
 261  
 262      return $code;
 263    }
 264  
 265  
 266    /**
 267     * @phutil-external-symbol class QRcode
 268     */
 269    private function renderQRCode($uri) {
 270      $root = dirname(phutil_get_library_root('phabricator'));
 271      require_once $root.'/externals/phpqrcode/phpqrcode.php';
 272  
 273      $lines = QRcode::text($uri);
 274  
 275      $total_width = 240;
 276      $cell_size = floor($total_width / count($lines));
 277  
 278      $rows = array();
 279      foreach ($lines as $line) {
 280        $cells = array();
 281        for ($ii = 0; $ii < strlen($line); $ii++) {
 282          if ($line[$ii] == '1') {
 283            $color = '#000';
 284          } else {
 285            $color = '#fff';
 286          }
 287  
 288          $cells[] = phutil_tag(
 289            'td',
 290            array(
 291              'width' => $cell_size,
 292              'height' => $cell_size,
 293              'style' => 'background: '.$color,
 294            ),
 295            '');
 296        }
 297        $rows[] = phutil_tag('tr', array(), $cells);
 298      }
 299  
 300      return phutil_tag(
 301        'table',
 302        array(
 303          'style' => 'margin: 24px auto;',
 304        ),
 305        $rows);
 306    }
 307  
 308  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1