openrat-cms

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit 35918ca50622c1f3a44cca67cb0cf293cd0c344a
parent 3f9baf0386540d4a33995624091771627635601e
Author: dankert <openrat@jandankert.de>
Date:   Wed, 23 Feb 2022 00:38:24 +0100

New: Enable HOTP with counter synchronization; New: TOTP of the last period are valid too.

Diffstat:
Mmodules/cms/action/user/UserEditAction.class.php | 3++-
Mmodules/cms/action/user/UserInfoAction.class.php | 3++-
Mmodules/cms/action/user/UserPropAction.class.php | 3++-
Mmodules/cms/auth/InternalAuth.class.php | 31+++++++++++++++++++++++++++----
Amodules/security/OTP.class.php | 147+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmodules/security/Password.class.php | 39+--------------------------------------
6 files changed, 181 insertions(+), 45 deletions(-)

diff --git a/modules/cms/action/user/UserEditAction.class.php b/modules/cms/action/user/UserEditAction.class.php @@ -6,6 +6,7 @@ use cms\base\Configuration; use cms\base\Startup; use cms\model\Group; use security\Base2n; +use security\OTP; use security\Password; @@ -48,7 +49,7 @@ class UserEditAction extends UserAction implements Method { $this->setTemplateVar('totpSecretUrl',"otpauth://totp/{$issuer}:{$account}?secret={$secret}&issuer={$issuer}"); $this->setTemplateVar('hotpSecretUrl',"otpauth://hotp/{$issuer}:{$account}?secret={$secret}&issuer={$issuer}&counter={$counter}"); - $this->setTemplateVar('totpToken' , Password::getTOTPCode($this->user->otpSecret)); + $this->setTemplateVar('totpToken' , OTP::getTOTPCode($this->user->otpSecret)); } diff --git a/modules/cms/action/user/UserInfoAction.class.php b/modules/cms/action/user/UserInfoAction.class.php @@ -6,6 +6,7 @@ use cms\base\Configuration; use cms\base\Startup; use cms\model\Group; use security\Base2n; +use security\OTP; use security\Password; @@ -48,7 +49,7 @@ class UserInfoAction extends UserAction implements Method { $this->setTemplateVar('totpSecretUrl',"otpauth://totp/{$issuer}:{$account}?secret={$secret}&issuer={$issuer}"); $this->setTemplateVar('hotpSecretUrl',"otpauth://hotp/{$issuer}:{$account}?secret={$secret}&issuer={$issuer}&counter={$counter}"); - $this->setTemplateVar('totpToken' , Password::getTOTPCode($this->user->otpSecret)); + $this->setTemplateVar('totpToken' , OTP::getTOTPCode($this->user->otpSecret)); } diff --git a/modules/cms/action/user/UserPropAction.class.php b/modules/cms/action/user/UserPropAction.class.php @@ -6,6 +6,7 @@ use cms\base\Configuration; use cms\base\Startup; use language\Messages; use security\Base2n; +use security\OTP; use security\Password; @@ -24,7 +25,7 @@ class UserPropAction extends UserAction implements Method { array('totpSecretUrl' => "otpauth://totp/{$issuer}:{$account}?secret={$secret}&issuer={$issuer}", 'hotpSecretUrl' => "otpauth://hotp/{$issuer}:{$account}?secret={$secret}&issuer={$issuer}&counter={$counter}" ) - + array('totpToken'=>Password::getTOTPCode($this->user->otpSecret)) + + array('totpToken'=> OTP::getTOTPCode($this->user->otpSecret)) ); $this->setTemplateVar( 'allstyles',$this->user->getAvailableStyles() ); diff --git a/modules/cms/auth/InternalAuth.class.php b/modules/cms/auth/InternalAuth.class.php @@ -7,7 +7,9 @@ use cms\base\DB as Db; use cms\base\Startup; use cms\model\User; use language\Messages; +use logger\Logger; use LogicException; +use security\OTP; use security\Password; use util\mail\Mail; @@ -133,14 +135,35 @@ SQL if ($row_user['totp'] == 1) { $user = new User($row_user['id']); $user->load(); - if (Password::getTOTPCode($user->otpSecret) == $token) + if ( DEVELOPMENT ) + Logger::info("valid TOTP tokens are \n".print_r(OTP::getValidTOTPCodes($user->otpSecret),true)); + if ( in_array( $token, OTP::getValidTOTPCodes($user->otpSecret) ) ) return Auth::STATUS_SUCCESS; else return Auth::STATUS_FAILED + Auth::STATUS_TOKEN_NEEDED; } - - if ($row_user['hotp'] == 1) { - throw new LogicException('HOTP not yet implemented.'); + elseif ($row_user['hotp'] == 1) { + $user = new User($row_user['id']); + $user->load(); + $validHOTPCodes = OTP::getValidHOTPCodes($user->otpSecret,$user->hotpCount); + if ( DEVELOPMENT ) + Logger::info("valid HOTP tokens are \n".print_r($validHOTPCodes,true)); + if ( in_array( $token, $validHOTPCodes ) ) { + // Synchronize the internal counter + $newCount = array_flip($validHOTPCodes)[$token]+1; + $sql = Db::sql(<<<SQL +UPDATE {{user}} + SET hotp_counter={count} + WHERE name={name} +SQL + ); + $sql->setString('name' ,$username ); + $sql->setInt ('count',$newCount ); + $sql->execute(); + return Auth::STATUS_SUCCESS; + } + else + return Auth::STATUS_FAILED + Auth::STATUS_TOKEN_NEEDED; } // Benutzer wurde erfolgreich authentifiziert. diff --git a/modules/security/OTP.class.php b/modules/security/OTP.class.php @@ -0,0 +1,147 @@ +<?php +namespace security; + + + +use cms\base\Startup; + +/** + * One time passwords (OTP). + * Both TOTP (time based OTP) and HOTP are supported. + * + * @author Jan Dankert + * + */ +class OTP +{ + /** + * Supporting only SHA1. + * This is the default as specified for the google authenticator. + */ + const SUPPORTED_ALGO = 'SHA1'; + + /** + * Default is 6 digits for the OTP. + */ + const OTP_LENGTH = 6; + + /** + * Duration of a TOTP timeslot in seconds. + */ + const TOTP_DURATION = 30; + + /** + * Allow the last n timeslots for TOTP. + */ + const TOTP_ALLOW_LAST = 1; + + /** + * Allow the next n counts for HOTP. + */ + const HOTP_ALLOW_NEXT = 10; + + + /** + * Calculate valids TOTPs for a secret. + * + * @param string $secret + * @return string valid OTP + */ + public static function getTOTPCode($secret) + { + // return the OTP for the actual timeslice and for the last one. + return self::getOTP( $secret, self::getTimeSlice() ); + } + + + /** + * Calculate valids TOTPs for a secret. + * + * @param string $secret + * @return array valid OTPs + */ + public static function getValidTOTPCodes($secret) + { + $actualTimeSlice = self::getTimeSlice(); + + // return the OTP for the actual timeslice and for the last one. + return self::getOTPs( $secret, range( + $actualTimeSlice - self::TOTP_ALLOW_LAST, + $actualTimeSlice + ) ); + } + + + /** + * Calculate the HOTP code, with given secret and counter. + * + * @param string $secret + * @param int $count + * @return array + */ + public static function getValidHOTPCodes($secret, $count) + { + return self::getOTPs($secret, range($count,$count+ self::HOTP_ALLOW_NEXT)); + } + + + /** + * Calculate OTP, with given secret and slot range. + * This calculates HOTP and TOTP values. + * + * @param string $secret + * @param array $slots + * + * @return array + */ + protected static function getOTPs($secret, $slotRange ) + { + // return the OTPs for the given range + return array_map(function ($slot) use ($secret) { + return OTP::getOTP($secret, $slot); + }, array_combine( $slotRange,$slotRange ) ); + } + + /** + * Calculate the code, with given secret and slot. + * This is valid for HOTP and TOTP. + * + * @param string $secret + * @param int|null $slot + * + * @return string + */ + protected static function getOTP( $secret, $slot ) + { + $secretKey = @hex2bin($secret); + // Pack time into binary string + $time = chr(0).chr(0).chr(0).chr(0).pack('N*', $slot); + // Hash it with users secret key + $hm = hash_hmac(self::SUPPORTED_ALGO, $time, $secretKey, true); + // Use last nipple of result as index/offset + $offset = ord(substr($hm, -1)) & 0x0F; + // grab 4 bytes of the result + $hashPart = substr($hm, $offset, 4); + // Unpak binary value + $value = unpack('N', $hashPart); + $value = $value[1]; + // Only 32 bits + $value = $value & 0x7FFFFFFF; + $modulo = pow(10, self::OTP_LENGTH); + return str_pad($value % $modulo, self::OTP_LENGTH, '0', STR_PAD_LEFT); + } + + + /** + * Actual timeslot. + * The number of timeslots since the beginning of unixtime. + * + * @return int timeslot + */ + public static function getTimeSlice() + { + return intval(Startup::getStartTime() / self::TOTP_DURATION ); + } + + +} diff --git a/modules/security/Password.class.php b/modules/security/Password.class.php @@ -230,41 +230,5 @@ class Password { time_nanosleep(0, Password::randomNumber(3)*10); // delay: 0-167772150ns (= 0-~168ms) } - - - - - - /** - * Calculate the code, with given secret and point in time. - * - * @param string $secret - * @param int|null $timeSlice - * - * @return string - */ - public static function getTOTPCode( $secret ) - { - $codeLength = 6; - $timeSlice = floor(time() / 30); - $secretkey = @hex2bin($secret); - // Pack time into binary string - $time = chr(0).chr(0).chr(0).chr(0).pack('N*', $timeSlice); - // Hash it with users secret key - $hm = hash_hmac('SHA1', $time, $secretkey, true); - // Use last nipple of result as index/offset - $offset = ord(substr($hm, -1)) & 0x0F; - // grab 4 bytes of the result - $hashpart = substr($hm, $offset, 4); - // Unpak binary value - $value = unpack('N', $hashpart); - $value = $value[1]; - // Only 32 bits - $value = $value & 0x7FFFFFFF; - $modulo = pow(10, $codeLength); - return str_pad($value % $modulo, $codeLength, '0', STR_PAD_LEFT); - } - - + } -?> -\ No newline at end of file