Skip to content

Commit

Permalink
Merge commit '137d802c18fc11cb5dd4dbe6806e52ddd8a1e5f6' as 'GAuthenti…
Browse files Browse the repository at this point in the history
…cator'
  • Loading branch information
jzwalk committed Nov 25, 2018
2 parents f44443d + 137d802 commit 43468ea
Show file tree
Hide file tree
Showing 5 changed files with 516 additions and 0 deletions.
45 changes: 45 additions & 0 deletions GAuthenticator/Action.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php
/**
* GAuthenticator Plugin
*
* @copyright Copyright (c) 2018 WeiCN (https://cuojue.org)
* @license GNU General Public License 2.0
*
*/
class GAuthenticator_Action extends Typecho_Widget implements Widget_Interface_Do
{

public function __construct($request, $response, $params = NULL)
{
parent::__construct($request, $response, $params);
}
/**
* 验证GAuthenticator POST
*
*/
public function auth(){
if(intval($this->request->get('otp'))>0){
//获取到CODE
if (isset($_SESSION['GAuthenticator'])&&$_SESSION['GAuthenticator']) return;//如果SESSION匹配则直接返回
$config = Helper::options()->plugin('GAuthenticator');
require_once 'GoogleAuthenticator.php';
$referer = $this->request->getReferer();
$Authenticator = new PHPGangsta_GoogleAuthenticator();//初始化生成类
$oneCode = intval($this->request->get('otp'));//手机端生成的一次性代码
if($Authenticator->verifyCode($config->SecretKey, $oneCode, $config->SecretTime)){//验证一次性代码
$expire = 1 == $this->request->get('remember') ? Helper::options()->time + Helper::options()->timezone + 30*24*3600 : 0;
$_SESSION['GAuthenticator'] = true;//session保存
Typecho_Cookie::set('__typecho_GAuthenticator',md5($config->SecretKey.Typecho_Cookie::getPrefix().Typecho_Widget::widget('Widget_User')->uid),$expire);//cookie保存
}else{
Typecho_Widget::widget('Widget_Notice')->set(_t('两步验证失败'), 'error');
}
Typecho_Response::redirect($referer);
}
}

public function action(){
$this->widget('Widget_User')->pass('administrator');
$this->on($this->request->is('otp'))->auth();
}
}
?>
208 changes: 208 additions & 0 deletions GAuthenticator/GoogleAuthenticator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
<?php

/**
* PHP Class for handling Google Authenticator 2-factor authentication
*
* @author Michael Kliewe
* @copyright 2012 Michael Kliewe
* @license http://www.opensource.org/licenses/bsd-license.php BSD License
* @link http://www.phpgangsta.de/
*/

class PHPGangsta_GoogleAuthenticator
{
protected $_codeLength = 6;

/**
* Create new secret.
* 16 characters, randomly chosen from the allowed base32 characters.
*
* @param int $secretLength
* @return string
*/
public function createSecret($secretLength = 16)
{
$validChars = $this->_getBase32LookupTable();
unset($validChars[32]);

$secret = '';
for ($i = 0; $i < $secretLength; $i++) {
$secret .= $validChars[array_rand($validChars)];
}
return $secret;
}

/**
* Calculate the code, with given secret and point in time
*
* @param string $secret
* @param int|null $timeSlice
* @return string
*/
public function getCode($secret, $timeSlice = null)
{
if ($timeSlice === null) {
$timeSlice = floor(time() / 30);
}

$secretkey = $this->_base32Decode($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, $this->_codeLength);
return str_pad($value % $modulo, $this->_codeLength, '0', STR_PAD_LEFT);
}

/**
* Get QR-Code URL for image, from google charts
*
* @param string $name
* @param string $secret
* @param string $title
* @return string
*/
public function getQRCodeGoogleUrl($name, $secret, $title = null) {
$urlencoded = urlencode('otpauth://totp/'.$name.'?secret='.$secret.'');
if(isset($title)) {
$urlencoded .= urlencode('&issuer='.urlencode($title));
}
return 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl='.$urlencoded.'';
}

/**
* Check if the code is correct. This will accept codes starting from $discrepancy*30sec ago to $discrepancy*30sec from now
*
* @param string $secret
* @param string $code
* @param int $discrepancy This is the allowed time drift in 30 second units (8 means 4 minutes before or after)
* @param int|null $currentTimeSlice time slice if we want use other that time()
* @return bool
*/
public function verifyCode($secret, $code, $discrepancy = 1, $currentTimeSlice = null)
{
if ($currentTimeSlice === null) {
$currentTimeSlice = floor(time() / 30);
}

for ($i = -$discrepancy; $i <= $discrepancy; $i++) {
$calculatedCode = $this->getCode($secret, $currentTimeSlice + $i);
if ($calculatedCode == $code ) {
return true;
}
}

return false;
}

/**
* Set the code length, should be >=6
*
* @param int $length
* @return PHPGangsta_GoogleAuthenticator
*/
public function setCodeLength($length)
{
$this->_codeLength = $length;
return $this;
}

/**
* Helper class to decode base32
*
* @param $secret
* @return bool|string
*/
protected function _base32Decode($secret)
{
if (empty($secret)) return '';

$base32chars = $this->_getBase32LookupTable();
$base32charsFlipped = array_flip($base32chars);

$paddingCharCount = substr_count($secret, $base32chars[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($secret, -($allowedValues[$i])) != str_repeat($base32chars[32], $allowedValues[$i])) return false;
}
$secret = str_replace('=','', $secret);
$secret = str_split($secret);
$binaryString = "";
for ($i = 0; $i < count($secret); $i = $i+8) {
$x = "";
if (!in_array($secret[$i], $base32chars)) return false;
for ($j = 0; $j < 8; $j++) {
$x .= str_pad(base_convert(@$base32charsFlipped[@$secret[$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;
}

/**
* Helper class to encode base32
*
* @param string $secret
* @param bool $padding
* @return string
*/
protected function _base32Encode($secret, $padding = true)
{
if (empty($secret)) return '';

$base32chars = $this->_getBase32LookupTable();

$secret = str_split($secret);
$binaryString = "";
for ($i = 0; $i < count($secret); $i++) {
$binaryString .= str_pad(base_convert(ord($secret[$i]), 10, 2), 8, '0', STR_PAD_LEFT);
}
$fiveBitBinaryArray = str_split($binaryString, 5);
$base32 = "";
$i = 0;
while ($i < count($fiveBitBinaryArray)) {
$base32 .= $base32chars[base_convert(str_pad($fiveBitBinaryArray[$i], 5, '0'), 2, 10)];
$i++;
}
if ($padding && ($x = strlen($binaryString) % 40) != 0) {
if ($x == 8) $base32 .= str_repeat($base32chars[32], 6);
elseif ($x == 16) $base32 .= str_repeat($base32chars[32], 4);
elseif ($x == 24) $base32 .= str_repeat($base32chars[32], 3);
elseif ($x == 32) $base32 .= $base32chars[32];
}
return $base32;
}

/**
* Get array with all 32 characters for decoding from/encoding to base32
*
* @return array
*/
protected function _getBase32LookupTable()
{
return 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
);
}
}
136 changes: 136 additions & 0 deletions GAuthenticator/Plugin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<?php
if (!defined('__TYPECHO_ROOT_DIR__')) exit;
/**
* Google Authenticator for Typecho
*
* @package GAuthenticator
* @author WeiCN
* @version 0.0.4
* @link https://cuojue.org/read/Typecho_Google_Authenticator_02.html
*/
class GAuthenticator_Plugin implements Typecho_Plugin_Interface
{
private static $pluginName = 'GAuthenticator';
/**
* 激活插件方法,如果激活失败,直接抛出异常
*
* @access public
* @return void
* @throws Typecho_Plugin_Exception
*/
public static function activate()
{
Helper::addRoute('GAuthenticator', '/GAuthenticator', 'GAuthenticator_Action', 'Action');
Typecho_Plugin::factory('admin/menu.php')->navBar = array(__CLASS__, 'Authenticator_safe');
Typecho_Plugin::factory('admin/common.php')->begin = array(__CLASS__, 'Authenticator_verification');
return _t('当前两步验证还未启用,请进行<a href="options-plugin.php?config=' . self::$pluginName . '">初始化设置</a>');
}

/**
* 禁用插件方法,如果禁用失败,直接抛出异常
*
* @static
* @access public
* @return void
* @throws Typecho_Plugin_Exception
*/
public static function deactivate(){
Helper::removeRoute('GAuthenticator');
}

/**
* 获取插件配置面板
*
* @access public
* @param Typecho_Widget_Helper_Form $form 配置面板
* @return void'
*/
public static function config(Typecho_Widget_Helper_Form $form)
{
$qrurl = 'http://qr.liantu.com/api.php?text='.urlencode('otpauth://totp/'.urlencode(Helper::options()->title.':'.Typecho_Widget::widget('Widget_User')->mail).'?secret='.Helper::options()->plugin(self::$pluginName)->SecretKey);//生成安全密钥的二维码网址
$element = new Typecho_Widget_Helper_Form_Element_Text('SecretKey', NULL, '', _t('SecretKey'), '安装的时候自动计算密钥,手动修改无效,如需要修改请卸载重新安装或者手动修改数据库<br><span style="font-weight: bold; color: #000; text-align: center; display: block;padding: 30px 0 30px 0;font-size: 24px;">请扫描下面的二维码绑定<br><img style="padding-top: 20px;" src="'.$qrurl.'"></span>');
$form->addInput($element);
$element = new Typecho_Widget_Helper_Form_Element_Text('SecretQRurl', NULL, '', _t('二维码的网址'), '本选项已过时,保留只是为了向下兼容。和上面图片的地址是相同的');
$form->addInput($element);
$element = new Typecho_Widget_Helper_Form_Element_Text('SecretTime', NULL, 2, _t('容差时间'), '允许的容差时间,单位为30秒的倍数,如果这里是2 那么就是 2* 30 sec 一分钟.');
$form->addInput($element);
$element = new Typecho_Widget_Helper_Form_Element_Text('SecretCode', NULL, '', _t('客户端代码'), '输入你APP或者其他什么鬼上面显示的六位数字。<br>用兼容的APP上面的扫描二维码或者手动输入第一行的SecretKey即可生成');
$form->addInput($element);
$element = new Typecho_Widget_Helper_Form_Element_Radio('SecretOn', array('1' => '开启','0' => '关闭'), 0, _t('插件开关'), '这里关掉了,就不需要验证即可登录。');
$form->addInput($element);
}
/**
* 手动保存配置面板
* @param $config array 插件配置
* @param $is_init bool 是否初始化
*/
public static function configHandle($config, $is_init)
{
if ($is_init) {//如果是第一次初始化插件
require_once 'GoogleAuthenticator.php';
$Authenticator = new PHPGangsta_GoogleAuthenticator();//初始化生成类
$config['SecretKey'] = $Authenticator->createSecret();//生成一个随机安全密钥
$config['SecretQRurl'] = 'http://qr.liantu.com/api.php?text='.urlencode('otpauth://totp/'.urlencode(Helper::options()->title.':'.Typecho_Widget::widget('Widget_User')->mail).'?secret='.$config['SecretKey']);//生成安全密钥的二维码网址
}else{
$config_old = Helper::options()->plugin(self::$pluginName);
if(($config['SecretCode']!='' && $config['SecretOn']==1) || $config['SecretOn']==1){//如果启用,并且验证码不为空
require_once 'GoogleAuthenticator.php';
$Authenticator = new PHPGangsta_GoogleAuthenticator();
if($Authenticator->verifyCode($config['SecretKey'], $config['SecretCode'], $config['SecretTime'])){
$config['SecretOn'] = 1;//如果匹配,则启用
}else{
throw new Typecho_Plugin_Exception('两步验证代码校验失败,请重试或选择关闭');
}
}
$config['SecretKey'] = $config_old->SecretKey;//保持初始化SecretKey不被修改
$config['SecretQRurl'] = $config_old->SecretQRurl;//保持初始化SecretQRurl不被修改 过时选项 兼容保留
}
$config['SecretCode'] = '';//每次保存不保存验证码
Helper::configPlugin(self::$pluginName, $config);//保存插件配置
}
/**
* 个人用户的配置面板
*
* @access public
* @param Typecho_Widget_Helper_Form $form
* @return void
*/
public static function personalConfig(Typecho_Widget_Helper_Form $form){}

/**
* 插件实现方法
*
* @access public
* @return void
*/
public static function Authenticator_safe()
{
$config = Helper::options()->plugin(self::$pluginName);
if($config->SecretOn==1){
echo '<span class="message success">'.htmlspecialchars('已启用 Authenticator 验证').'</span>';
}else{
echo '<span class="message error">'.htmlspecialchars('未启用 Authenticator 验证').'</span>';
}
}

public static function Authenticator_verification()
{
if(isset($Authenticator_init))return;
$Authenticator_init = true;
if (!Typecho_Widget::widget('Widget_User')->hasLogin()){
return;//如果没登录则直接返回
}else{
//已经登录就验证
$config = Helper::options()->plugin(self::$pluginName);
if (isset($_SESSION['GAuthenticator'])&&$_SESSION['GAuthenticator']) return;//如果SESSION匹配则直接返回
if (Typecho_Cookie::get('__typecho_GAuthenticator') == md5($config->SecretKey.Typecho_Cookie::getPrefix().Typecho_Widget::widget('Widget_User')->uid)) return;//如果COOKIE匹配则直接返回
if($config->SecretOn==1){
$options = Helper::options();
$request = new Typecho_Request();
require_once 'verification.php';
}else{
return;//如果未开启插件则直接返回
}
}
}
}
Loading

0 comments on commit 43468ea

Please sign in to comment.