From 5926f62bd17795d707980e774e9478594aa3af7d Mon Sep 17 00:00:00 2001 From: Nathan Adams Date: Sat, 10 Aug 2013 20:58:59 -0500 Subject: [PATCH] Adding OTP support --- indefero/src/IDF/Form/UserAccount.php | 19 +++ indefero/src/IDF/Plugin/SyncMercurial.php | 2 + .../src/IDF/templates/idf/user/myaccount.html | 40 +++++- pluf/src/Pluf/User.php | 36 +++++- pluf/src/Pluf/Utils.php | 31 +++++ pluf/src/Pluf/thirdparty/otp/hotp.php | 74 +++++++++++ pluf/src/Pluf/thirdparty/otp/otp.php | 120 ++++++++++++++++++ pluf/src/Pluf/thirdparty/otp/otphp.php | 26 ++++ pluf/src/Pluf/thirdparty/otp/totp.php | 106 ++++++++++++++++ .../src/Pluf/thirdparty/otp/vendor/base32.php | 82 ++++++++++++ pluf/src/Pluf/thirdparty/otp/vendor/libs.php | 26 ++++ 11 files changed, 554 insertions(+), 8 deletions(-) create mode 100644 pluf/src/Pluf/thirdparty/otp/hotp.php create mode 100644 pluf/src/Pluf/thirdparty/otp/otp.php create mode 100644 pluf/src/Pluf/thirdparty/otp/otphp.php create mode 100644 pluf/src/Pluf/thirdparty/otp/totp.php create mode 100644 pluf/src/Pluf/thirdparty/otp/vendor/base32.php create mode 100644 pluf/src/Pluf/thirdparty/otp/vendor/libs.php diff --git a/indefero/src/IDF/Form/UserAccount.php b/indefero/src/IDF/Form/UserAccount.php index 37ba62b..7f3381c 100644 --- a/indefero/src/IDF/Form/UserAccount.php +++ b/indefero/src/IDF/Form/UserAccount.php @@ -172,8 +172,25 @@ class IDF_Form_UserAccount extends Pluf_Form 'initial' => '', 'help_text' => __('You will get an email to confirm that you own the address you specify.'), )); + $otp = ""; + if ($user_data->otpkey != "") + $otp = Pluf_Utils::convBase($this->user->otpkey, '0123456789abcdef', 'abcdefghijklmnopqrstuvwxyz234567'); + $this->fields['otpkey'] = new Pluf_Form_Field_Varchar( + array('required' => false, + 'label' => __('Add a OTP Key'), + //'initial' => (!empty($user_data->otpkey)) ? : "", + //'initial' => (string)(!empty($user_data->otpkey)), + 'initial' => $otp, + 'help_text' => __('Key must be in base32 for generated QRcode and import into Google Authenticator.'), + 'widget_attrs' => array( + 'maxlength' => 50, + 'size' => 32, + ), + )); } + + private function send_validation_mail($new_email, $secondary_mail=false) { if ($secondary_mail) { @@ -243,6 +260,8 @@ class IDF_Form_UserAccount extends Pluf_Form } if ($commit) { + if ($this->cleaned_data["otpkey"] != "") + $this->user->otpkey = Pluf_Utils::convBase($this->cleaned_data["otpkey"], 'abcdefghijklmnopqrstuvwxyz234567', '0123456789abcdef'); $this->user->update(); // FIXME: go the extra mile and check the input lengths for diff --git a/indefero/src/IDF/Plugin/SyncMercurial.php b/indefero/src/IDF/Plugin/SyncMercurial.php index 98998b3..097c050 100644 --- a/indefero/src/IDF/Plugin/SyncMercurial.php +++ b/indefero/src/IDF/Plugin/SyncMercurial.php @@ -231,6 +231,8 @@ class IDF_Plugin_SyncMercurial $fcontent .= 'shortname).'>'."\n"; $fcontent .= 'AuthType Basic'."\n"; $fcontent .= 'AuthName "Restricted"'."\n"; + $fcontent .= 'AuthExternal otpauth\n'; + $fcontent .= 'AuthBasicProvider external\n'; $fcontent .= sprintf('AuthUserFile %s', Pluf::f('idf_plugin_syncmercurial_passwd_file'))."\n"; $fcontent .= sprintf('Require user %s', $user)."\n"; $fcontent .= ''."\n\n"; diff --git a/indefero/src/IDF/templates/idf/user/myaccount.html b/indefero/src/IDF/templates/idf/user/myaccount.html index 4fceeae..49969fb 100644 --- a/indefero/src/IDF/templates/idf/user/myaccount.html +++ b/indefero/src/IDF/templates/idf/user/myaccount.html @@ -100,6 +100,16 @@ {$form.f.public_key.help_text} + + {$form.f.otpkey.labelTag}: + {if $form.f.otpkey.errors}{$form.f.otpkey.fieldErrors}{/if} + {$form.f.otpkey|unsafe} Generate
+ {$form.f.otpkey.help_text} +
+
+
+ + {trans "Secondary Emails"} {$form.f.secondary_mail.labelTag}: @@ -153,8 +163,10 @@

{trans 'The extra password is used to access some of the external systems and the API key is used to interact with this website using a program.'}

{/block} -{block javascript} {/block} diff --git a/pluf/src/Pluf/User.php b/pluf/src/Pluf/User.php index 8a8d17f..e70adee 100644 --- a/pluf/src/Pluf/User.php +++ b/pluf/src/Pluf/User.php @@ -21,6 +21,8 @@ # # ***** END LICENSE BLOCK ***** */ +require_once dirname(__FILE__).'/thirdparty/otp/otphp.php'; + /** * User Model. */ @@ -155,6 +157,14 @@ class Pluf_User extends Pluf_Model 'verbose' => __('last login'), 'editable' => false, ), + 'otpkey' => + array( + 'type' => 'Pluf_DB_Field_Varchar', + 'blank' => true, + 'size' => 50, + 'verbose' => __('OTP Key for user'), + 'help_text' => __('OTP Key used for authentication against repos.') + ), ); $this->_a['idx'] = array( 'login_idx' => @@ -237,9 +247,7 @@ class Pluf_User extends Pluf_Model { //$salt = Pluf_Utils::getRandomString(5); //$this->password = 'sha1:'.$salt.':'.sha1($salt.$password); - //$this->password = sha1($password); - //file_put_contents("/tmp/test", $password); - $this->password = base64_encode(sha1($password, TRUE)); + $this->password = base64_encode(sha1($password, TRUE)); return true; } @@ -254,10 +262,24 @@ class Pluf_User extends Pluf_Model if ($this->password == '') { return false; } - if ($this->password == base64_encode(sha1($password, TRUE))) - return true; - else - return false; + if ($this->otpkey == "") + { + if ($this->password == base64_encode(sha1($password, TRUE))) + return true; + else + return false; + } else { + $otp = substr($password, 0, 6); + $pass = substr($password, 6); + $totp = new \OTPHP\TOTP(strtoupper($this->otpkey)); + if ($totp->verify($otp) && $this->password == base64_encode(sha1($pass, TRUE))) + { + return true; + } else { + return false; + } + + } /*list($algo, $salt, $hash) = explode(':', $this->password); if ($hash == $algo($salt.$password)) { return true; diff --git a/pluf/src/Pluf/Utils.php b/pluf/src/Pluf/Utils.php index 95d4765..06d5fe0 100644 --- a/pluf/src/Pluf/Utils.php +++ b/pluf/src/Pluf/Utils.php @@ -71,6 +71,37 @@ class Pluf_Utils return $string; } + static public function convBase($numberInput, $fromBaseInput, $toBaseInput) + { + if ($fromBaseInput==$toBaseInput) return $numberInput; + $fromBase = str_split($fromBaseInput,1); + $toBase = str_split($toBaseInput,1); + $number = str_split($numberInput,1); + $fromLen=strlen($fromBaseInput); + $toLen=strlen($toBaseInput); + $numberLen=strlen($numberInput); + $retval=''; + if ($toBaseInput == '0123456789') + { + $retval=0; + for ($i = 1;$i <= $numberLen; $i++) + $retval = bcadd($retval, bcmul(array_search($number[$i-1], $fromBase),bcpow($fromLen,$numberLen-$i))); + return $retval; + } + if ($fromBaseInput != '0123456789') + $base10=Pluf_Utils::convBase($numberInput, $fromBaseInput, '0123456789'); + else + $base10 = $numberInput; + if ($base10generateOTP($count); + } + + + /** + * Verify if a password is valid for a specific counter value + * + * @param integer $otp the one-time password + * @param integer $counter the counter value + * @return bool true if the counter is valid, false otherwise + */ + public function verify($otp, $counter) { + return ($otp == $this->at($counter)); + } + + /** + * Returns the uri for a specific secret for hotp method. + * Can be encoded as a image for simple configuration in + * Google Authenticator. + * + * @param string $name the name of the account / profile + * @param integer $initial_count the initial counter + * @return string the uri for the hmac secret + */ + public function provisioning_uri($name, $initial_count) { + return "otpauth://hotp/".urlencode($name)."?secret={$this->secret}&counter=$initial_count"; + } + } + +} \ No newline at end of file diff --git a/pluf/src/Pluf/thirdparty/otp/otp.php b/pluf/src/Pluf/thirdparty/otp/otp.php new file mode 100644 index 0000000..015a4f6 --- /dev/null +++ b/pluf/src/Pluf/thirdparty/otp/otp.php @@ -0,0 +1,120 @@ +digits = isset($opt['digits']) ? $opt['digits'] : 6; + $this->digest = isset($opt['digest']) ? $opt['digest'] : 'sha1'; + $this->secret = $secret; + } + + /** + * Generate a one-time password + * + * @param integer $input : number used to seed the hmac hash function. + * This number is usually a counter (HOTP) or calculated based on the current + * timestamp (see TOTP class). + * @return integer the one-time password + */ + public function generateOTP($input) { + $hash = hash_hmac($this->digest, $this->intToBytestring($input), $this->byteSecret()); + foreach(str_split($hash, 2) as $hex) { // stupid PHP has bin2hex but no hex2bin WTF + $hmac[] = hexdec($hex); + } + $offset = $hmac[19] & 0xf; + $code = ($hmac[$offset+0] & 0x7F) << 24 | + ($hmac[$offset + 1] & 0xFF) << 16 | + ($hmac[$offset + 2] & 0xFF) << 8 | + ($hmac[$offset + 3] & 0xFF); + return $code % pow(10, $this->digits); + } + + /** + * Returns the binary value of the base32 encoded secret + * @access private + * This method should be private but was left public for + * phpunit tests to work. + * @return binary secret key + */ + public function byteSecret() { + return \Base32::decode($this->secret); + } + + /** + * Turns an integer in a OATH bytestring + * @param integer $int + * @access private + * @return string bytestring + */ + public function intToBytestring($int) { + $result = Array(); + while($int != 0) { + $result[] = chr($int & 0xFF); + $int >>= 8; + } + return str_pad(join(array_reverse($result)), 8, "\000", STR_PAD_LEFT); + } + } +} \ No newline at end of file diff --git a/pluf/src/Pluf/thirdparty/otp/otphp.php b/pluf/src/Pluf/thirdparty/otp/otphp.php new file mode 100644 index 0000000..6d5825f --- /dev/null +++ b/pluf/src/Pluf/thirdparty/otp/otphp.php @@ -0,0 +1,26 @@ +interval = isset($opt['interval']) ? $opt['interval'] : 30; + parent::__construct($s, $opt); + } + + /** + * Get the password for a specific timestamp value + * + * @param integer $timestamp the timestamp which is timecoded and + * used to seed the hmac hash function. + * @return integer the One Time Password + */ + public function at($timestamp) { + return $this->generateOTP($this->timecode($timestamp)); + } + + /** + * Get the password for the current timestamp value + * + * @return integer the current One Time Password + */ + public function now() { + return $this->generateOTP($this->timecode(time())); + } + + /** + * Verify if a password is valid for a specific counter value + * + * @param integer $otp the one-time password + * @param integer $timestamp the timestamp for the a given time, defaults to current time. + * @return bool true if the counter is valid, false otherwise + */ + public function verify($otp, $timestamp = null) { + if($timestamp === null) + $timestamp = time(); + return ($otp == $this->at($timestamp)); + } + + /** + * Returns the uri for a specific secret for totp method. + * Can be encoded as a image for simple configuration in + * Google Authenticator. + * + * @param string $name the name of the account / profile + * @return string the uri for the hmac secret + */ + public function provisioning_uri($name) { + return "otpauth://totp/".urlencode($name)."?secret={$this->secret}"; + } + + /** + * Transform a timestamp in a counter based on specified internal + * + * @param integer $timestamp + * @return integer the timecode + */ + protected function timecode($timestamp) { + return (int)( (((int)$timestamp * 1000) / ($this->interval * 1000))); + } + } + +} \ No newline at end of file diff --git a/pluf/src/Pluf/thirdparty/otp/vendor/base32.php b/pluf/src/Pluf/thirdparty/otp/vendor/base32.php new file mode 100644 index 0000000..9d01898 --- /dev/null +++ b/pluf/src/Pluf/thirdparty/otp/vendor/base32.php @@ -0,0 +1,82 @@ +'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' + ); + + /** + * Use padding false when encoding for urls + * + * @return base32 encoded string + * @author Bryan Ruiz + **/ + public static function encode($input, $padding = true) { + if(empty($input)) return ""; + $input = str_split($input); + $binaryString = ""; + for($i = 0; $i < count($input); $i++) { + $binaryString .= str_pad(base_convert(ord($input[$i]), 10, 2), 8, '0', STR_PAD_LEFT); + } + $fiveBitBinaryArray = str_split($binaryString, 5); + $base32 = ""; + $i=0; + while($i < count($fiveBitBinaryArray)) { + $base32 .= self::$map[base_convert(str_pad($fiveBitBinaryArray[$i], 5,'0'), 2, 10)]; + $i++; + } + if($padding && ($x = strlen($binaryString) % 40) != 0) { + if($x == 8) $base32 .= str_repeat(self::$map[32], 6); + else if($x == 16) $base32 .= str_repeat(self::$map[32], 4); + else if($x == 24) $base32 .= str_repeat(self::$map[32], 3); + else if($x == 32) $base32 .= self::$map[32]; + } + return $base32; + } + + public static function decode($input) { + if(empty($input)) return; + $paddingCharCount = substr_count($input, self::$map[32]); + $allowedValues = array(6,4,3,1,0); + if(!in_array($paddingCharCount, $allowedValues)) return false; + for($i=0; $i<4; $i++){ + if($paddingCharCount == $allowedValues[$i] && + substr($input, -($allowedValues[$i])) != str_repeat(self::$map[32], $allowedValues[$i])) return false; + } + $input = str_replace('=','', $input); + $input = str_split($input); + $binaryString = ""; + for($i=0; $i < count($input); $i = $i+8) { + $x = ""; + if(!in_array($input[$i], self::$map)) return false; + for($j=0; $j < 8; $j++) { + $x .= str_pad(base_convert(@self::$flippedMap[@$input[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT); + } + $eightBits = str_split($x, 8); + for($z = 0; $z < count($eightBits); $z++) { + $binaryString .= ( ($y = chr(base_convert($eightBits[$z], 2, 10))) || ord($y) == 48 ) ? $y:""; + } + } + return $binaryString; + } +} diff --git a/pluf/src/Pluf/thirdparty/otp/vendor/libs.php b/pluf/src/Pluf/thirdparty/otp/vendor/libs.php new file mode 100644 index 0000000..e509e51 --- /dev/null +++ b/pluf/src/Pluf/thirdparty/otp/vendor/libs.php @@ -0,0 +1,26 @@ +