[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |