/
totp.php
156 lines (149 loc) · 4.58 KB
/
totp.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
<?php
class TOTP{
//Charset as defined by RFC3548
private $charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=";
//Window for clock differences (x30 seconds) , +/- $skewx30sec
private $skew = 2;
/**
*base32encodestr
*
*converts ascii string into base32
*@param string $str The ascii input string
*@return string a base32 version of the input string
*/
public function base32encodestr($str){
if(empty($str))return "";
$len = strlen($str);
$nquanta = ceil($len/5);//1 character is 8 bits, and a quantum is 40 bits
$leftover = $len % 5;
/*
If last quantum has less than 40 bits (5 chars),
add bits with 0 value to the right.
*/
if($leftover > 0){
for($i=0; $i<5-$leftover;$i++){
$str .= "\0";
}
}
$_8bitchars = "";
$encoded = "";
$quanta = str_split($str, 5);
foreach($quanta as $quantum){
for($i=0; $i<strlen($quantum); $i++){
$_8bitchars .= str_pad(decbin(ord($quantum[$i])), 8, "0", STR_PAD_LEFT);
}
}
foreach(str_split($_8bitchars, 5) as $_5bitchar)
{
$encoded .= $this->charset[bindec($_5bitchar)];
}
if($leftover == 1){
$encoded = substr_replace($encoded, "======", strlen($encoded)-6);
}elseif($leftover == 2){
$encoded = substr_replace($encoded, "====", strlen($encoded)-4);
}
elseif($leftover == 3){
$encoded = substr_replace($encoded, "===", strlen($encoded)-3);
}
elseif($leftover == 4){
$encoded = substr_replace($encoded, "=", strlen($encoded)-1);
}
return $encoded;
}
/*
Reverse base32encode
*/
public function strdecodebase32($str){
if(strlen($str) % 8 > 0){
throw new Exception("Str length must be a multiple of 8");
}
if(empty($str))return "";
$len = strpos($str, "=");
if($len !== FALSE){
$str = substr($str, 0, $len);
}
$quanta = str_split($str, 8);
$decoded = "";
foreach($quanta as $quantum){
$_8bitchars = "";
for($i=0; $i<strlen($quantum); $i++){
$code = strpos($this->charset, $quantum[$i]);
if($code === FALSE){
throw new Exception("Invalid charset.");
}
$_8bitchars .= str_pad(decbin($code), 5, "0", STR_PAD_LEFT);
}
foreach(str_split($_8bitchars,8) as $char){
$decoded .= chr(bindec($char));
}
}
return $decoded;
}
private function int_to_bytestring($num, $padding=8){
/*
Turns an integer into the OATH specified
bytestring
*/
$ret = array();
$idx=0;
while($num != 0){
$ret[$idx++] = chr($num & 0xff);
$num = $num >> 8;
}
while($idx < $padding){
$ret[$idx++] = chr(0x00);
}
return implode("", array_reverse($ret));
}
public function generate_truncated_otp($secret, $counter, $length=6){
//Timecode derived from counter
$key = $this->int_to_bytestring($counter);
//Compute a 20-byte HMAC from the secret and timecode:
$LEN = 20;
$sharedKey = $this->strdecodebase32($secret);
$hash = hash_hmac("sha1", $key, $sharedKey);
/*
Convert hex-encoded string to dec-encoded byte array
*/
foreach(str_split($hash,2) as $hex)
{
$hmac[]=hexdec($hex);
}
//Put selected bytes into result int to produce the OTP
//Algorithm from RFC 6238
$offs = $hmac[$LEN-1] & 0xf;
$ret = (($hmac[$offs] & 0x7f) << 24) |
(($hmac[$offs+1] & 0xff) << 16) |
(($hmac[$offs+2] & 0xff) << 8) |
($hmac[$offs+3] & 0xff);
$ret %= pow(10, $length);
return str_pad($ret, $length, "0", STR_PAD_LEFT);
}
private function cur_timecode($interval=30){
$t = time();
return intval($t/$interval);
}
public function get_current_otps($secret){
$ret = array();
$start = $this->cur_timecode();
for($i=-($this->skew); $i<=$this->skew; $i++)
{
$checktime = (int)($start+$i);
$ret[] = $this->generate_truncated_otp($secret, $checktime);
}
return $ret;
}
public function validate_code($secret, $code){
$start = $this->cur_timecode();
for($i=-($this->skew); $i<=$this->skew; $i++)
{
$checktime = (int)($start+$i);
$tok = intval($this->generate_truncated_otp($secret, $checktime));
if (intval($code) == $tok)
{
return true;
}
}
return false;
}
}