summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/OATHAuth/includes
diff options
context:
space:
mode:
authorYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
committerYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
commitfc7369835258467bf97eb64f184b93691f9a9fd5 (patch)
treedaabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/extensions/OATHAuth/includes
first commit
Diffstat (limited to 'www/wiki/extensions/OATHAuth/includes')
-rw-r--r--www/wiki/extensions/OATHAuth/includes/OATHAuthHooks.php221
-rw-r--r--www/wiki/extensions/OATHAuth/includes/OATHAuthKey.php187
-rw-r--r--www/wiki/extensions/OATHAuth/includes/OATHAuthUtils.php141
-rw-r--r--www/wiki/extensions/OATHAuth/includes/OATHUser.php83
-rw-r--r--www/wiki/extensions/OATHAuth/includes/OATHUserRepository.php103
-rw-r--r--www/wiki/extensions/OATHAuth/includes/api/ApiOATHValidate.php101
-rw-r--r--www/wiki/extensions/OATHAuth/includes/api/ApiQueryOATH.php90
-rw-r--r--www/wiki/extensions/OATHAuth/includes/auth/TOTPAuthenticationRequest.php43
-rw-r--r--www/wiki/extensions/OATHAuth/includes/auth/TOTPSecondaryAuthenticationProvider.php121
-rw-r--r--www/wiki/extensions/OATHAuth/includes/lib/base32.php105
-rw-r--r--www/wiki/extensions/OATHAuth/includes/lib/hotp.php179
-rw-r--r--www/wiki/extensions/OATHAuth/includes/special/ProxySpecialPage.php227
-rw-r--r--www/wiki/extensions/OATHAuth/includes/special/SpecialOATH.php44
-rw-r--r--www/wiki/extensions/OATHAuth/includes/special/SpecialOATHDisable.php136
-rw-r--r--www/wiki/extensions/OATHAuth/includes/special/SpecialOATHEnable.php241
15 files changed, 2022 insertions, 0 deletions
diff --git a/www/wiki/extensions/OATHAuth/includes/OATHAuthHooks.php b/www/wiki/extensions/OATHAuth/includes/OATHAuthHooks.php
new file mode 100644
index 00000000..8842b16a
--- /dev/null
+++ b/www/wiki/extensions/OATHAuth/includes/OATHAuthHooks.php
@@ -0,0 +1,221 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+use MediaWiki\Auth\AuthenticationRequest;
+use MediaWiki\MediaWikiServices;
+use Wikimedia\Rdbms\IDatabase;
+
+/**
+ * Hooks for Extension:OATHAuth
+ *
+ * @ingroup Extensions
+ */
+class OATHAuthHooks {
+ /**
+ * Get the singleton OATH user repository
+ *
+ * @return OATHUserRepository
+ */
+ public static function getOATHUserRepository() {
+ global $wgOATHAuthDatabase;
+
+ static $service = null;
+
+ if ( $service == null ) {
+ $factory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+ $service = new OATHUserRepository(
+ $factory->getMainLB( $wgOATHAuthDatabase ),
+ new HashBagOStuff(
+ [
+ 'maxKeys' => 5,
+ ]
+ )
+ );
+ }
+
+ return $service;
+ }
+
+ /**
+ * @param AuthenticationRequest[] $requests
+ * @param array $fieldInfo Field information array (union of the
+ * AuthenticationRequest::getFieldInfo() responses).
+ * @param array &$formDescriptor HTMLForm descriptor. The special key 'weight' can be set
+ * to change the order of the fields.
+ * @param string $action One of the AuthManager::ACTION_* constants.
+ * @return bool
+ */
+ public static function onAuthChangeFormFields(
+ array $requests, array $fieldInfo, array &$formDescriptor, $action
+ ) {
+ if ( isset( $fieldInfo['OATHToken'] ) ) {
+ $formDescriptor['OATHToken'] += [
+ 'cssClass' => 'loginText',
+ 'id' => 'wpOATHToken',
+ 'size' => 20,
+ 'autofocus' => true,
+ 'persistent' => false,
+ 'autocomplete' => false,
+ 'spellcheck' => false,
+ ];
+ }
+ return true;
+ }
+
+ /**
+ * Determine if two-factor authentication is enabled for $wgUser
+ *
+ * This isn't the preferred mechanism for controlling access to sensitive features
+ * (see AuthManager::securitySensitiveOperationStatus() for that) but there is no harm in
+ * keeping it.
+ *
+ * @param bool &$isEnabled Will be set to true if enabled, false otherwise
+ * @return bool False if enabled, true otherwise
+ */
+ public static function onTwoFactorIsEnabled( &$isEnabled ) {
+ global $wgUser;
+
+ $user = self::getOATHUserRepository()->findByUser( $wgUser );
+ if ( $user && $user->getKey() !== null ) {
+ $isEnabled = true;
+ # This two-factor extension is enabled by the user,
+ # we don't need to check others.
+ return false;
+ } else {
+ $isEnabled = false;
+ # This two-factor extension isn't enabled by the user,
+ # but others may be.
+ return true;
+ }
+ }
+
+ /**
+ * Add the necessary user preferences for OATHAuth
+ *
+ * @param User $user
+ * @param array &$preferences
+ * @return bool
+ */
+ public static function onGetPreferences( User $user, array &$preferences ) {
+ $oathUser = self::getOATHUserRepository()->findByUser( $user );
+
+ // If there is no existing key, and the user is not allowed to enable it,
+ // we have nothing to show. (
+ if ( $oathUser->getKey() === null && !$user->isAllowed( 'oathauth-enable' ) ) {
+ return true;
+ }
+
+ $title = SpecialPage::getTitleFor( 'OATH' );
+ $msg = $oathUser->getKey() !== null ? 'oathauth-disable' : 'oathauth-enable';
+
+ $preferences[$msg] = [
+ 'type' => 'info',
+ 'raw' => 'true',
+ 'default' => Linker::link(
+ $title,
+ wfMessage( $msg )->escaped(),
+ [],
+ [ 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ]
+ ),
+ 'label-message' => 'oathauth-prefs-label',
+ 'section' => 'personal/info', ];
+
+ return true;
+ }
+
+ /**
+ * @param DatabaseUpdater $updater
+ * @return bool
+ */
+ public static function onLoadExtensionSchemaUpdates( $updater ) {
+ $base = dirname( __DIR__ );
+ switch ( $updater->getDB()->getType() ) {
+ case 'mysql':
+ case 'sqlite':
+ $updater->addExtensionTable( 'oathauth_users', "$base/sql/mysql/tables.sql" );
+ $updater->addExtensionUpdate( [ [ __CLASS__, 'schemaUpdateOldUsersFromInstaller' ] ] );
+ $updater->dropExtensionField(
+ 'oathauth_users',
+ 'secret_reset',
+ "$base/sql/mysql/patch-remove_reset.sql"
+ );
+ break;
+
+ case 'oracle':
+ $updater->addExtensionTable( 'oathauth_users', "$base/sql/oracle/tables.sql" );
+ break;
+
+ case 'postgres':
+ $updater->addExtensionTable( 'oathauth_users', "$base/sql/postgres/tables.sql" );
+ break;
+ }
+
+ return true;
+ }
+
+ /**
+ * Helper function for converting old users to the new schema
+ * @see OATHAuthHooks::OATHAuthSchemaUpdates
+ *
+ * @param DatabaseUpdater $updater
+ *
+ * @return bool
+ */
+ public static function schemaUpdateOldUsersFromInstaller( DatabaseUpdater $updater ) {
+ return self::schemaUpdateOldUsers( $updater->getDB() );
+ }
+
+ /**
+ * Helper function for converting old users to the new schema
+ * @see OATHAuthHooks::OATHAuthSchemaUpdates
+ *
+ * @param IDatabase $db
+ * @return bool
+ */
+ public static function schemaUpdateOldUsers( IDatabase $db ) {
+ if ( !$db->fieldExists( 'oathauth_users', 'secret_reset' ) ) {
+ return true;
+ }
+
+ $res = $db->select(
+ 'oathauth_users',
+ [ 'id', 'scratch_tokens' ],
+ [ 'is_validated != 0' ],
+ __METHOD__
+ );
+
+ foreach ( $res as $row ) {
+ Wikimedia\suppressWarnings();
+ $scratchTokens = unserialize( base64_decode( $row->scratch_tokens ) );
+ Wikimedia\restoreWarnings();
+ if ( $scratchTokens ) {
+ $db->update(
+ 'oathauth_users',
+ [ 'scratch_tokens' => implode( ',', $scratchTokens ) ],
+ [ 'id' => $row->id ],
+ __METHOD__
+ );
+ }
+ }
+
+ // Remove rows from the table where user never completed the setup process
+ $db->delete( 'oathauth_users', [ 'is_validated' => 0 ], __METHOD__ );
+
+ return true;
+ }
+}
diff --git a/www/wiki/extensions/OATHAuth/includes/OATHAuthKey.php b/www/wiki/extensions/OATHAuth/includes/OATHAuthKey.php
new file mode 100644
index 00000000..2e178803
--- /dev/null
+++ b/www/wiki/extensions/OATHAuth/includes/OATHAuthKey.php
@@ -0,0 +1,187 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+/**
+ * Class representing a two-factor key
+ *
+ * Keys can be tied to OATHUsers
+ *
+ * @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 array 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 ) ),
+ []
+ );
+
+ $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 = [
+ '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, OATHUser $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( [] );
+ $uid = CentralIdLookup::factory()->centralIdFromLocalUser( $user->getUser() );
+ $memcKey = wfMemcKey( 'oathauth', 'usedtokens', $uid );
+ $lastWindow = (int)$memc->get( $memcKey );
+
+ $retval = false;
+ $results = HOTP::generateByTimeWindow(
+ Base32::decode( $this->secret['secret'] ),
+ $this->secret['period'], -$wgOATHAuthWindowRadius, $wgOATHAuthWindowRadius
+ );
+
+ // Remove any whitespace from the received token, which can be an intended group seperator
+ // or trimmeable whitespace
+ $token = preg_replace( '/\s+/', '', $token );
+
+ // 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 = OATHAuthHooks::getOATHUserRepository();
+ $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 )
+ );
+ } else {
+ // Increase rate limit counter for failed request
+ $user->getUser()->pingLimiter( 'badoath' );
+ }
+
+ return $retval;
+ }
+
+ public function regenerateScratchTokens() {
+ $scratchTokens = [];
+ for ( $i = 0; $i < 5; $i++ ) {
+ array_push( $scratchTokens, Base32::encode( MWCryptRand::generate( 10, true ) ) );
+ }
+ $this->scratchTokens = $scratchTokens;
+ }
+
+ /**
+ * Check if a token is one of the scratch tokens for this two factor key.
+ *
+ * @param string $token Token to verify
+ *
+ * @return bool true if this is a scratch token.
+ */
+ public function isScratchToken( $token ) {
+ $token = preg_replace( '/\s+/', '', $token );
+ return in_array( $token, $this->scratchTokens, true );
+ }
+}
diff --git a/www/wiki/extensions/OATHAuth/includes/OATHAuthUtils.php b/www/wiki/extensions/OATHAuth/includes/OATHAuthUtils.php
new file mode 100644
index 00000000..2afd3bf8
--- /dev/null
+++ b/www/wiki/extensions/OATHAuth/includes/OATHAuthUtils.php
@@ -0,0 +1,141 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+/**
+ * Utility class for various OATH functions
+ *
+ * @ingroup Extensions
+ */
+class OATHAuthUtils {
+ /**
+ * Check whether OATH two-factor authentication is enabled for a given user.
+ * This is a stable method that does not change and can be used in other extensions.
+ * @param User $user
+ * @return bool
+ */
+ public static function isEnabledFor( User $user ) {
+ $oathUser = OATHAuthHooks::getOATHUserRepository()->findByUser( $user );
+ return $oathUser && $oathUser->getKey();
+ }
+
+ /**
+ * Encrypt an aray of variables to put into the user's session. We use this
+ * when storing the user's password in their session. We can use json as the
+ * serialization format because $plaintextVars is an array of strings.
+ * @param array $plaintextVars array of user input strings
+ * @param int $userId passed to key derivation functions so each user uses
+ * distinct encryption and hmac keys
+ * @return string encrypted data packet
+ */
+ public static function encryptSessionData( array $plaintextVars, $userId ) {
+ $keyMaterial = self::getKeyMaterials();
+ $keys = self::getUserKeys( $keyMaterial, $userId );
+ return self::seal( json_encode( $plaintextVars ), $keys['encrypt'], $keys['hmac'] );
+ }
+
+ /**
+ * Decrypt an encrypted packet, generated with encryptSessionData
+ * @param string $ciphertext Encrypted data packet
+ * @param string|int $userId
+ * @return array of strings
+ */
+ public static function decryptSessionData( $ciphertext, $userId ) {
+ $keyMaterial = self::getKeyMaterials();
+ $keys = self::getUserKeys( $keyMaterial, $userId );
+ return json_decode( self::unseal( $ciphertext, $keys['encrypt'], $keys['hmac'] ), true );
+ }
+
+ /**
+ * Get the base secret for this wiki, used to derive all of the encryption
+ * keys. When $wgOATHAuthSecret is rotated, users who are part way through the
+ * two-step login will get an exception, and have to re-start the login.
+ * @return string
+ */
+ private static function getKeyMaterials() {
+ global $wgOATHAuthSecret, $wgSecretKey;
+ return $wgOATHAuthSecret ?: $wgSecretKey;
+ }
+
+ /**
+ * Generate encryption and hmac keys, unique to this user, based on a single
+ * wiki secret. Use a moderate pbkdf2 work factor in case we ever leak keys.
+ * @param string $secret
+ * @param string|int $userid
+ * @return array including key for encryption and integrity checking
+ */
+ private static function getUserKeys( $secret, $userid ) {
+ $keymats = hash_pbkdf2( 'sha256', $secret, "oath-$userid", 10001, 64, true );
+ return [
+ 'encrypt' => substr( $keymats, 0, 32 ),
+ 'hmac' => substr( $keymats, 32, 32 ),
+ ];
+ }
+
+ /**
+ * Actually encrypt the data, using a new random IV, and prepend the hmac
+ * of the encrypted data + IV, using a separate hmac key.
+ * @param string $data
+ * @param string $encKey
+ * @param string $hmacKey
+ * @return string $hmac.$iv.$ciphertext, each component b64 encoded
+ */
+ private static function seal( $data, $encKey, $hmacKey ) {
+ $iv = MWCryptRand::generate( 16, true );
+ $ciphertext = openssl_encrypt(
+ $data,
+ 'aes-256-ctr',
+ $encKey,
+ OPENSSL_RAW_DATA,
+ $iv
+ );
+ $sealed = base64_encode( $iv ) . '.' . base64_encode( $ciphertext );
+ $hmac = hash_hmac( 'sha256', $sealed, $hmacKey, true );
+ return base64_encode( $hmac ) . '.' . $sealed;
+ }
+
+ /**
+ * Decrypt data sealed using seal(). First checks the hmac to prevent various
+ * attacks.
+ * @param string $encrypted
+ * @param string $encKey
+ * @param string $hmacKey
+ * @return string plaintext
+ * @throws Exception
+ */
+ private static function unseal( $encrypted, $encKey, $hmacKey ) {
+ $pieces = explode( '.', $encrypted );
+ if ( count( $pieces ) !== 3 ) {
+ throw new InvalidArgumentException( 'Invalid sealed-secret format' );
+ }
+
+ list( $hmac, $iv, $ciphertext ) = $pieces;
+ $integCalc = hash_hmac( 'sha256', $iv . '.' . $ciphertext, $hmacKey, true );
+ if ( !hash_equals( $integCalc, base64_decode( $hmac ) ) ) {
+ throw new Exception( 'Sealed secret has been tampered with, aborting.' );
+ }
+
+ return openssl_decrypt(
+ base64_decode( $ciphertext ),
+ 'aes-256-ctr',
+ $encKey,
+ OPENSSL_RAW_DATA,
+ base64_decode( $iv )
+ );
+ }
+
+}
diff --git a/www/wiki/extensions/OATHAuth/includes/OATHUser.php b/www/wiki/extensions/OATHAuth/includes/OATHUser.php
new file mode 100644
index 00000000..ed1b4a16
--- /dev/null
+++ b/www/wiki/extensions/OATHAuth/includes/OATHUser.php
@@ -0,0 +1,83 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+/**
+ * Class representing a user from OATH's perspective
+ *
+ * @ingroup Extensions
+ */
+class OATHUser {
+ /** @var User */
+ private $user;
+
+ /** @var OATHAuthKey|null */
+ private $key;
+
+ /**
+ * Constructor. Can't be called directly. Use OATHUserRepository::findByUser instead.
+ * @param User $user
+ * @param OATHAuthKey|null $key
+ */
+ public function __construct( User $user, OATHAuthKey $key = null ) {
+ $this->user = $user;
+ $this->key = $key;
+ }
+
+ /**
+ * @return User
+ */
+ public function getUser() {
+ return $this->user;
+ }
+
+ /**
+ * @return String
+ */
+ public function getIssuer() {
+ global $wgSitename, $wgOATHAuthAccountPrefix;
+ if ( $wgOATHAuthAccountPrefix !== false ) {
+ return $wgOATHAuthAccountPrefix;
+ }
+ return $wgSitename;
+ }
+
+ /**
+ * @return String
+ */
+ public function getAccount() {
+ return $this->user->getName();
+ }
+
+ /**
+ * Get the key associated with this user.
+ *
+ * @return null|OATHAuthKey
+ */
+ public function getKey() {
+ return $this->key;
+ }
+
+ /**
+ * Set the key associated with this user.
+ *
+ * @param OATHAuthKey|null $key
+ */
+ public function setKey( OATHAuthKey $key = null ) {
+ $this->key = $key;
+ }
+}
diff --git a/www/wiki/extensions/OATHAuth/includes/OATHUserRepository.php b/www/wiki/extensions/OATHAuth/includes/OATHUserRepository.php
new file mode 100644
index 00000000..698ca49b
--- /dev/null
+++ b/www/wiki/extensions/OATHAuth/includes/OATHUserRepository.php
@@ -0,0 +1,103 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+use Wikimedia\Rdbms\LoadBalancer;
+use Wikimedia\Rdbms\DBConnRef;
+
+class OATHUserRepository {
+ /** @var LoadBalancer */
+ protected $lb;
+
+ /** @var BagOStuff */
+ protected $cache;
+
+ /**
+ * OATHUserRepository constructor.
+ * @param LoadBalancer $lb
+ * @param BagOStuff $cache
+ */
+ public function __construct( LoadBalancer $lb, BagOStuff $cache ) {
+ $this->lb = $lb;
+ $this->cache = $cache;
+ }
+
+ /**
+ * @param User $user
+ * @return OATHUser
+ */
+ public function findByUser( User $user ) {
+ $oathUser = $this->cache->get( $user->getName() );
+ if ( !$oathUser ) {
+ $oathUser = new OATHUser( $user, null );
+
+ $uid = CentralIdLookup::factory()->centralIdFromLocalUser( $user );
+ $res = $this->getDB( DB_REPLICA )->selectRow(
+ 'oathauth_users',
+ '*',
+ [ 'id' => $uid ],
+ __METHOD__
+ );
+ if ( $res ) {
+ $key = new OATHAuthKey( $res->secret, explode( ',', $res->scratch_tokens ) );
+ $oathUser->setKey( $key );
+ }
+
+ $this->cache->set( $user->getName(), $oathUser );
+ }
+ return $oathUser;
+ }
+
+ /**
+ * @param OATHUser $user
+ */
+ public function persist( OATHUser $user ) {
+ $this->getDB( DB_MASTER )->replace(
+ 'oathauth_users',
+ [ 'id' ],
+ [
+ 'id' => CentralIdLookup::factory()->centralIdFromLocalUser( $user->getUser() ),
+ 'secret' => $user->getKey()->getSecret(),
+ 'scratch_tokens' => implode( ',', $user->getKey()->getScratchTokens() ),
+ ],
+ __METHOD__
+ );
+ $this->cache->set( $user->getUser()->getName(), $user );
+ }
+
+ /**
+ * @param OATHUser $user
+ */
+ public function remove( OATHUser $user ) {
+ $this->getDB( DB_MASTER )->delete(
+ 'oathauth_users',
+ [ 'id' => CentralIdLookup::factory()->centralIdFromLocalUser( $user->getUser() ) ],
+ __METHOD__
+ );
+ $this->cache->delete( $user->getUser()->getName() );
+ }
+
+ /**
+ * @param integer $index DB_MASTER/DB_REPLICA
+ * @return DBConnRef
+ */
+ private function getDB( $index ) {
+ global $wgOATHAuthDatabase;
+
+ return $this->lb->getConnectionRef( $index, [], $wgOATHAuthDatabase );
+ }
+}
diff --git a/www/wiki/extensions/OATHAuth/includes/api/ApiOATHValidate.php b/www/wiki/extensions/OATHAuth/includes/api/ApiOATHValidate.php
new file mode 100644
index 00000000..14f46414
--- /dev/null
+++ b/www/wiki/extensions/OATHAuth/includes/api/ApiOATHValidate.php
@@ -0,0 +1,101 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+/**
+ * Validate an OATH token.
+ *
+ * @ingroup API
+ * @ingroup Extensions
+ */
+class ApiOATHValidate extends ApiBase {
+ public function execute() {
+ // Be extra paranoid about the data that is sent
+ $this->requirePostedParameters( [ 'totp', 'token' ] );
+
+ $params = $this->extractRequestParams();
+ if ( $params['user'] === null ) {
+ $params['user'] = $this->getUser()->getName();
+ }
+
+ $this->checkUserRightsAny( 'oathauth-api-all' );
+
+ $user = User::newFromName( $params['user'] );
+ if ( $user === false ) {
+ $this->dieWithError( 'noname' );
+ }
+
+ // Don't increase pingLimiter, just check for limit exceeded.
+ if ( $user->pingLimiter( 'badoath', 0 ) ) {
+ $this->dieWithError( 'apierror-ratelimited' );
+ }
+
+ $result = [
+ ApiResult::META_BC_BOOLS => [ 'enabled', 'valid' ],
+ 'enabled' => false,
+ 'valid' => false,
+ ];
+
+ if ( !$user->isAnon() ) {
+ $oathUser = OATHAuthHooks::getOATHUserRepository()
+ ->findByUser( $user );
+ if ( $oathUser ) {
+ $key = $oathUser->getKey();
+ if ( $key !== null ) {
+ $result['enabled'] = true;
+ $result['valid'] = $key->verifyToken(
+ $params['totp'], $oathUser ) !== false;
+ }
+ }
+ }
+
+ $this->getResult()->addValue( null, $this->getModuleName(), $result );
+ }
+
+ public function getCacheMode( $params ) {
+ return 'private';
+ }
+
+ public function isInternal() {
+ return true;
+ }
+
+ public function needsToken() {
+ return 'csrf';
+ }
+
+ public function getAllowedParams() {
+ return [
+ 'user' => [
+ ApiBase::PARAM_TYPE => 'user',
+ ],
+ 'totp' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ApiBase::PARAM_REQUIRED => true,
+ ],
+ ];
+ }
+
+ protected function getExamplesMessages() {
+ return [
+ 'action=oathvalidate&totp=123456&token=123ABC'
+ => 'apihelp-oathvalidate-example-1',
+ 'action=oathvalidate&user=Example&totp=123456&token=123ABC'
+ => 'apihelp-oathvalidate-example-2',
+ ];
+ }
+}
diff --git a/www/wiki/extensions/OATHAuth/includes/api/ApiQueryOATH.php b/www/wiki/extensions/OATHAuth/includes/api/ApiQueryOATH.php
new file mode 100644
index 00000000..e131f193
--- /dev/null
+++ b/www/wiki/extensions/OATHAuth/includes/api/ApiQueryOATH.php
@@ -0,0 +1,90 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+/**
+ * Query module to check if a user has OATH authentication enabled.
+ *
+ * Usage requires the 'oathauth-api-all' grant which is not given to any group
+ * by default. Use of this API is security sensitive and should not be granted
+ * lightly. Configuring a special 'oathauth' user group is recommended.
+ *
+ * @ingroup API
+ * @ingroup Extensions
+ */
+class ApiQueryOATH extends ApiQueryBase {
+ public function __construct( $query, $moduleName ) {
+ parent::__construct( $query, $moduleName, 'oath' );
+ }
+
+ public function execute() {
+ $params = $this->extractRequestParams();
+ if ( $params['user'] === null ) {
+ $params['user'] = $this->getUser()->getName();
+ }
+
+ $this->checkUserRightsAny( 'oathauth-api-all' );
+
+ $user = User::newFromName( $params['user'] );
+ if ( $user === false ) {
+ $this->dieWithError( 'noname' );
+ }
+
+ $result = $this->getResult();
+ $data = [
+ ApiResult::META_BC_BOOLS => [ 'enabled' ],
+ 'enabled' => false,
+ ];
+
+ if ( !$user->isAnon() ) {
+ $oathUser = OATHAuthHooks::getOATHUserRepository()
+ ->findByUser( $user );
+ $data['enabled'] = $oathUser && $oathUser->getKey() !== null;
+ }
+ $result->addValue( 'query', $this->getModuleName(), $data );
+ }
+
+ /**
+ * @param array $params
+ *
+ * @return string
+ */
+ public function getCacheMode( $params ) {
+ return 'private';
+ }
+
+ public function isInternal() {
+ return true;
+ }
+
+ public function getAllowedParams() {
+ return [
+ 'user' => [
+ ApiBase::PARAM_TYPE => 'user',
+ ],
+ ];
+ }
+
+ protected function getExamplesMessages() {
+ return [
+ 'action=query&meta=oath'
+ => 'apihelp-query+oath-example-1',
+ 'action=query&meta=oath&oathuser=Example'
+ => 'apihelp-query+oath-example-2',
+ ];
+ }
+}
diff --git a/www/wiki/extensions/OATHAuth/includes/auth/TOTPAuthenticationRequest.php b/www/wiki/extensions/OATHAuth/includes/auth/TOTPAuthenticationRequest.php
new file mode 100644
index 00000000..c10f5e2f
--- /dev/null
+++ b/www/wiki/extensions/OATHAuth/includes/auth/TOTPAuthenticationRequest.php
@@ -0,0 +1,43 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+use MediaWiki\Auth\AuthenticationRequest;
+
+/**
+ * AuthManager value object for the TOTP second factor of an authentication:
+ * a pseudorandom token that is generated from the current time independently
+ * by the server and the client.
+ */
+class TOTPAuthenticationRequest extends AuthenticationRequest {
+ public $OATHToken;
+
+ public function describeCredentials() {
+ return [
+ 'provider' => wfMessage( 'oathauth-describe-provider' ),
+ 'account' => new \RawMessage( '$1', [ $this->username ] ),
+ ] + parent::describeCredentials();
+ }
+
+ public function getFieldInfo() {
+ return [
+ 'OATHToken' => [
+ 'type' => 'string',
+ 'label' => wfMessage( 'oathauth-auth-token-label' ),
+ 'help' => wfMessage( 'oathauth-auth-token-help' ), ], ];
+ }
+}
diff --git a/www/wiki/extensions/OATHAuth/includes/auth/TOTPSecondaryAuthenticationProvider.php b/www/wiki/extensions/OATHAuth/includes/auth/TOTPSecondaryAuthenticationProvider.php
new file mode 100644
index 00000000..90d69cc3
--- /dev/null
+++ b/www/wiki/extensions/OATHAuth/includes/auth/TOTPSecondaryAuthenticationProvider.php
@@ -0,0 +1,121 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+use MediaWiki\Auth\AbstractSecondaryAuthenticationProvider;
+use MediaWiki\Auth\AuthenticationRequest;
+use MediaWiki\Auth\AuthenticationResponse;
+use MediaWiki\Auth\AuthManager;
+
+/**
+ * AuthManager secondary authentication provider for TOTP second-factor authentication.
+ *
+ * After a successful primary authentication, requests a time-based one-time password
+ * (typically generated by a mobile app such as Google Authenticator) from the user.
+ *
+ * @see AuthManager
+ * @see https://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm
+ */
+class TOTPSecondaryAuthenticationProvider extends AbstractSecondaryAuthenticationProvider {
+
+ /**
+ * @param string $action
+ * @param array $options
+ *
+ * @return array
+ */
+ public function getAuthenticationRequests( $action, array $options ) {
+ switch ( $action ) {
+ case AuthManager::ACTION_LOGIN:
+ // don't ask for anything initially so the second factor is on a separate screen
+ return [];
+ default:
+ return [];
+ }
+ }
+
+ /**
+ * If the user has enabled two-factor authentication, request a second factor.
+ *
+ * @param User $user
+ * @param array $reqs
+ *
+ * @return AuthenticationResponse
+ */
+ public function beginSecondaryAuthentication( $user, array $reqs ) {
+ $oathuser = OATHAuthHooks::getOATHUserRepository()->findByUser( $user );
+
+ if ( $oathuser->getKey() === null ) {
+ return AuthenticationResponse::newAbstain();
+ } else {
+ return AuthenticationResponse::newUI( [ new TOTPAuthenticationRequest() ],
+ wfMessage( 'oathauth-auth-ui' ), 'warning' );
+ }
+ }
+
+ /**
+ * Verify the second factor.
+ * @inheritDoc
+ */
+ public function continueSecondaryAuthentication( $user, array $reqs ) {
+ /** @var TOTPAuthenticationRequest $request */
+ $request = AuthenticationRequest::getRequestByClass( $reqs, TOTPAuthenticationRequest::class );
+ if ( !$request ) {
+ return AuthenticationResponse::newUI( [ new TOTPAuthenticationRequest() ],
+ wfMessage( 'oathauth-login-failed' ), 'error' );
+ }
+
+ $oathuser = OATHAuthHooks::getOATHUserRepository()->findByUser( $user );
+ /** @suppress PhanUndeclaredProperty */
+ $token = $request->OATHToken;
+
+ if ( $oathuser->getKey() === null ) {
+ $this->logger->warning( 'Two-factor authentication was disabled mid-authentication for '
+ . $user->getName() );
+ return AuthenticationResponse::newAbstain();
+ }
+
+ // Don't increase pingLimiter, just check for limit exceeded.
+ if ( $user->pingLimiter( 'badoath', 0 ) ) {
+ return AuthenticationResponse::newUI(
+ [ new TOTPAuthenticationRequest() ],
+ new Message(
+ 'oathauth-throttled',
+ // Arbitrary duration given here
+ [ Message::durationParam( 60 ) ]
+ ), 'error' );
+ }
+
+ if ( $oathuser->getKey()->verifyToken( $token, $oathuser ) ) {
+ return AuthenticationResponse::newPass();
+ } else {
+ return AuthenticationResponse::newUI( [ new TOTPAuthenticationRequest() ],
+ wfMessage( 'oathauth-login-failed' ), 'error' );
+ }
+ }
+
+ /**
+ * @param User $user
+ * @param User $creator
+ * @param array $reqs
+ *
+ * @return AuthenticationResponse
+ */
+ public function beginSecondaryAccountCreation( $user, $creator, array $reqs ) {
+ return AuthenticationResponse::newAbstain();
+ }
+}
diff --git a/www/wiki/extensions/OATHAuth/includes/lib/base32.php b/www/wiki/extensions/OATHAuth/includes/lib/base32.php
new file mode 100644
index 00000000..d4ca1dff
--- /dev/null
+++ b/www/wiki/extensions/OATHAuth/includes/lib/base32.php
@@ -0,0 +1,105 @@
+<?php
+/**
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * PHP Google two-factor authentication module.
+ *
+ * See http://www.idontplaydarts.com/2011/07/google-totp-two-factor-authentication-for-php/
+ * for more details
+ *
+ * @author Phil
+ **/
+
+class Base32 {
+
+ private static $lut = array(
+ "A" => 0, "B" => 1,
+ "C" => 2, "D" => 3,
+ "E" => 4, "F" => 5,
+ "G" => 6, "H" => 7,
+ "I" => 8, "J" => 9,
+ "K" => 10, "L" => 11,
+ "M" => 12, "N" => 13,
+ "O" => 14, "P" => 15,
+ "Q" => 16, "R" => 17,
+ "S" => 18, "T" => 19,
+ "U" => 20, "V" => 21,
+ "W" => 22, "X" => 23,
+ "Y" => 24, "Z" => 25,
+ "2" => 26, "3" => 27,
+ "4" => 28, "5" => 29,
+ "6" => 30, "7" => 31
+ );
+
+ /**
+ * Decodes a base32 string into a binary string according to RFC 4648.
+ **/
+ public static function decode($b32) {
+
+ $b32 = strtoupper($b32);
+
+ if (!preg_match('/^[ABCDEFGHIJKLMNOPQRSTUVWXYZ234567]+$/', $b32, $match))
+ throw new Exception('Invalid characters in the base32 string.');
+
+ $l = strlen($b32);
+ $n = 0;
+ $j = 0;
+ $binary = "";
+
+ for ($i = 0; $i < $l; $i++) {
+ // Move buffer left by 5 to make room
+ $n = $n << 5;
+ // Add value into buffer
+ $n = $n + self::$lut[$b32[$i]];
+ // Keep track of number of bits in buffer
+ $j = $j + 5;
+
+ if ($j >= 8) {
+ $j = $j - 8;
+ $binary .= chr(($n & (0xFF << $j)) >> $j);
+ }
+ }
+
+ return $binary;
+ }
+
+ /**
+ * Encodes a binary string into a base32 string according to RFC 4648 (no padding).
+ **/
+ public static function encode($string) {
+
+ if (empty($string))
+ throw new Exception('Empty string.');
+
+ $b32 = "";
+ $binary = "";
+
+ $bytes = str_split($string);
+ $length = count( $bytes );
+ for ($i = 0; $i < $length; $i++) {
+ $bits = base_convert(ord($bytes[$i]), 10, 2);
+ $binary .= str_pad($bits, 8, '0', STR_PAD_LEFT);
+ }
+
+ $map = array_keys(self::$lut);
+ $fivebits = str_split($binary, 5);
+ $length = count( $fivebits );
+ for ($i = 0; $i < $length; $i++) {
+ $dec = base_convert(str_pad($fivebits[$i], 5, '0'), 2, 10);
+ $b32 .= $map[$dec];
+ }
+
+ return $b32;
+ }
+}
diff --git a/www/wiki/extensions/OATHAuth/includes/lib/hotp.php b/www/wiki/extensions/OATHAuth/includes/lib/hotp.php
new file mode 100644
index 00000000..8fd3d94b
--- /dev/null
+++ b/www/wiki/extensions/OATHAuth/includes/lib/hotp.php
@@ -0,0 +1,179 @@
+<?php
+/**
+ * HOTP Class
+ * Based on the work of OAuth, and the sample implementation of HMAC OTP
+ * http://tools.ietf.org/html/draft-mraihi-oath-hmac-otp-04#appendix-D
+ * @author Jakob Heuser (firstname)@felocity.com
+ * @copyright 2011
+ * @license BSD-3-Clause
+ * @version 1.0
+ */
+class HOTP {
+ /**
+ * Generate a HOTP key based on a counter value (event based HOTP)
+ * @param string $key the key to use for hashing
+ * @param int $counter the number of attempts represented in this hashing
+ * @return HOTPResult a HOTP Result which can be truncated or output
+ */
+ public static function generateByCounter( $key, $counter ) {
+ // the counter value can be more than one byte long,
+ // so we need to pack it down properly.
+ $cur_counter = array( 0, 0, 0, 0, 0, 0, 0, 0 );
+ for ( $i = 7; $i >= 0; $i-- ) {
+ $cur_counter[$i] = pack( 'C*', $counter );
+ $counter = $counter >> 8;
+ }
+
+ $bin_counter = implode( $cur_counter );
+
+ // Pad to 8 chars
+ if ( strlen( $bin_counter ) < 8 ) {
+ $bin_counter = str_repeat( "\0", 8 - strlen( $bin_counter ) ) . $bin_counter;
+ }
+
+ // HMAC
+ $hash = hash_hmac( 'sha1', $bin_counter, $key );
+
+ return new HOTPResult( $hash );
+ }
+
+ /**
+ * Generate a HOTP key based on a timestamp and window size
+ *
+ * @param string $key the key to use for hashing
+ * @param int $window the size of the window a key is valid for in seconds
+ * @param int|bool $timestamp a timestamp to calculate for, defaults to time()
+ *
+ * @return HOTPResult a HOTP Result which can be truncated or output
+ */
+ public static function generateByTime( $key, $window, $timestamp = false ) {
+ if ( !$timestamp && $timestamp !== 0 ) {
+ $timestamp = HOTP::getTime();
+ }
+
+ $counter = (int)( $timestamp / $window );
+
+ return HOTP::generateByCounter( $key, $counter );
+ }
+
+ /**
+ * Generate a HOTP key collection based on a timestamp and window size
+ * all keys that could exist between a start and end time will be included
+ * in the returned array
+ *
+ * @param string $key the key to use for hashing
+ * @param int $window the size of the window a key is valid for in seconds
+ * @param int $min the minimum window to accept before $timestamp
+ * @param int $max the maximum window to accept after $timestamp
+ * @param int|bool $timestamp a timestamp to calculate for, defaults to time()
+ *
+ * @return HOTPResult[]
+ */
+ public static function generateByTimeWindow( $key, $window, $min = -1,
+ $max = 1, $timestamp = false
+ ) {
+ if ( !$timestamp && $timestamp !== 0 ) {
+ $timestamp = HOTP::getTime();
+ }
+
+ $counter = (int)( $timestamp / $window );
+ $window = range( $min, $max );
+
+ $out = array();
+ $length = count( $window );
+ for ( $i = 0; $i < $length; $i++ ) {
+ $shift_counter = $counter + $window[$i];
+ $out[$shift_counter] = HOTP::generateByCounter($key, $shift_counter);
+ }
+
+ return $out;
+ }
+
+ /**
+ * Gets the current time
+ * Ensures we are operating in UTC for the entire framework
+ * Restores the timezone on exit.
+ * @return int the current time
+ */
+ public static function getTime() {
+ return time(); // PHP's time is always UTC
+ }
+}
+
+/**
+ * The HOTPResult Class converts an HOTP item to various forms
+ * Supported formats include hex, decimal, string, and HOTP
+ * @author Jakob Heuser (firstname)@felocity.com
+ */
+class HOTPResult {
+ protected $hash;
+ protected $binary;
+ protected $decimal;
+ protected $hex;
+
+ /**
+ * Build an HOTP Result
+ * @param string $value the value to construct with
+ */
+ public function __construct( $value ) {
+ // store raw
+ $this->hash = $value;
+
+ // store calculate decimal
+ $hmac_result = array();
+
+ // Convert to decimal
+ foreach ( str_split( $this->hash, 2 ) as $hex ) {
+ $hmac_result[] = hexdec($hex);
+ }
+
+ $offset = $hmac_result[19] & 0xf;
+
+ $this->decimal = (
+ ( ( $hmac_result[$offset+0] & 0x7f ) << 24 ) |
+ ( ( $hmac_result[$offset+1] & 0xff ) << 16 ) |
+ ( ( $hmac_result[$offset+2] & 0xff ) << 8 ) |
+ ( $hmac_result[$offset+3] & 0xff )
+ );
+
+ // calculate hex
+ $this->hex = dechex( $this->decimal );
+ }
+
+ /**
+ * Returns the string version of the HOTP
+ * @return string
+ */
+ public function toString() {
+ return $this->hash;
+ }
+
+ /**
+ * Returns the hex version of the HOTP
+ * @return string
+ */
+ public function toHex() {
+ return $this->hex;
+ }
+
+ /**
+ * Returns the decimal version of the HOTP
+ * @return int
+ */
+ public function toDec() {
+ return $this->decimal;
+ }
+
+ /**
+ * Returns the truncated decimal form of the HOTP
+ * @param int $length the length of the HOTP to return
+ * @return string
+ */
+ public function toHOTP( $length ) {
+ $str = str_pad( $this->toDec(), $length, "0", STR_PAD_LEFT );
+ $str = substr( $str, ( -1 * $length ) );
+
+ return $str;
+ }
+
+}
diff --git a/www/wiki/extensions/OATHAuth/includes/special/ProxySpecialPage.php b/www/wiki/extensions/OATHAuth/includes/special/ProxySpecialPage.php
new file mode 100644
index 00000000..bdb808f0
--- /dev/null
+++ b/www/wiki/extensions/OATHAuth/includes/special/ProxySpecialPage.php
@@ -0,0 +1,227 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+/**
+ * A proxy class that routes a special page to other special pages based on
+ * request parameters
+ */
+abstract class ProxySpecialPage extends SpecialPage {
+ /**
+ * @var SpecialPage|null Target page to execute
+ */
+ private $target = null;
+
+ /**
+ * Instantiate a SpecialPage based on request parameters
+ *
+ * The page returned by this function will be cached and used as
+ * the target page for this proxy object.
+ *
+ * @return SpecialPage
+ */
+ abstract protected function getTargetPage();
+
+ /**
+ * Helper function that initializes the target SpecialPage object
+ */
+ private function init() {
+ if ( $this->target === null ) {
+ $this->target = $this->getTargetPage();
+ }
+ }
+
+ /**
+ * Magic function that proxies function calls to the target object
+ *
+ * @param string $method Method name being called
+ * @param array $args Array of arguments
+ *
+ * @return mixed
+ */
+ public function __call( $method, $args ) {
+ $this->init();
+ return call_user_func_array( [ $this->target, $method ], $args );
+ }
+
+ /**
+ * @return string
+ */
+ function getName() {
+ $this->init();
+ return $this->target->getName();
+ }
+
+ /**
+ * @param string|bool $subpage
+ * @return Title
+ */
+ function getPageTitle( $subpage = false ) {
+ $this->init();
+ return $this->target->getPageTitle( $subpage );
+ }
+
+ /**
+ * @return string
+ */
+ function getLocalName() {
+ $this->init();
+ return $this->target->getLocalName();
+ }
+
+ /**
+ * @return string
+ */
+ function getRestriction() {
+ $this->init();
+ return $this->target->getRestriction();
+ }
+
+ /**
+ * @return bool
+ */
+ function isListed() {
+ $this->init();
+ return $this->target->isListed();
+ }
+
+ /**
+ * @param bool $listed
+ * @return bool
+ */
+ function setListed( $listed ) {
+ $this->init();
+ return $this->target->setListed( $listed );
+ }
+
+ /**
+ * @param bool $x
+ * @return bool
+ */
+ function listed( $x = null ) {
+ $this->init();
+ return $this->target->listed( $x );
+ }
+
+ /**
+ * @return bool
+ */
+ public function isIncludable() {
+ $this->init();
+ return $this->target->isIncludable();
+ }
+
+ /**
+ * @param bool $x
+ * @return bool
+ */
+ function including( $x = null ) {
+ $this->init();
+ return $this->target->including( $x );
+ }
+
+ /**
+ * @return bool
+ */
+ public function isRestricted() {
+ $this->init();
+ return $this->target->isRestricted();
+ }
+
+ /**
+ * @param User $user
+ * @return bool
+ */
+ public function userCanExecute( User $user ) {
+ $this->init();
+ return $this->target->userCanExecute( $user );
+ }
+
+ /**
+ * @throws PermissionsError
+ */
+ function displayRestrictionError() {
+ $this->init();
+ $this->target->displayRestrictionError();
+ }
+
+ /**
+ * @return void
+ * @throws PermissionsError
+ */
+ public function checkPermissions() {
+ $this->init();
+ $this->target->checkPermissions();
+ }
+
+ /**
+ * @param string|null $subPage
+ */
+ protected function beforeExecute( $subPage ) {
+ $this->init();
+ $this->target->beforeExecute( $subPage );
+ }
+
+ /**
+ * @param string|null $subPage
+ */
+ protected function afterExecute( $subPage ) {
+ $this->init();
+ $this->target->afterExecute( $subPage );
+ }
+
+ /**
+ * @param string|null $subPage
+ */
+ public function execute( $subPage ) {
+ $this->init();
+ $this->target->execute( $subPage );
+ }
+
+ /**
+ * @return string
+ */
+ function getDescription() {
+ $this->init();
+ return $this->target->getDescription();
+ }
+
+ /**
+ * @param IContextSource $context
+ */
+ public function setContext( $context ) {
+ $this->init();
+ $this->target->setContext( $context );
+ parent::setContext( $context );
+ }
+
+ /**
+ * @return string
+ */
+ protected function getRobotPolicy() {
+ $this->init();
+ return $this->target->getRobotPolicy();
+ }
+
+ /**
+ * @return string
+ */
+ protected function getGroupName() {
+ $this->init();
+ return $this->target->getGroupName();
+ }
+}
diff --git a/www/wiki/extensions/OATHAuth/includes/special/SpecialOATH.php b/www/wiki/extensions/OATHAuth/includes/special/SpecialOATH.php
new file mode 100644
index 00000000..21a854c2
--- /dev/null
+++ b/www/wiki/extensions/OATHAuth/includes/special/SpecialOATH.php
@@ -0,0 +1,44 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+/**
+ * Proxy page that redirects to the proper OATH special page
+ */
+class SpecialOATH extends ProxySpecialPage {
+ /**
+ * If the user already has OATH enabled, show them a page to disable
+ * If the user has OATH disabled, show them a page to enable
+ *
+ * @return SpecialOATHDisable|SpecialOATHEnable
+ */
+ protected function getTargetPage() {
+ $repo = OATHAuthHooks::getOATHUserRepository();
+
+ $user = $repo->findByUser( $this->getUser() );
+
+ if ( $user->getKey() === null ) {
+ return new SpecialOATHEnable( $repo, $user );
+ } else {
+ return new SpecialOATHDisable( $repo, $user );
+ }
+ }
+
+ protected function getGroupName() {
+ return 'oath';
+ }
+}
diff --git a/www/wiki/extensions/OATHAuth/includes/special/SpecialOATHDisable.php b/www/wiki/extensions/OATHAuth/includes/special/SpecialOATHDisable.php
new file mode 100644
index 00000000..7e5758b9
--- /dev/null
+++ b/www/wiki/extensions/OATHAuth/includes/special/SpecialOATHDisable.php
@@ -0,0 +1,136 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+/**
+ * Special page to display key information to the user
+ *
+ * @ingroup Extensions
+ */
+class SpecialOATHDisable extends FormSpecialPage {
+ /** @var OATHUserRepository */
+ private $OATHRepository;
+
+ /** @var OATHUser */
+ private $OATHUser;
+
+ /**
+ * Initialize the OATH user based on the current local User object in the context
+ *
+ * @param OATHUserRepository $repository
+ * @param OATHUser $user
+ */
+ public function __construct( OATHUserRepository $repository, OATHUser $user ) {
+ parent::__construct( 'OATH', '', false );
+ $this->OATHRepository = $repository;
+ $this->OATHUser = $user;
+ }
+
+ public function doesWrites() {
+ return true;
+ }
+
+ /**
+ * Set the page title and add JavaScript RL modules
+ *
+ * @param HTMLForm $form
+ */
+ public function alterForm( HTMLForm $form ) {
+ $form->setMessagePrefix( 'oathauth' );
+ $form->setWrapperLegend( false );
+ $form->getOutput()->setPageTitle( $this->msg( 'oathauth-disable' ) );
+ }
+
+ /**
+ * @return string
+ */
+ protected function getDisplayFormat() {
+ return 'ooui';
+ }
+
+ /**
+ * @return bool
+ */
+ public function requiresUnblock() {
+ return false;
+ }
+
+ /**
+ * Require users to be logged in
+ *
+ * @param User $user
+ *
+ * @return bool|void
+ */
+ protected function checkExecutePermissions( User $user ) {
+ parent::checkExecutePermissions( $user );
+
+ $this->requireLogin();
+ }
+
+ /**
+ * @return array[]
+ */
+ protected function getFormFields() {
+ return [
+ 'token' => [
+ 'type' => 'text',
+ 'label-message' => 'oathauth-entertoken',
+ 'name' => 'token',
+ 'required' => true,
+ 'autofocus' => true,
+ ],
+ 'returnto' => [
+ 'type' => 'hidden',
+ 'default' => $this->getRequest()->getVal( 'returnto' ),
+ 'name' => 'returnto',
+ ],
+ 'returntoquery' => [
+ 'type' => 'hidden',
+ 'default' => $this->getRequest()->getVal( 'returntoquery' ),
+ 'name' => 'returntoquery',
+ ]
+ ];
+ }
+
+ /**
+ * @param array $formData
+ *
+ * @return array|bool
+ */
+ public function onSubmit( array $formData ) {
+ // Don't increase pingLimiter, just check for limit exceeded.
+ if ( $this->OATHUser->getUser()->pingLimiter( 'badoath', 0 ) ) {
+ // Arbitrary duration given here
+ return [ 'oathauth-throttled', Message::durationParam( 60 ) ];
+ }
+
+ if ( !$this->OATHUser->getKey()->verifyToken( $formData['token'], $this->OATHUser ) ) {
+ return [ 'oathauth-failedtovalidateoath' ];
+ }
+
+ $this->OATHUser->setKey( null );
+ $this->OATHRepository->remove( $this->OATHUser );
+
+ return true;
+ }
+
+ public function onSuccess() {
+ $this->getOutput()->addWikiMsg( 'oathauth-disabledoath' );
+ $this->getOutput()->returnToMain();
+ }
+}
diff --git a/www/wiki/extensions/OATHAuth/includes/special/SpecialOATHEnable.php b/www/wiki/extensions/OATHAuth/includes/special/SpecialOATHEnable.php
new file mode 100644
index 00000000..bf4b62e5
--- /dev/null
+++ b/www/wiki/extensions/OATHAuth/includes/special/SpecialOATHEnable.php
@@ -0,0 +1,241 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+/**
+ * Special page to display key information to the user
+ *
+ * @ingroup Extensions
+ */
+class SpecialOATHEnable extends FormSpecialPage {
+ /** @var OATHUserRepository */
+ private $OATHRepository;
+
+ /** @var OATHUser */
+ private $OATHUser;
+
+ /**
+ * Initialize the OATH user based on the current local User object in the context
+ *
+ * @param OATHUserRepository $repository
+ * @param OATHUser $user
+ */
+ public function __construct( OATHUserRepository $repository, OATHUser $user ) {
+ parent::__construct( 'OATH', 'oathauth-enable', false );
+
+ $this->OATHRepository = $repository;
+ $this->OATHUser = $user;
+ }
+
+ public function doesWrites() {
+ return true;
+ }
+
+ /**
+ * Set the page title and add JavaScript RL modules
+ *
+ * @param HTMLForm $form
+ */
+ public function alterForm( HTMLForm $form ) {
+ $form->setMessagePrefix( 'oathauth' );
+ $form->setWrapperLegend( false );
+ $form->getOutput()->setPageTitle( $this->msg( 'oathauth-enable' ) );
+ $form->getOutput()->addModules( 'ext.oath.showqrcode' );
+ $form->getOutput()->addModuleStyles( 'ext.oath.showqrcode.styles' );
+ }
+
+ /**
+ * @return string
+ */
+ protected function getDisplayFormat() {
+ return 'ooui';
+ }
+
+ /**
+ * @return bool
+ */
+ public function requiresUnblock() {
+ return false;
+ }
+
+ /**
+ * Require users to be logged in
+ *
+ * @param User $user
+ *
+ * @return bool|void
+ */
+ protected function checkExecutePermissions( User $user ) {
+ parent::checkExecutePermissions( $user );
+
+ $this->requireLogin();
+ }
+
+ /**
+ * @return array[]
+ */
+ protected function getFormFields() {
+ $key = $this->getRequest()->getSessionData( 'oathauth_key' );
+
+ if ( $key === null ) {
+ $key = OATHAuthKey::newFromRandom();
+ $this->getRequest()->setSessionData( 'oathauth_key', $key );
+ }
+
+ $secret = $key->getSecret();
+ $label = "{$this->OATHUser->getIssuer()}:{$this->OATHUser->getAccount()}";
+ $qrcodeUrl = "otpauth://totp/"
+ . rawurlencode( $label )
+ . "?secret="
+ . rawurlencode( $secret )
+ . "&issuer="
+ . rawurlencode( $this->OATHUser->getIssuer() );
+
+ $qrcodeElement = Html::element( 'div', [
+ 'data-mw-qrcode-url' => $qrcodeUrl,
+ 'class' => 'mw-display-qrcode',
+ // Include width/height, so js won't re-arrange layout
+ // And non-js users will have this hidden with CSS
+ 'style' => 'width: 256px; height: 256px;'
+ ] );
+
+ return [
+ 'app' => [
+ 'type' => 'info',
+ 'default' => $this->msg( 'oathauth-step1-test' )->escaped(),
+ 'raw' => true,
+ 'section' => 'step1',
+ ],
+ 'qrcode' => [
+ 'type' => 'info',
+ 'default' => $qrcodeElement,
+ 'raw' => true,
+ 'section' => 'step2',
+ ],
+ 'manual' => [
+ 'type' => 'info',
+ 'label-message' => 'oathauth-step2alt',
+ 'default' =>
+ '<strong>' . $this->msg( 'oathauth-account' )->escaped() . '</strong><br/>'
+ . $this->OATHUser->getAccount() . '<br/><br/>'
+ . '<strong>' . $this->msg( 'oathauth-secret' )->escaped() . '</strong><br/>'
+ . '<kbd>' . $this->getSecretForDisplay( $key ) . '</kbd><br/>',
+ 'raw' => true,
+ 'section' => 'step2',
+ ],
+ 'scratchtokens' => [
+ 'type' => 'info',
+ 'default' =>
+ $this->msg( 'oathauth-scratchtokens' )
+ . $this->createResourceList( $this->getScratchTokensForDisplay( $key ) ),
+ 'raw' => true,
+ 'section' => 'step3',
+ ],
+ 'token' => [
+ 'type' => 'text',
+ 'default' => '',
+ 'label-message' => 'oathauth-entertoken',
+ 'name' => 'token',
+ 'section' => 'step4',
+ ],
+ 'returnto' => [
+ 'type' => 'hidden',
+ 'default' => $this->getRequest()->getVal( 'returnto' ),
+ 'name' => 'returnto',
+ ],
+ 'returntoquery' => [
+ 'type' => 'hidden',
+ 'default' => $this->getRequest()->getVal( 'returntoquery' ),
+ 'name' => 'returntoquery', ]
+ ];
+ }
+
+ /**
+ * @param array $formData
+ *
+ * @return array|bool
+ */
+ public function onSubmit( array $formData ) {
+ /** @var OATHAuthKey $key */
+ $key = $this->getRequest()->getSessionData( 'oathauth_key' );
+
+ if ( $key->isScratchToken( $formData['token'] ) ) {
+ // A scratch token is not allowed for enrollement
+ return [ 'oathauth-noscratchforvalidation' ];
+ }
+ if ( !$key->verifyToken( $formData['token'], $this->OATHUser ) ) {
+ return [ 'oathauth-failedtovalidateoath' ];
+ }
+
+ $this->getRequest()->setSessionData( 'oathauth_key', null );
+ $this->OATHUser->setKey( $key );
+ $this->OATHRepository->persist( $this->OATHUser );
+
+ return true;
+ }
+
+ public function onSuccess() {
+ $this->getOutput()->addWikiMsg( 'oathauth-validatedoath' );
+ $this->getOutput()->returnToMain();
+ }
+
+ /**
+ * @param $resources array
+ * @return string
+ */
+ private function createResourceList( $resources ) {
+ $resourceList = '';
+ foreach ( $resources as $resource ) {
+ $resourceList .= Html::rawElement( 'li', [], Html::rawElement( 'kbd', [], $resource ) );
+ }
+ return Html::rawElement( 'ul', [], $resourceList );
+ }
+
+ /**
+ * Retrieve the current secret for display purposes
+ *
+ * The characters of the token are split in groups of 4
+ *
+ * @param OATHAuthKey $key
+ * @return String
+ */
+ protected function getSecretForDisplay( OATHAuthKey $key ) {
+ return $this->tokenFormatterFunction( $key->getSecret() );
+ }
+
+ /**
+ * Retrieve current scratch tokens for display purposes
+ *
+ * The characters of the token are split in groups of 4
+ *
+ * @param OATHAuthKey $key
+ * @return string[]
+ */
+ protected function getScratchTokensForDisplay( OATHAuthKey $key ) {
+ return array_map( [ $this, 'tokenFormatterFunction' ], $key->getScratchTokens() );
+ }
+
+ /**
+ * Formats a key or scratch token by creating groups of 4 seperated by space characters
+ *
+ * @param string $token Token to format
+ * @return string The token formatted for display
+ */
+ private function tokenFormatterFunction( $token ) {
+ return implode( ' ', str_split( $token, 4 ) );
+ }
+}