From 63968b418eed45bc306997fa86d8e4e10dd77c42 Mon Sep 17 00:00:00 2001 From: Ad Schellevis Date: Sun, 28 Aug 2016 12:17:10 +0200 Subject: [PATCH] (auth, totp) isolate TOTP functionality into a trait, refactor LocalTOTP. all needed for https://github.com/opnsense/core/issues/1030 --- .../app/library/OPNsense/Auth/LocalTOTP.php | 174 +------------ .../mvc/app/library/OPNsense/Auth/TOTP.php | 232 ++++++++++++++++++ 2 files changed, 235 insertions(+), 171 deletions(-) create mode 100644 src/opnsense/mvc/app/library/OPNsense/Auth/TOTP.php diff --git a/src/opnsense/mvc/app/library/OPNsense/Auth/LocalTOTP.php b/src/opnsense/mvc/app/library/OPNsense/Auth/LocalTOTP.php index 010633bf7..66c426a9c 100644 --- a/src/opnsense/mvc/app/library/OPNsense/Auth/LocalTOTP.php +++ b/src/opnsense/mvc/app/library/OPNsense/Auth/LocalTOTP.php @@ -30,28 +30,13 @@ namespace OPNsense\Auth; -require_once 'base32/Base32.php'; - /** * RFC 6238 TOTP: Time-Based One-Time Password Authenticator * @package OPNsense\Auth */ class LocalTOTP extends Local { - /** - * @var int time window in seconds (google auth uses 30, some hardware tokens use 60) - */ - private $timeWindow = 30; - - /** - * @var int key length (6,8) - */ - private $otpLength = 6; - - /** - * @var int number of seconds the clocks (local, remote) may differ - */ - private $graceperiod = 10; + use TOTP; /** * type name in configuration @@ -78,117 +63,7 @@ class LocalTOTP extends Local public function setProperties($config) { parent::setProperties($config); - if (!empty($config['timeWindow'])) { - $this->timeWindow = $config['timeWindow']; - } - if (!empty($config['otpLength'])) { - $this->otpLength = $config['otpLength']; - } - if (!empty($config['graceperiod'])) { - $this->graceperiod = $config['graceperiod']; - } - } - - /** - * use graceperiod and timeWindow to calculate which moments in time we should check - * @return array timestamps - */ - private function timesToCheck() - { - $result = array(); - if ($this->graceperiod > $this->timeWindow) { - $step = $this->timeWindow; - $start = -1 * floor($this->graceperiod / $this->timeWindow) * $this->timeWindow; - } else { - $step = $this->graceperiod; - $start = -1 * $this->graceperiod; - } - $now = time(); - for ($count = $start; $count <= $this->graceperiod; $count += $step) { - $result[] = $now + $count; - if ($this->graceperiod == 0) { - // special case, we expect the clocks to match 100%, so step and target are both 0 - break; - } - } - return $result; - } - - /** - * @param int $moment timestemp - * @param string $secret secret to use - * @return calculated token code - */ - private function calculateToken($moment, $secret) - { - // calculate binary 8 character time for provided window - $binary_time = pack("N", (int)($moment/$this->timeWindow)); - $binary_time = str_pad($binary_time, 8, chr(0), STR_PAD_LEFT); - - // Generate the hash using the SHA1 algorithm - $hash = hash_hmac('sha1', $binary_time, $secret, true); - $offset = ord($hash[19]) & 0xf; - $otp = ( - ((ord($hash[$offset+0]) & 0x7f) << 24 ) | - ((ord($hash[$offset+1]) & 0xff) << 16 ) | - ((ord($hash[$offset+2]) & 0xff) << 8 ) | - (ord($hash[$offset+3]) & 0xff) - ) % pow(10, $this->otpLength); - - - $otp = str_pad($otp, $this->otpLength, "0", STR_PAD_LEFT); - return $otp; - } - - /** - * return current token code - * @param $base32seed secret to use - * @return string token code - */ - public function testToken($base32seed) - { - $otp_seed = \Base32\Base32::decode($base32seed); - return $this->calculateToken(time(), $otp_seed); - } - - /** - * authenticate TOTP RFC 6238 - * @param string $secret secret seed to use - * @param string $code provided code - * @return bool is valid - */ - private function authTOTP($secret, $code) - { - foreach ($this->timesToCheck() as $moment) { - if ($code == $this->calculateToken($moment, $secret)) { - return true; - } - } - return false; - } - - /** - * authenticate user against otp key stored in local database - * @param string $username username to authenticate - * @param string $password user password - * @return bool authentication status - */ - public function authenticate($username, $password) - { - $userObject = $this->getUser($username); - if ($userObject != null && !empty($userObject->otp_seed)) { - if (strlen($password) > $this->otpLength) { - // split otp token code and userpassword - $code = substr($password, 0, $this->otpLength); - $userPassword = substr($password, $this->otpLength); - $otp_seed = \Base32\Base32::decode($userObject->otp_seed); - if ($this->authTOTP($otp_seed, $code)) { - // token valid, do local auth - return parent::authenticate($userObject, $userPassword); - } - } - } - return false; + $this->setTOTPProperties($config); } /** @@ -197,49 +72,6 @@ class LocalTOTP extends Local */ public function getConfigurationOptions() { - $fields = array(); - $fields["otpLength"] = array(); - $fields["otpLength"]["name"] = gettext("Token length"); - $fields["otpLength"]["type"] = "dropdown"; - $fields["otpLength"]["default"] = 6; - $fields["otpLength"]["options"] = array(); - $fields["otpLength"]["options"]["6"] = "6"; - $fields["otpLength"]["options"]["8"] = "8"; - $fields["otpLength"]["help"] = gettext("Token length to use"); - $fields["otpLength"]["validate"] = function ($value) { - if (!in_array($value, array(6,8))) { - return array(gettext("Only token lengths of 6 or 8 characters are supported")); - } else { - return array(); - } - }; - $fields["timeWindow"] = array(); - $fields["timeWindow"]["name"] = gettext("Time window"); - $fields["timeWindow"]["type"] = "text"; - $fields["timeWindow"]["default"] = null; - $fields["timeWindow"]["help"] = gettext("The time period in which the token will be valid,". - " default is 30 seconds (google authenticator)"); - $fields["timeWindow"]["validate"] = function ($value) { - if (!empty($value) && filter_var($value, FILTER_SANITIZE_NUMBER_INT) != $value) { - return array(gettext("Please enter a valid time window in seconds")); - } else { - return array(); - } - }; - $fields["graceperiod"] = array(); - $fields["graceperiod"]["name"] = gettext("Grace period"); - $fields["graceperiod"]["type"] = "text"; - $fields["graceperiod"]["default"] = null; - $fields["graceperiod"]["help"] = gettext("Time in seconds in which this server and the token may differ,". - " default is 10 seconds. Set higher for a less secure easier match."); - $fields["graceperiod"]["validate"] = function ($value) { - if (!empty($value) && filter_var($value, FILTER_SANITIZE_NUMBER_INT) != $value) { - return array(gettext("Please enter a valid grace period in seconds")); - } else { - return array(); - } - }; - - return $fields; + return $this->getTOTPConfigurationOptions(); } } diff --git a/src/opnsense/mvc/app/library/OPNsense/Auth/TOTP.php b/src/opnsense/mvc/app/library/OPNsense/Auth/TOTP.php new file mode 100644 index 000000000..d0be27f82 --- /dev/null +++ b/src/opnsense/mvc/app/library/OPNsense/Auth/TOTP.php @@ -0,0 +1,232 @@ +graceperiod > $this->timeWindow) { + $step = $this->timeWindow; + $start = -1 * floor($this->graceperiod / $this->timeWindow) * $this->timeWindow; + } else { + $step = $this->graceperiod; + $start = -1 * $this->graceperiod; + } + $now = time(); + for ($count = $start; $count <= $this->graceperiod; $count += $step) { + $result[] = $now + $count; + if ($this->graceperiod == 0) { + // special case, we expect the clocks to match 100%, so step and target are both 0 + break; + } + } + return $result; + } + + /** + * @param int $moment timestemp + * @param string $secret secret to use + * @return calculated token code + */ + private function calculateToken($moment, $secret) + { + // calculate binary 8 character time for provided window + $binary_time = pack("N", (int)($moment/$this->timeWindow)); + $binary_time = str_pad($binary_time, 8, chr(0), STR_PAD_LEFT); + + // Generate the hash using the SHA1 algorithm + $hash = hash_hmac('sha1', $binary_time, $secret, true); + $offset = ord($hash[19]) & 0xf; + $otp = ( + ((ord($hash[$offset+0]) & 0x7f) << 24 ) | + ((ord($hash[$offset+1]) & 0xff) << 16 ) | + ((ord($hash[$offset+2]) & 0xff) << 8 ) | + (ord($hash[$offset+3]) & 0xff) + ) % pow(10, $this->otpLength); + + + $otp = str_pad($otp, $this->otpLength, "0", STR_PAD_LEFT); + return $otp; + } + + /** + * return current token code + * @param $base32seed secret to use + * @return string token code + */ + public function testToken($base32seed) + { + $otp_seed = \Base32\Base32::decode($base32seed); + return $this->calculateToken(time(), $otp_seed); + } + + /** + * authenticate TOTP RFC 6238 + * @param string $secret secret seed to use + * @param string $code provided code + * @return bool is valid + */ + private function authTOTP($secret, $code) + { + foreach ($this->timesToCheck() as $moment) { + if ($code == $this->calculateToken($moment, $secret)) { + return true; + } + } + return false; + } + + /** + * authenticate user against otp key stored in local database + * @param string $username username to authenticate + * @param string $password user password + * @return bool authentication status + */ + public function authenticate($username, $password) + { + $getUserMethod = $this->getUserMethod; + $userObject = $this->$getUserMethod($username); + if ($userObject != null && !empty($userObject->otp_seed)) { + if (strlen($password) > $this->otpLength) { + // split otp token code and userpassword + $code = substr($password, 0, $this->otpLength); + $userPassword = substr($password, $this->otpLength); + $otp_seed = \Base32\Base32::decode($userObject->otp_seed); + if ($this->authTOTP($otp_seed, $code)) { + // token valid, do parents auth + return parent::authenticate($userObject, $userPassword); + } + } + } + return false; + } + + /** + * set TOTP specific connector properties + * @param array $config connection properties + */ + public function setTOTPProperties($config) + { + if (!empty($config['timeWindow'])) { + $this->timeWindow = $config['timeWindow']; + } + if (!empty($config['otpLength'])) { + $this->otpLength = $config['otpLength']; + } + if (!empty($config['graceperiod'])) { + $this->graceperiod = $config['graceperiod']; + } + } + + /** + * retrieve TOTP specific configuration options + * @return array + */ + private function getTOTPConfigurationOptions() + { + $fields = array(); + $fields["otpLength"] = array(); + $fields["otpLength"]["name"] = gettext("Token length"); + $fields["otpLength"]["type"] = "dropdown"; + $fields["otpLength"]["default"] = 6; + $fields["otpLength"]["options"] = array(); + $fields["otpLength"]["options"]["6"] = "6"; + $fields["otpLength"]["options"]["8"] = "8"; + $fields["otpLength"]["help"] = gettext("Token length to use"); + $fields["otpLength"]["validate"] = function ($value) { + if (!in_array($value, array(6,8))) { + return array(gettext("Only token lengths of 6 or 8 characters are supported")); + } else { + return array(); + } + }; + $fields["timeWindow"] = array(); + $fields["timeWindow"]["name"] = gettext("Time window"); + $fields["timeWindow"]["type"] = "text"; + $fields["timeWindow"]["default"] = null; + $fields["timeWindow"]["help"] = gettext("The time period in which the token will be valid,". + " default is 30 seconds (google authenticator)"); + $fields["timeWindow"]["validate"] = function ($value) { + if (!empty($value) && filter_var($value, FILTER_SANITIZE_NUMBER_INT) != $value) { + return array(gettext("Please enter a valid time window in seconds")); + } else { + return array(); + } + }; + $fields["graceperiod"] = array(); + $fields["graceperiod"]["name"] = gettext("Grace period"); + $fields["graceperiod"]["type"] = "text"; + $fields["graceperiod"]["default"] = null; + $fields["graceperiod"]["help"] = gettext("Time in seconds in which this server and the token may differ,". + " default is 10 seconds. Set higher for a less secure easier match."); + $fields["graceperiod"]["validate"] = function ($value) { + if (!empty($value) && filter_var($value, FILTER_SANITIZE_NUMBER_INT) != $value) { + return array(gettext("Please enter a valid grace period in seconds")); + } else { + return array(); + } + }; + + return $fields; + } +}