1 <?php
2 /**
3 * @package FrameworkOnFramework
4 * @subpackage encrypt
5 * @copyright Copyright (C) 2010-2016 Nicholas K. Dionysopoulos / Akeeba Ltd. All rights reserved.
6 * @license GNU General Public License version 2 or later; see LICENSE.txt
7 */
8 defined('FOF_INCLUDED') or die;
9
10 /**
11 * This class provides an RFC6238-compliant Time-based One Time Passwords,
12 * compatible with Google Authenticator (with PassCodeLength = 6 and TimePeriod = 30).
13 *
14 * @package FrameworkOnFramework
15 * @since 1.0
16 */
17 class FOFEncryptTotp
18 {
19 private $_passCodeLength = 6;
20
21 private $_pinModulo;
22
23 private $_secretLength = 10;
24
25 private $_timeStep = 30;
26
27 private $_base32 = null;
28
29 /**
30 * Initialises an RFC6238-compatible TOTP generator. Please note that this
31 * class does not implement the constraint in the last paragraph of §5.2
32 * of RFC6238. It's up to you to ensure that the same user/device does not
33 * retry validation within the same Time Step.
34 *
35 * @param int $timeStep The Time Step (in seconds). Use 30 to be compatible with Google Authenticator.
36 * @param int $passCodeLength The generated passcode length. Default: 6 digits.
37 * @param int $secretLength The length of the secret key. Default: 10 bytes (80 bits).
38 * @param Object $base32 The base32 en/decrypter
39 */
40 public function __construct($timeStep = 30, $passCodeLength = 6, $secretLength = 10, $base32=null)
41 {
42 $this->_timeStep = $timeStep;
43 $this->_passCodeLength = $passCodeLength;
44 $this->_secretLength = $secretLength;
45 $this->_pinModulo = pow(10, $this->_passCodeLength);
46
47 if (is_null($base32))
48 {
49 $this->_base32 = new FOFEncryptBase32;
50 }
51 else
52 {
53 $this->_base32 = $base32;
54 }
55 }
56
57 /**
58 * Get the time period based on the $time timestamp and the Time Step
59 * defined. If $time is skipped or set to null the current timestamp will
60 * be used.
61 *
62 * @param int|null $time Timestamp
63 *
64 * @return int The time period since the UNIX Epoch
65 */
66 public function getPeriod($time = null)
67 {
68 if (is_null($time))
69 {
70 $time = time();
71 }
72
73 $period = floor($time / $this->_timeStep);
74
75 return $period;
76 }
77
78 /**
79 * Check is the given passcode $code is a valid TOTP generated using secret
80 * key $secret
81 *
82 * @param string $secret The Base32-encoded secret key
83 * @param string $code The passcode to check
84 *
85 * @return boolean True if the code is valid
86 */
87 public function checkCode($secret, $code)
88 {
89 $time = $this->getPeriod();
90
91 for ($i = -1; $i <= 1; $i++)
92 {
93 if ($this->getCode($secret, ($time + $i) * $this->_timeStep) == $code)
94 {
95 return true;
96 }
97 }
98
99 return false;
100 }
101
102 /**
103 * Gets the TOTP passcode for a given secret key $secret and a given UNIX
104 * timestamp $time
105 *
106 * @param string $secret The Base32-encoded secret key
107 * @param int $time UNIX timestamp
108 *
109 * @return string
110 */
111 public function getCode($secret, $time = null)
112 {
113 $period = $this->getPeriod($time);
114 $secret = $this->_base32->decode($secret);
115
116 $time = pack("N", $period);
117 $time = str_pad($time, 8, chr(0), STR_PAD_LEFT);
118
119 $hash = hash_hmac('sha1', $time, $secret, true);
120 $offset = ord(substr($hash, -1));
121 $offset = $offset & 0xF;
122
123 $truncatedHash = $this->hashToInt($hash, $offset) & 0x7FFFFFFF;
124 $pinValue = str_pad($truncatedHash % $this->_pinModulo, $this->_passCodeLength, "0", STR_PAD_LEFT);
125
126 return $pinValue;
127 }
128
129 /**
130 * Extracts a part of a hash as an integer
131 *
132 * @param string $bytes The hash
133 * @param string $start The char to start from (0 = first char)
134 *
135 * @return string
136 */
137 protected function hashToInt($bytes, $start)
138 {
139 $input = substr($bytes, $start, strlen($bytes) - $start);
140 $val2 = unpack("N", substr($input, 0, 4));
141
142 return $val2[1];
143 }
144
145 /**
146 * Returns a QR code URL for easy setup of TOTP apps like Google Authenticator
147 *
148 * @param string $user User
149 * @param string $hostname Hostname
150 * @param string $secret Secret string
151 *
152 * @return string
153 */
154 public function getUrl($user, $hostname, $secret)
155 {
156 $url = sprintf("otpauth://totp/%s@%s?secret=%s", $user, $hostname, $secret);
157 $encoder = "https://chart.googleapis.com/chart?chs=200x200&chld=Q|2&cht=qr&chl=";
158 $encoderURL = $encoder . urlencode($url);
159
160 return $encoderURL;
161 }
162
163 /**
164 * Generates a (semi-)random Secret Key for TOTP generation
165 *
166 * @return string
167 */
168 public function generateSecret()
169 {
170 $secret = "";
171
172 for ($i = 1; $i <= $this->_secretLength; $i++)
173 {
174 $c = rand(0, 255);
175 $secret .= pack("c", $c);
176 }
177 $base32 = new FOFEncryptBase32;
178
179 return $this->_base32->encode($secret);
180 }
181 }
182