diff --git a/src/opnsense/mvc/app/library/OPNsense/Auth/LocalTOTP.php b/src/opnsense/mvc/app/library/OPNsense/Auth/LocalTOTP.php new file mode 100644 index 000000000..0962c014c --- /dev/null +++ b/src/opnsense/mvc/app/library/OPNsense/Auth/LocalTOTP.php @@ -0,0 +1,163 @@ +timeWindow = $config['timeWindow']; + } + if (!empty($config['otpLength'])) { + $this->otpLength = $config['otpLength']; + } + if (!empty($config['graceperiod'])) { + $this->otpLength = $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; + } + + /** + * 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; + } +}