├── .gitignore
├── Action.php
├── GoogleAuthenticator.php
├── Plugin.php
├── README.md
├── jquery.qrcode.min.js
└── verification.php
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | .idea/
3 | .DS_Store
4 |
--------------------------------------------------------------------------------
/Action.php:
--------------------------------------------------------------------------------
1 | request->get('otp'))>0){
22 | //获取到CODE
23 | if (isset($_SESSION['GAuthenticator'])&&$_SESSION['GAuthenticator']) return;//如果SESSION匹配则直接返回
24 | $config = Helper::options()->plugin('GAuthenticator');
25 | require_once 'GoogleAuthenticator.php';
26 | $referer = $this->request->getReferer();
27 | $Authenticator = new PHPGangsta_GoogleAuthenticator();//初始化生成类
28 | $oneCode = intval($this->request->get('otp'));//手机端生成的一次性代码
29 | if($Authenticator->verifyCode($config->SecretKey, $oneCode, $config->SecretTime)){//验证一次性代码
30 | $expire = 1 == $this->request->get('remember') ? Helper::options()->time + Helper::options()->timezone + 30*24*3600 : 0;
31 | $_SESSION['GAuthenticator'] = true;//session保存
32 | Typecho_Cookie::set('__typecho_GAuthenticator',md5($config->SecretKey.Typecho_Cookie::getPrefix().Typecho_Widget::widget('Widget_User')->uid),$expire);//cookie保存
33 | }else{
34 | Typecho_Widget::widget('Widget_Notice')->set(_t('两步验证失败'), 'error');
35 | }
36 | $response = Typecho_Response::getInstance();
37 | $this->response->redirect($referer);
38 | }
39 | }
40 |
41 | public function action(){
42 | $this->widget('Widget_User')->pass('administrator');
43 | $this->on($this->request->is('otp'))->auth();
44 | }
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/GoogleAuthenticator.php:
--------------------------------------------------------------------------------
1 | _getBase32LookupTable();
26 | unset($validChars[32]);
27 |
28 | $secret = '';
29 | for ($i = 0; $i < $secretLength; $i++) {
30 | $secret .= $validChars[array_rand($validChars)];
31 | }
32 | return $secret;
33 | }
34 |
35 | /**
36 | * Calculate the code, with given secret and point in time
37 | *
38 | * @param string $secret
39 | * @param int|null $timeSlice
40 | * @return string
41 | */
42 | public function getCode($secret, $timeSlice = null)
43 | {
44 | if ($timeSlice === null) {
45 | $timeSlice = floor(time() / 30);
46 | }
47 |
48 | $secretkey = $this->_base32Decode($secret);
49 |
50 | // Pack time into binary string
51 | $time = chr(0).chr(0).chr(0).chr(0).pack('N*', $timeSlice);
52 | // Hash it with users secret key
53 | $hm = hash_hmac('SHA1', $time, $secretkey, true);
54 | // Use last nipple of result as index/offset
55 | $offset = ord(substr($hm, -1)) & 0x0F;
56 | // grab 4 bytes of the result
57 | $hashpart = substr($hm, $offset, 4);
58 |
59 | // Unpak binary value
60 | $value = unpack('N', $hashpart);
61 | $value = $value[1];
62 | // Only 32 bits
63 | $value = $value & 0x7FFFFFFF;
64 |
65 | $modulo = pow(10, $this->_codeLength);
66 | return str_pad($value % $modulo, $this->_codeLength, '0', STR_PAD_LEFT);
67 | }
68 |
69 | /**
70 | * Get QR-Code URL for image, from google charts
71 | *
72 | * @param string $name
73 | * @param string $secret
74 | * @param string $title
75 | * @return string
76 | */
77 | public function getQRCodeGoogleUrl($name, $secret, $title = null) {
78 | $urlencoded = urlencode('otpauth://totp/'.$name.'?secret='.$secret.'');
79 | if(isset($title)) {
80 | $urlencoded .= urlencode('&issuer='.urlencode($title));
81 | }
82 | return 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl='.$urlencoded.'';
83 | }
84 |
85 | /**
86 | * Check if the code is correct. This will accept codes starting from $discrepancy*30sec ago to $discrepancy*30sec from now
87 | *
88 | * @param string $secret
89 | * @param string $code
90 | * @param int $discrepancy This is the allowed time drift in 30 second units (8 means 4 minutes before or after)
91 | * @param int|null $currentTimeSlice time slice if we want use other that time()
92 | * @return bool
93 | */
94 | public function verifyCode($secret, $code, $discrepancy = 1, $currentTimeSlice = null)
95 | {
96 | if ($currentTimeSlice === null) {
97 | $currentTimeSlice = floor(time() / 30);
98 | }
99 |
100 | for ($i = -$discrepancy; $i <= $discrepancy; $i++) {
101 | $calculatedCode = $this->getCode($secret, $currentTimeSlice + $i);
102 | if ($calculatedCode == $code ) {
103 | return true;
104 | }
105 | }
106 |
107 | return false;
108 | }
109 |
110 | /**
111 | * Set the code length, should be >=6
112 | *
113 | * @param int $length
114 | * @return PHPGangsta_GoogleAuthenticator
115 | */
116 | public function setCodeLength($length)
117 | {
118 | $this->_codeLength = $length;
119 | return $this;
120 | }
121 |
122 | /**
123 | * Helper class to decode base32
124 | *
125 | * @param $secret
126 | * @return bool|string
127 | */
128 | protected function _base32Decode($secret)
129 | {
130 | if (empty($secret)) return '';
131 |
132 | $base32chars = $this->_getBase32LookupTable();
133 | $base32charsFlipped = array_flip($base32chars);
134 |
135 | $paddingCharCount = substr_count($secret, $base32chars[32]);
136 | $allowedValues = array(6, 4, 3, 1, 0);
137 | if (!in_array($paddingCharCount, $allowedValues)) return false;
138 | for ($i = 0; $i < 4; $i++){
139 | if ($paddingCharCount == $allowedValues[$i] &&
140 | substr($secret, -($allowedValues[$i])) != str_repeat($base32chars[32], $allowedValues[$i])) return false;
141 | }
142 | $secret = str_replace('=','', $secret);
143 | $secret = str_split($secret);
144 | $binaryString = "";
145 | for ($i = 0; $i < count($secret); $i = $i+8) {
146 | $x = "";
147 | if (!in_array($secret[$i], $base32chars)) return false;
148 | for ($j = 0; $j < 8; $j++) {
149 | $x .= str_pad(base_convert(@$base32charsFlipped[@$secret[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT);
150 | }
151 | $eightBits = str_split($x, 8);
152 | for ($z = 0; $z < count($eightBits); $z++) {
153 | $binaryString .= ( ($y = chr(base_convert($eightBits[$z], 2, 10))) || ord($y) == 48 ) ? $y:"";
154 | }
155 | }
156 | return $binaryString;
157 | }
158 |
159 | /**
160 | * Helper class to encode base32
161 | *
162 | * @param string $secret
163 | * @param bool $padding
164 | * @return string
165 | */
166 | protected function _base32Encode($secret, $padding = true)
167 | {
168 | if (empty($secret)) return '';
169 |
170 | $base32chars = $this->_getBase32LookupTable();
171 |
172 | $secret = str_split($secret);
173 | $binaryString = "";
174 | for ($i = 0; $i < count($secret); $i++) {
175 | $binaryString .= str_pad(base_convert(ord($secret[$i]), 10, 2), 8, '0', STR_PAD_LEFT);
176 | }
177 | $fiveBitBinaryArray = str_split($binaryString, 5);
178 | $base32 = "";
179 | $i = 0;
180 | while ($i < count($fiveBitBinaryArray)) {
181 | $base32 .= $base32chars[base_convert(str_pad($fiveBitBinaryArray[$i], 5, '0'), 2, 10)];
182 | $i++;
183 | }
184 | if ($padding && ($x = strlen($binaryString) % 40) != 0) {
185 | if ($x == 8) $base32 .= str_repeat($base32chars[32], 6);
186 | elseif ($x == 16) $base32 .= str_repeat($base32chars[32], 4);
187 | elseif ($x == 24) $base32 .= str_repeat($base32chars[32], 3);
188 | elseif ($x == 32) $base32 .= $base32chars[32];
189 | }
190 | return $base32;
191 | }
192 |
193 | /**
194 | * Get array with all 32 characters for decoding from/encoding to base32
195 | *
196 | * @return array
197 | */
198 | protected function _getBase32LookupTable()
199 | {
200 | return array(
201 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 7
202 | 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15
203 | 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23
204 | 'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31
205 | '=' // padding char
206 | );
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/Plugin.php:
--------------------------------------------------------------------------------
1 | navBar = array(__CLASS__, 'Authenticator_safe');
25 | Typecho_Plugin::factory('admin/common.php')->begin = array(__CLASS__, 'Authenticator_verification');
26 | return _t('当前两步验证还未启用,请进行初始化设置');
27 | }
28 |
29 | /**
30 | * 禁用插件方法,如果禁用失败,直接抛出异常
31 | *
32 | * @static
33 | * @access public
34 | * @return void
35 | * @throws Typecho_Plugin_Exception
36 | */
37 | public static function deactivate(){
38 | Helper::removeRoute('GAuthenticator');
39 | }
40 |
41 | /**
42 | * 获取插件配置面板
43 | *
44 | * @access public
45 | * @param Typecho_Widget_Helper_Form $form 配置面板
46 | * @return void'
47 | */
48 | public static function config(Typecho_Widget_Helper_Form $form)
49 | {
50 | $qrurl = 'otpauth://totp/'.urlencode(Helper::options()->title.':'.Typecho_Widget::widget('Widget_User')->mail).'?secret=';
51 | $element = new Typecho_Widget_Helper_Form_Element_Text('SecretKey', NULL, '', _t('SecretKey'), '
52 | 安装的时候自动计算密钥,手动修改无效,如需要修改请卸载重新安装或者手动修改数据库
53 |
32 | 33 | allowRegister): ?> 34 | • 35 | 36 | 37 | • 38 | 39 |
40 |