Seditio Source
Root |
./othercms/ips_4.3.4/system/MFA/GoogleAuthenticator/Handler.php
<?php
/**
 * @brief        Multi Factor Authentication Handler for Google Authenticator
 * @author        <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
 * @copyright    (c) Invision Power Services, Inc.
 * @license        https://www.invisioncommunity.com/legal/standards/
 * @package        Invision Community
 * @since        2 Sep 2016
 */

namespace IPS\MFA\GoogleAuthenticator;

/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
   
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
    exit;
}

/**
 * Multi Factor Authentication Handler for Google Authenticator
 */
class _Handler extends \IPS\MFA\MFAHandler
{
   
/**
     * @brief    Key
     */
   
protected $key = 'google';
   
   
/* !Setup */
   
    /**
     * Handler is enabled
     *
     * @return    bool
     */
   
public function isEnabled()
    {
        return \
IPS\Settings::i()->googleauth_enabled;
    }
   
   
/**
     * Member *can* use this handler (even if they have not yet configured it)
     *
     * @return    bool
     */
   
public function memberCanUseHandler( \IPS\Member $member )
    {
        return \
IPS\Settings::i()->googleauth_groups == '*' or $member->inGroup( explode( ',', \IPS\Settings::i()->googleauth_groups ) );
    }
   
   
/**
     * Member has configured this handler
     *
     * @param    \IPS\Member    $member    The member
     * @return    bool
     */
   
public function memberHasConfiguredHandler( \IPS\Member $member )
    {
        return isset(
$member->mfa_details['google'] );
    }
       
   
/**
     * Show a setup screen
     *
     * @param    \IPS\Member        $member                        The member
     * @param    bool            $showingMultipleHandlers    Set to TRUE if multiple options are being displayed
     * @param    \IPS\Http\Url    $url                        URL for page
     * @return    string
     */
   
public function configurationScreen( \IPS\Member $member, $showingMultipleHandlers=FALSE, \IPS\Http\Url $url )
    {
       
/* Generate a secret */
       
if ( isset( \IPS\Request::i()->secret ) )
        {
           
$secret = \IPS\Request::i()->secret;
        }
        else
        {
            if (
function_exists( 'random_bytes' ) )
            {
               
$randomString = random_bytes( 16 );
            }
            elseif (
function_exists( 'mcrypt_create_iv' ) )
            {
               
$randomString = mcrypt_create_iv( 16, MCRYPT_DEV_URANDOM );
            }
            elseif (
function_exists( 'openssl_random_pseudo_bytes' ) )
            {
               
$randomString = openssl_random_pseudo_bytes( 16 );
            }
            else
            {
               
$randomString = \substr( md5( uniqid( microtime(), true ) ) . md5( uniqid( microtime(), true ) ), 0, 16 );
            }
           
$validChars = array( 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',  'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '2', '3', '4', '5', '6', '7', '=' );
           
$secret = '';
            for (
$i = 0; $i < 16; ++$i )
            {
               
$secret .= $validChars[ ord( $randomString[ $i ] ) & 31 ];
            }
        }
       
       
/* Generate QR code */
       
$qrCode = \IPS\Http\Url::external("https://chart.googleapis.com/chart")->setQueryString( array(
           
'cht'    => 'qr',
           
'chs'    => '200x200',
           
'chl'    => "otpauth://totp/{$member->email}?secret={$secret}&issuer=" . rawurlencode( \IPS\Settings::i()->board_name ),
        ) );
       
       
/* Display */
       
return \IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->googleAuthenticatorSetup( $qrCode, $secret, $showingMultipleHandlers );
    }
   
   
/**
     * Submit configuration screen. Return TRUE if was accepted
     *
     * @param    \IPS\Member        $member    The member
     * @return    bool
     */
   
public function configurationScreenSubmit( \IPS\Member $member )
    {
        if ( \
IPS\Request::i()->google_authenticator_setup_code )
        {
            if ( static::
checkSubmittedCode( \IPS\Request::i()->google_authenticator_setup_code, \IPS\Request::i()->secret, $member ) )
            {
               
$mfaDetails = $member->mfa_details;
               
$mfaDetails['google'] = \IPS\Request::i()->secret;
               
$member->mfa_details = $mfaDetails;
               
$member->save();

               
/* Log MFA Enable */
               
$member->logHistory( 'core', 'mfa', array( 'handler' => $this->key, 'enable' => TRUE ) );

                return
TRUE;
            }
        }
        return
FALSE;
    }
   
   
/* !Authentication */
   
    /**
     * Get the form for a member to authenticate
     *
     * @param    \IPS\Member        $member        The member
     * @param    \IPS\Http\Url    $url        URL for page
     * @return    string
     */
   
public function authenticationScreen( \IPS\Member $member, \IPS\Http\Url $url )
    {
        try
        {
           
$waitUntil = ( \IPS\Db::i()->select( 'time', 'core_googleauth_used_codes', array( 'member=?', $member->member_id ), 'time DESC', 1 )->first() * 30 ) + 30;
        }
        catch ( \
UnderflowException $e )
        {
           
$waitUntil = NULL;
        }
               
        return \
IPS\Theme::i()->getTemplate( 'login', 'core', 'global' )->googleAuthenticatorAuth( $waitUntil );
    }
   
   
/**
     * Submit authentication screen. Return TRUE if was accepted
     *
     * @param    \IPS\Member        $member    The member
     * @return    string
     */
   
public function authenticationScreenSubmit( \IPS\Member $member )
    {
        if ( \
IPS\Request::i()->google_authenticator_auth_code )
        {
            if (
$codeTime = static::checkSubmittedCode( \IPS\Request::i()->google_authenticator_auth_code, $member->mfa_details['google'], $member ) )
            {
                \
IPS\Db::i()->insert( 'core_googleauth_used_codes', array(
                   
'member'    => $member->member_id,
                   
'time'        => $codeTime
               
) );
                return
TRUE;
            }
            return
FALSE;
        }
        return
FALSE;
    }
   
   
/* !ACP */
   
