Adding OTP support

This commit is contained in:
Nathan Adams 2013-08-10 20:58:59 -05:00
parent 5fc1c99ccf
commit 5926f62bd1
11 changed files with 554 additions and 8 deletions

View File

@ -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

View File

@ -231,6 +231,8 @@ class IDF_Plugin_SyncMercurial
$fcontent .= '<Location '. sprintf(Pluf::f('idf_plugin_syncmercurial_private_url'), $project->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 .= '</Location>'."\n\n";

View File

@ -100,6 +100,16 @@
<span class="helptext">{$form.f.public_key.help_text}</span>
</td>
</tr>
<tr>
<th>{$form.f.otpkey.labelTag}:</th>
<td>{if $form.f.otpkey.errors}{$form.f.otpkey.fieldErrors}{/if}
{$form.f.otpkey|unsafe} <a id="id_otpgen" href="#">Generate</a><br />
<span class="helptext">{$form.f.otpkey.help_text}</span>
<br/>
<br/>
<div id="QRcode"></div>
</td>
</tr>
<tr><td colspan="2" class="separator">{trans "Secondary Emails"}</td></tr>
<tr>
<th>{$form.f.secondary_mail.labelTag}:</th>
@ -153,8 +163,10 @@
<p>{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.'}</p>
</div>{/block}
{block javascript}<script type="text/javascript">
{block javascript}
<script type="text/javascript">
document.getElementById('id_first_name').focus();
var user = "{$user.login}";
{literal}
$(document).ready(function() {
// Hide the key password by default.
@ -165,6 +177,32 @@ $(document).ready(function() {
return false;
});
$(".pass-info").hide();
$("#id_otpkey").bind('input', function ()
{
if ($("#id_otpkey").val() != "")
{
var url = "https://chart.googleapis.com/chart?chs=300x300&cht=qr&chl=otpauth%3A%2F%2Ftotp%2F" + user + "?secret=" + $("#id_otpkey").val() + "%26issuer=srchub&choe=UTF-8";
$("#QRcode").html('<img src="' + url + '" />');
} else {
$("#QRcode").html('');
}
});
$("#id_otpgen").click(function ()
{
var chars = "abcdefghijklmnopqrstuvwxyz234567";
var key = "";
for(var i = 0; i < 32; i++)
key += chars[Math.floor(Math.random() * 32)];
$("#id_otpkey").val(key);
var url = "https://chart.googleapis.com/chart?chs=300x300&cht=qr&chl=otpauth%3A%2F%2Ftotp%2F" + user + "?secret=" + $("#id_otpkey").val().toUpperCase() + "%26issuer=srchub&choe=UTF-8";
$("#QRcode").html('<img src="' + url + '" />')
return false;
});
if ($("#id_otpkey").val() != "")
{
var url = "https://chart.googleapis.com/chart?chs=300x300&cht=qr&chl=otpauth%3A%2F%2Ftotp%2F" + user + "?secret=" + $("#id_otpkey").val().toUpperCase() + "%26issuer=srchub&choe=UTF-8";
$("#QRcode").html('<img src="' + url + '" />')
}
});{/literal}
</script>
{/block}

View File

@ -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;

View File

@ -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 ($base10<strlen($toBaseInput))
return $toBase[$base10];
while($base10 != '0')
{
$retval = $toBase[bcmod($base10,$toLen)].$retval;
$base10 = bcdiv($base10,$toLen,0);
}
return $retval;
}
/**
* Clean the name of a file to only have alphanumeric characters.
*

74
pluf/src/Pluf/thirdparty/otp/hotp.php vendored Normal file
View File

@ -0,0 +1,74 @@
<?php
/*
* Copyright (c) 2011 Le Lag
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
namespace OTPHP {
/**
* HOTP - One time password generator
*
* The HOTP class allow for the generation
* and verification of one-time password using
* the HOTP specified algorithm.
*
* This class is meant to be compatible with
* Google Authenticator
*
* This class was originally ported from the rotp
* ruby library available at https://github.com/mdp/rotp
*/
class HOTP extends OTP {
/**
* Get the password for a specific counter value
* @param integer $count the counter which is used to
* seed the hmac hash function.
* @return integer the One Time Password
*/
public function at($count) {
return $this->generateOTP($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";
}
}
}

120
pluf/src/Pluf/thirdparty/otp/otp.php vendored Normal file
View File

@ -0,0 +1,120 @@
<?php
/*
* Copyright (c) 2011 Le Lag
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
namespace OTPHP {
/**
* One Time Password Generator
*
* The OTP class allow the generation of one-time
* password that is described in rfc 4xxx.
*
* This is class is meant to be compatible with
* Google Authenticator.
*
* This class was originally ported from the rotp
* ruby library available at https://github.com/mdp/rotp
*/
class OTP {
/**
* The base32 encoded secret key
* @var string
*/
public $secret;
/**
* The algorithm used for the hmac hash function
* @var string
*/
public $digest;
/**
* The number of digits in the one-time password
* @var integer
*/
public $digits;
/**
* Constructor for the OTP class
* @param string $secret the secret key
* @param array $opt options array can contain the
* following keys :
* @param integer digits : the number of digits in the one time password
* Currently Google Authenticator only support 6. Defaults to 6.
* @param string digest : the algorithm used for the hmac hash function
* Google Authenticator only support sha1. Defaults to sha1
*
* @return new OTP class.
*/
public function __construct($secret, $opt = Array()) {
$this->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);
}
}
}

26
pluf/src/Pluf/thirdparty/otp/otphp.php vendored Normal file
View File

@ -0,0 +1,26 @@
<?php
/*
* Copyright (c) 2011 Le Lag
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
require_once dirname(__FILE__).'/vendor/libs.php';
require_once dirname(__FILE__).'/otp.php';
require_once dirname(__FILE__).'/hotp.php';
require_once dirname(__FILE__).'/totp.php';

106
pluf/src/Pluf/thirdparty/otp/totp.php vendored Normal file
View File

@ -0,0 +1,106 @@
<?php
/*
* Copyright (c) 2011 Le Lag
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
namespace OTPHP {
/**
* TOTP - One time password generator
*
* The TOTP class allow for the generation
* and verification of one-time password using
* the TOTP specified algorithm.
*
* This class is meant to be compatible with
* Google Authenticator
*
* This class was originally ported from the rotp
* ruby library available at https://github.com/mdp/rotp
*/
class TOTP extends OTP {
/**
* The interval in seconds for a one-time password timeframe
* Defaults to 30
* @var integer
*/
public $interval;
public function __construct($s, $opt = Array()) {
$this->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)));
}
}
}

View File

@ -0,0 +1,82 @@
<?php
/**
* Encode in Base32 based on RFC 4648.
* Requires 20% more space than base64
* Great for case-insensitive filesystems like Windows and URL's (except for = char which can be excluded using the pad option for urls)
*
* @package default
* @author Bryan Ruiz
**/
class Base32 {
private static $map = array(
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 7
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23
'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31
'=' // padding char
);
private static $flippedMap = 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'
);
/**
* 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;
}
}

View File

@ -0,0 +1,26 @@
<?php
/*
* Copyright (c) 2011 Le Lag
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
// Add any needed third party library to this directory
//require_once dirname(__FILE__).'/some_lib/lib.php';
require_once dirname(__FILE__).'/base32.php';