mediawiki-extensions-OATHAuth/OATHAuthKey.php
Tyler Anthony Romeo 89455cdfb2 Refactor extension key storage
This takes out the actual key information from
OATHUser and puts it into an OATHKey class, which OATHUser
depends on. This allows easily swapping keys in/out from
a user.

Change-Id: Ife5f1bae4ad65b66c5e20017cc43c0576b4aba19
2016-03-22 18:08:45 -07:00

132 lines
3.2 KiB
PHP

<?php
/**
* Class representing a two-factor key
*
* Keys can be tied to OAUTHUsers
*/
class OATHAuthKey {
/** @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 bool True on match, false otherwise
*/
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 = true;
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 = true;
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;
}
}