mediawiki-extensions-OATHAuth/OATHAuthKey.php
Tyler Anthony Romeo 0c389f5025 Refactored special pages into HTMLForm and proxy
Made new class ProxySpecialPage, which acts as a
proxy object to another SpecialPage object that is
determined based on context information other than
the title.

Then Special:OATH has been split into two separate
special page classes (both FormSpecialPages using
HTMLForm) that are routed to by a ProxySpecialPage
object.

In addition, the form for enabling two-factor auth
has been refactored into vform style, with some
better instructions on how to enable two-factor
authentication.

Change-Id: Ib9117cbc9d7f044de9607db81a157e1b472b5ec0
2016-03-23 11:26:04 -07:00

147 lines
3.5 KiB
PHP

<?php
/**
* Class representing a two-factor key
*
* Keys can be tied to OAUTHUsers
*
* @ingroup Extensions
*/
class OATHAuthKey {
/**
* Represents that a token corresponds to the main secret
* @see verifyToken
*/
const MAIN_TOKEN = 1;
/**
* Represents that a token corresponds to a scratch token
* @see verifyToken
*/
const SCRATCH_TOKEN = -1;
/** @var string Two factor binary secret */
private $secret;
/** @var string[] List of scratch tokens */
private $scratchTokens;
/**
* Make a new key from random values
*
* @return OATHAuthKey
*/
public static function newFromRandom() {
$object = new self(
Base32::encode( MWCryptRand::generate( 10, true ) ),
array()
);
$object->regenerateScratchTokens();
return $object;
}
/**
* @param string $secret
* @param array $scratchTokens
*/
public function __construct( $secret, array $scratchTokens ) {
// Currently harcoded values; might be used in future
$this->secret = array(
'mode' => 'hotp',
'secret' => $secret,
'period' => 30,
'algorithm' => 'SHA1',
);
$this->scratchTokens = $scratchTokens;
}
/**
* @return String
*/
public function getSecret() {
return $this->secret['secret'];
}
/**
* @return Array
*/
public function getScratchTokens() {
return $this->scratchTokens;
}
/**
* Verify a token against the secret or scratch tokens
*
* @param string $token Token to verify
* @param OATHUser $user
*
* @return int|false Returns a constant represent what type of token was matched,
* or false for no match
*/
public function verifyToken( $token, $user ) {
global $wgOATHAuthWindowRadius;
if ($this->secret['mode'] !== 'hotp') {
throw new \DomainException( 'OATHAuth extension does not support non-HOTP tokens' );
}
// Prevent replay attacks
$memc = ObjectCache::newAnything( array() );
$memcKey = wfMemcKey( 'oauthauth', 'usedtokens', $user->getUser()->getId() );
$lastWindow = (int)$memc->get( $memcKey );
$retval = false;
$results = HOTP::generateByTimeWindow(
Base32::decode( $this->secret['secret'] ),
$this->secret['period'], -$wgOATHAuthWindowRadius, $wgOATHAuthWindowRadius );
// Check to see if the user's given token is in the list of tokens generated
// for the time window.
foreach ( $results as $window => $result ) {
if ( $window > $lastWindow && $result->toHOTP( 6 ) === $token ) {
$lastWindow = $window;
$retval = self::MAIN_TOKEN;
break;
}
}
// See if the user is using a scratch token
if ( !$retval ) {
$length = count( $this->scratchTokens );
// Detect condition where all scratch tokens have been used
if ( $length == 1 && "" === $this->scratchTokens[0] ) {
$retval = false;
} else {
for ( $i = 0; $i < $length; $i++ ) {
if ( $token === $this->scratchTokens[$i] ) {
// If there is a scratch token, remove it from the scratch token list
unset( $this->scratchTokens[$i] );
$oathrepo = new OATHUserRepository( wfGetLB() );
$user->setKey( $this );
$oathrepo->persist( $user );
// Only return true if we removed it from the database
$retval = self::SCRATCH_TOKEN;
break;
}
}
}
}
if ( $retval ) {
$memc->set( $memcKey, $lastWindow,
$this->secret['period'] * (1 + 2 * $wgOATHAuthWindowRadius) );
}
return $retval;
}
public function regenerateScratchTokens() {
$scratchTokens = array();
for ( $i = 0; $i < 5; $i++ ) {
array_push( $scratchTokens, Base32::encode( MWCryptRand::generate( 10, true ) ) );
}
$this->scratchTokens = $scratchTokens;
}
}