|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+<?php
|
|
|
2
|
+/*
|
|
|
3
|
+ TOTP v0.2.1 - a simple TOTP (RFC 6238) class using the SHA1 default
|
|
|
4
|
+ (c) 2014 Robin Leffmann <djinn at stolendata dot net>
|
|
|
5
|
+ https://github.com/stolendata/totp/
|
|
|
6
|
+ Licensed under CC BY-NC-SA 4.0 - http://creativecommons.org/licenses/by-nc-sa/4.0/
|
|
|
7
|
+*/
|
|
|
8
|
+class TOTP
|
|
|
9
|
+{
|
|
|
10
|
+ private static $base32Map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
|
11
|
+ private static function base32Decode( $in )
|
|
|
12
|
+ {
|
|
|
13
|
+ $out = "";
|
|
|
14
|
+ $l = strlen( $in );
|
|
|
15
|
+ $n = $bs = 0;
|
|
|
16
|
+ for( $i = 0; $i < $l; $i++ )
|
|
|
17
|
+ {
|
|
|
18
|
+ $n <<= 5;
|
|
|
19
|
+ $n += stripos( self::$base32Map, $in[$i] );
|
|
|
20
|
+ $bs = ( $bs + 5 ) % 8;
|
|
|
21
|
+ $out .= $bs < 5 ? chr( ($n & (255 << $bs)) >> $bs ) : null;
|
|
|
22
|
+ }
|
|
|
23
|
+ return $out;
|
|
|
24
|
+ }
|
|
|
25
|
+ public static function getOTP( $secret, $digits = 6, $period = 30, $offset = 0 )
|
|
|
26
|
+ {
|
|
|
27
|
+ if( strlen($secret) < 16 || strlen($secret) % 8 != 0 )
|
|
|
28
|
+ return [ 'err'=>'length of secret must be a multiple of 8, and at least 16 characters' ];
|
|
|
29
|
+ if( preg_match('/[^a-z2-7]/i', $secret) === 1 )
|
|
|
30
|
+ return [ 'err'=>'secret contains non-base32 characters' ];
|
|
|
31
|
+ $digits = intval( $digits );
|
|
|
32
|
+ if( $digits < 6 || $digits > 8 )
|
|
|
33
|
+ return [ 'err'=>'digits must be 6, 7 or 8' ];
|
|
|
34
|
+ $seed = self::base32Decode( $secret );
|
|
|
35
|
+ $time = str_pad( pack('N', intval($offset + time() / $period)), 8, "\x00", STR_PAD_LEFT );
|
|
|
36
|
+ $hash = hash_hmac( 'sha1', $time, $seed, false );
|
|
|
37
|
+ $otp = ( hexdec(substr($hash, hexdec($hash[39]) * 2, 8)) & 0x7fffffff ) % pow( 10, $digits );
|
|
|
38
|
+ return [ 'otp'=>sprintf("%'0{$digits}u", $otp) ];
|
|
|
39
|
+ }
|
|
|
40
|
+ public static function genSecret( $length = 24 )
|
|
|
41
|
+ {
|
|
|
42
|
+ if( $length < 16 || $length % 8 !== 0 )
|
|
|
43
|
+ return [ 'err'=>'length must be a multiple of 8, and at least 16' ];
|
|
|
44
|
+ $secret = "";
|
|
|
45
|
+ while( $length-- )
|
|
|
46
|
+ {
|
|
|
47
|
+ $c = @gettimeofday()['usec'] % 53;
|
|
|
48
|
+ while( $c-- )
|
|
|
49
|
+ mt_rand();
|
|
|
50
|
+ $secret .= self::$base32Map[mt_rand(0, 31)];
|
|
|
51
|
+ }
|
|
|
52
|
+ return [ 'secret'=>$secret ];
|
|
|
53
|
+ }
|
|
|
54
|
+ public static function genURI( $account, $secret, $digits = null, $period = null, $issuer = null )
|
|
|
55
|
+ {
|
|
|
56
|
+ if( empty($account) || empty($secret) )
|
|
|
57
|
+ return [ 'err'=>'you must provide at least an account and a secret' ];
|
|
|
58
|
+ if( mb_strpos($account . $issuer, ':') !== false )
|
|
|
59
|
+ return [ 'err'=>'neither account nor issuer can contain a colon (:) character' ];
|
|
|
60
|
+ $account = rawurlencode( $account );
|
|
|
61
|
+ $issuer = rawurlencode( $issuer );
|
|
|
62
|
+ $label = empty( $issuer ) ? $account : "$issuer:$account";
|
|
|
63
|
+ return [ 'uri'=>'otpauth://totp/' . $label . "?secret=$secret" .
|
|
|
64
|
+ (is_null($digits) ? '' : "&digits=$digits") .
|
|
|
65
|
+ (is_null($period) ? '' : "&period=$period") .
|
|
|
66
|
+ (empty($issuer) ? '' : "&issuer=$issuer") ];
|
|
|
67
|
+ }
|
|
|
68
|
+}
|
|
|
69
|
+?>
|