-
Notifications
You must be signed in to change notification settings - Fork 5
/
nonce.php
351 lines (310 loc) · 10.3 KB
/
nonce.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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
<?php
/**
* Create Nonce's (Number Used Once) in php. Can be used in a 'fake' nonce mode that
* doesn't require a database by setting $store to false. Not recommended though.
* When $store is set to true, it's possible to safely delete any nonce in the DB
* that are older than $expire. This can be done through a cron job and will help to
* keep the size of the database much smaller.
*
* A MYSQL database can be created using the following SQL:
*
* CREATE TABLE `nonce` (
* `nonce` varchar(128) NOT NULL DEFAULT '',
* `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
* PRIMARY KEY (`nonce`)
* ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
*
* Remeber, if you're using a hash type that is larger than 512bit then you'll need
* to increase the size of the varchar for nonce.
*
* Don't forget to set $secret or the class will throw an exception.
*
* @version 1.0
* @author Nick Verwymeren
* @license http://opensource.org/licenses/MIT MIT License
**/
class Nonce
{
/**
* How long in seconds the nonce will be good for. If you don't want the token to expire use -1.
*
* @var int
**/
protected $expire = 43200; // 12 Hours
/**
* A secret string that is hashed with a unique id and time. The longer
* and more complex this is the better.
*
* @var string
**/
private $secret = "";
/**
* The hashing type used to create the nonce.
*
* @var string
**/
protected $hash = 'sha256';
/**
* The amount of iternations done on a hash. This is done to enhance security. Larger
* numbers will be more secure but will increase the time needed to create the hash.
*
* @var int
**/
protected $iter = 100;
/**
* If true nonces will be stored in a database to ensure only one use.
*
* @var boolean
**/
protected $store = true;
/**
* The database username. Only used if $store is set to true.
*
* @var string
**/
private $db_user = "";
/**
* The database password. Only used if $store is set to true.
*
* @var string
**/
private $db_pass = "";
/**
* The database name. Only used if $store is set to true.
*
* @var string
**/
private $db_name = "my_site";
/**
* The database table name. Only used if $store is set to true.
*
* @var string
**/
private $db_table = "nonce";
/**
* The database host. Only used if $store is set to true.
*
* @var string
**/
private $db_host = "127.0.0.1";
/**
* Is a PDO database handler object. Only used if $store is set to true.
*
* @var object
**/
protected $dbh;
public function __construct()
{
if(!$this->secret) throw new Exception("You cannot leave \$secret blank. Please set it to a random string.");
if(strlen($this->secret) < 32) throw new Exception("Your secret key should be at least 32 characters");
$this->secret = hash('sha224', $this->secret);
if($this->store){
try{
$this->dbh = new PDO('mysql:host=' . $this->db_host . ';dbname=' . $this->db_name, $this->db_user, $this->db_pass);
$this->dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}catch (PDOException $e){
throw new Exception($e);
}
}
}
/**
* Checks the validity of a nonce. If valid (and $store is true) the nonce
* will become 'used' and invalid (meaning it cannot be used again).
*
* @param int $timestamp the time in the form of the unix epoch
* @param float $uid a unique id created by php's uniqid() function (although this can technically be anything).
* @param string $content optional additional content supplied by the user.
* @param float $uid a unique id created by php's uniqid() function (although this can technically be anything).
* @return Boolean true on success or will throw exception on error.
**/
public function validateAndUseNonce($timestamp, $uid, $content = '', $nonce)
{
$hash = $this->getNonce($timestamp, $uid, $content, strlen($nonce));
// Check to see if nonce has been used. Only checks if nonce's are being stored.
if($this->store && $this->nonceExists($nonce)){
throw new Exception("This form has already been submitted once.");
}
// Check to see if time has expired
if($this->expire > -1){
if(time() > $timestamp + $this->expire){
throw new Exception("This form has expired. Please reload the page and try submitting again.");
}
}
if($nonce == $hash){
if($this->store) $this->storeNonce($nonce);
return true;
} else {
throw new Exception("Invalid form request. Please try again.");
}
}
/**
* Creates a unique nonce string with an optional length. Max length is dependent upon hashing algorithm.
* @param int $timestamp the time in the form of the unix epoch
* @param float $uid a unique id.
* @param string $content optional additional content supplied by the user.
* @param int length optional the length of the returned nonce. Max Dependent upon hashing algorithm.
* @return string the nonce.
**/
public function getNonce($timestamp, $uid, $content = '', $length = NULL)
{
global $site;
$hash = hash($this->hash, $timestamp . $this->secret . $uid . $content);
$i = 0;
do{
$hash = hash($this->hash, $hash);
$i++;
} while ($i < $this->iter);
if($length){
$hash = substr($hash, 0, $length);
}
return $hash;
}
/**
* Store the nonce in the database.
* @param string $nonce
* @return boolean true on success false on failure
**/
private function storeNonce($nonce)
{
$sql = "INSERT INTO " . $this->db_table . " (nonce) VALUES (:nonce)";
$q = $this->dbh->prepare($sql);
return $q->execute(array(":nonce" => $nonce));
}
/**
* Checks the existence of a nonce in a database
* @param string $nonce
* @return mixed boolean false if does not exist, or int 1 if it does
**/
private function nonceExists($nonce)
{
if(!$this->store) throw new Exception("Cannot determine if this nonce has been used since \$store is set to false. Set \$store to true in order to track nonce usage.");
$sql = "SELECT COUNT(*) FROM " . $this->db_table . " WHERE nonce = :nonce LIMIT 1";
$q = $this->dbh->prepare($sql);
$q->execute(array(":nonce" => $nonce));
return $q->fetchColumn();
}
/**
* This may be called to validate a form that was generated using generateFormFields()
*
* @param string $content optional the additional content that was provided when
* generateFormFields() was called
*
* @return boolean true if valid
**/
public function validateForm($content = '')
{
$plain = $this->fnDecrypt($_POST['key']);
$plain = explode(' ', $plain, 2);
$time = $plain[0];
$uid = $plain[1];
if($content && $content !== $plain[2]){
}
return $this->validateAndUseNonce($time, $uid, $content, $_REQUEST['nonce']);
}
/**
* Generates 2 hidden fields to add nonce capability to a form. Forms using this method
* can be validated using validateForm().
*
* @param integer $length optional The length of the nonce
* @param string $content optional content that will be hashed into the nonce.
* This might be useful if you want to include a user id. Remeber anything added here
* must also be included as an argument when validateForm() is called.
*
* @return string
**/
public function generateFormFields($content = '', $length = NULL)
{
$time = time();
$uid = $this->generateUid();
$key = $time . " " . $uid;
// We'll need this info later so we don't want to simply hash it. We could just send it in plain
// text but this is a little more secure and makes things very difficult to break.
$key = $this->fnEncrypt($key);
echo "\r\n<input type='hidden' name='nonce' value='" . $this->getNonce($time, $uid, $content, $length) . "'>\r\n";
echo "<input type='hidden' name='key' value='$key'>\r\n";
}
/**
* Checks to see if a form was posted that contains fields generated by generateFormFields().
*
* @return boolean true if form was posted
**/
public function isFormPosted()
{
if(isset($_REQUEST['key']) && isset($_REQUEST['nonce'])) return true;
}
/**
* Creates a cryptographically secure random string. Tries first using urandom (for *nix systems),
* then tries openssl_random_pseudo_bytes and as a last resort mt_rand.
*
* @return string a random string
**/
public function generateUid($length = 32)
{
// Best option, but only on *nix systems. Also some web servers don't have access to this.
if(is_readable('/dev/urandom')){
$f = fopen('/dev/urandom', 'r');
$seed = fgets($f, $length); // note that this will always return full bytes
fclose($f);
return base64_encode($seed);
}
// Next best thing but requires openssl
if(extension_loaded('openssl')){
$seed = bin2hex(openssl_random_pseudo_bytes($length));
return base64_encode($seed);
}
// Last resort, mt_rand
for ($i = 0; $i < $length; $i++) {
$seed .= chr(mt_rand(0, 255));
}
return base64_encode($seed);
}
private function fnEncrypt($sValue)
{
return trim(
base64_encode(
mcrypt_encrypt(
MCRYPT_RIJNDAEL_256,
hash($this->hash, $this->secret, true), $sValue,
MCRYPT_MODE_ECB,
mcrypt_create_iv(
mcrypt_get_iv_size(
MCRYPT_RIJNDAEL_256,
MCRYPT_MODE_ECB
),
MCRYPT_RAND
)
)
)
);
}
private function fnDecrypt($sValue)
{
return trim(
mcrypt_decrypt(
MCRYPT_RIJNDAEL_256,
hash($this->hash, $this->secret, true),
base64_decode($sValue),
MCRYPT_MODE_ECB,
mcrypt_create_iv(
mcrypt_get_iv_size(
MCRYPT_RIJNDAEL_256,
MCRYPT_MODE_ECB
),
MCRYPT_RAND
)
)
);
}
/**
* Deletes any nonce's from the DB that are older than $expire. Nonce's older than $expire
* can be safely deleted since they cannot be used anymore.
*
* @return boolean true on success
**/
public function cleanUpDb()
{
$sql = "DELETE FROM " . $this->db_table . " WHERE timestamp < DATE_ADD(now(), INTERVAL -:expire second)";
$q = $this->dbh->prepare($sql);
return $q->execute(array(":expire" => $this->expire));
}
}