    /**
     * Toggle
     *
     * @param    bool    $enabled    On/Off
     * @return    bool
     */
   
public function toggle( $enabled )
    {
        \
IPS\Settings::i()->changeValues( array( 'googleauth_enabled' => $enabled ) );
    }
   
   
/**
     * ACP Settings
     *
     * @return    string
     */
   
public function acpSettings()
    {
       
$form = new \IPS\Helpers\Form;
       
$form->add( new \IPS\Helpers\Form\Select( 'googleauth_groups', \IPS\Settings::i()->googleauth_groups == '*' ? '*' : explode( ',', \IPS\Settings::i()->googleauth_groups ), FALSE, array(
           
'multiple'        => TRUE,
           
'options'        => array_combine( array_keys( \IPS\Member\Group::groups() ), array_map( function( $_group ) { return (string) $_group; }, \IPS\Member\Group::groups() ) ),
           
'unlimited'        => '*',
           
'unlimitedLang'    => 'everyone'
       
) ) );
       
        if (
$values = $form->values() )
        {
           
$values['googleauth_groups'] = ( $values['googleauth_groups'] == '*' ) ? '*' : implode( ',', $values['googleauth_groups'] );
           
$form->saveAsSettings( $values );    
            \
IPS\Session::i()->log( 'acplogs__mfa_handler_enabled', array( "mfa_google_title" => TRUE ) );        
            \
IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=core&module=settings&controller=mfa' ), 'saved' );
        }
       
        return (string)
$form;
    }
   
   
/* !Misc */
   
    /**
     * If member has configured this handler, disable it
     *
     * @param    \IPS\Member    $member    The member
     * @return    bool
     */
   
public function disableHandlerForMember( \IPS\Member $member )
    {
       
$mfaDetails = $member->mfa_details;
        unset(
$mfaDetails['google'] );
       
$member->mfa_details = $mfaDetails;
       
$member->save();

       
/* Log MFA Disable */
       
$member->logHistory( 'core', 'mfa', array( 'handler' => $this->key, 'enable' => FALSE ) );
    }
   
   
/* !Helper Methods */
   
    /**
     * Verify a submitted code with a ±30 seconds leeway
     *
     * @param    string            $submittedCode        The code that was submitted
     * @param    string            $secret                The secret key
     * @param    \IPS\Member        $member                The member this is for
     * @return    int|FALSE
     */
   
protected static function checkSubmittedCode( $submittedCode, $secret, $member )
    {
       
$submittedCode = str_replace( ' ', '', $submittedCode );
               
       
$validTimes = array( new \IPS\DateTime(), ( new \IPS\DateTime() )->add( new \DateInterval('PT30S') ), ( new \IPS\DateTime() )->sub( new \DateInterval('PT30S') ) );
       
$blockedTimes = iterator_to_array( \IPS\Db::i()->select( 'time', 'core_googleauth_used_codes', array( 'member=?', $member->member_id ) ) );

       
$allowedCodes = array();
        foreach (
$validTimes as $time )
        {
           
$codeTime = floor( $time->getTimestamp() / 30 );
            if ( !
in_array( $codeTime, $blockedTimes ) )
            {
               
$allowedCodes[ static::getCodeForSecretAtTime( $secret, $time ) ] = $codeTime;
            }
        }
       
        foreach (
$allowedCodes as $code => $time )
        {
            if ( \
IPS\Login::compareHashes( (string) $code, $submittedCode ) )
            {
                return
$time;
            }
        }
       
        return
FALSE;
    }
   
   
/**
     * Get the code
     *
     * @param    string            $secret        The secret key for the user
     * @param    \IPS\DateTime    $time    Timestamp
     * @return    string
     */
   
protected static function getCodeForSecretAtTime( $secret, \IPS\DateTime $time )
    {
       
/* Decode secret key */
       
$secret = str_split( str_replace( '=', '', $secret ) );
       
$chars = 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, '=' => 32 );        
       
$decodedSecretKey = '';
        for (
$i = 0; $i < 16; $i += 8 )
        {
           
$block = '';
            for (
$j = 0; $j < 8; ++$j )
            {
               
$block .= str_pad( base_convert( $chars[ $secret[ $i + $j ] ], 10, 2 ), 5, '0', STR_PAD_LEFT );
            }
           
$eightBits = str_split( $block, 8 );
            for (
$z = 0; $z < count( $eightBits ); ++$z )
            {
               
$decodedSecretKey .=  ( ( $y = chr( base_convert( $eightBits[ $z ], 2, 10) ) ) || ord( $y ) == 48) ? $y : '';
            }
        }
               
       
/* Hash the timestamp with the secret key */
       
$hash = hash_hmac('SHA1', chr(0).chr(0).chr(0).chr(0).pack('N*', floor( $time->getTimestamp() / 30 ) ), $decodedSecretKey, true);
       
       
/* Unpack it */
       
$value = unpack( 'N', \substr( $hash, ord( \substr( $hash, -1 ) ) & 0x0F, 4 ) );
       
$value = $value[1];
       
       
/* Get 32 bits */
       
$value = $value & 0x7FFFFFFF;
        return
str_pad( $value % pow( 10, 6 ), 6, '0', STR_PAD_LEFT );
    }
}