forked from cchan/doeqs_new
/
accountManagement.php
282 lines (232 loc) · 8.83 KB
/
accountManagement.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
<?php
if(!defined('ROOT_PATH')){header('HTTP/1.0 404 Not Found');die();}
/*
accountManagement.php
Implements decent account-management scheme.
Also does session stuff.
Dependencies
NEEDED AT INCLUDETIME:
$SESSION_TIMEOUT_MINUTES
NEEDED AT CALLTIME:
posted()
sessioned()
err()?
...
Special Note:
userAccess
//In order of precedence:
//a=admin
//c=captain
//u=regular user
//x=not logged in
*/
//session_name('doeqs');
session_start();
setcookie(session_name(),session_id(),time()+$SESSION_TIMEOUT_MINUTES*60);
function session_total_reset(){//Destroys a session according to the php.net method, plus some modifications.
// Unset all of the session variables.
$_SESSION = array();
unset($_SESSION);
// If it's desired to kill the session, also delete the session cookie.
// Note: This will destroy the session, and not just the session data!
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$params['path'], $params['domain'],
$params['secure'], $params['httponly']
);
}
// Finally, destroy the session.
session_destroy();
session_start();
session_regenerate_id(true);//Regenerates the session ID so that it's hard to attack.
}
//15min session timeout
if (sessioned('LAST_ACTIVITY')&&time()-$_SESSION['LAST_ACTIVITY']>$SESSION_TIMEOUT_MINUTES*60)
session_total_reset();
$_SESSION['LAST_ACTIVITY'] = time();
// lock session to IP address
if (!isSet($_SESSION['IP_ADDR']))
$_SESSION['IP_ADDR'] = $_SERVER['REMOTE_ADDR'];
if ($_SESSION['IP_ADDR'] != $_SERVER['REMOTE_ADDR'])
session_total_reset();
/*
limit_attempts
Returns true if there have been more than $attempts attempts in $seconds seconds to do $process.
*/
function limit_attempts($process,$attempts,$seconds){
$sp='attempts_'.$process;
if(!sessioned($sp)||!is_array($_SESSION[$sp]))$_SESSION[$sp]=array();
$_SESSION[$sp][]=time();//Rudimentary, since they could just reset the session key.
foreach($_SESSION[$sp] as $t)
if(time()-$t>$seconds)
unset($t);
if(count($_SESSION[$sp])>$attempts)return true;
return false;
}
function reset_attempts($process){
$_SESSION['attempts_'.$process]=array();
}
function genRandStr($length=NULL){
if(!$length)$length=mt_rand(64,96);
$c = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';$cl = strlen($c);
$s = '';
for($i=0;$i<$length;$i++)$s.=$c[mt_rand(0,$cl-1)];
return $s;
}
function csrfVerify(){//Checks CSRF code validity, and returns whether to proceed. The return value is static. Erases 'ver'.
static $valid=NULL;
if(is_null($valid)){
if(hashEquals(POST('ver'),SESSION('ver')))$valid=true;
else $valid=false;//alert("Submission security check failed.",-1);}//On every page :(
unset($_POST['ver'],$_SESSION['ver']);
}
return $valid;
//--todo-- Exceptions are bad and messy and not being caught. They're not meant to propagate all the way up.
}
function csrfCode(/*$forceNew*/ /*$ver_name*/){//Returns randomly generated CSRF code. The return value is static.
static $code='';
if(sessioned('ver')&&$code===$_SESSION['ver'])return $code;
return ($code=$_SESSION['ver']=genRandStr());
}
function generateForm($form,$inputs){
$csrf=csrfCode();
$a='';
foreach($form as $name=>$value)
$a.=' '.$name.'="'.$value.'" ';
$form="<form $a><input type='hidden' name='ver' value='$csrf'/><table>";
foreach($inputs as $input){
if($input=='')
$form.='<tr><td colspan="2"> </td></tr>';
elseif(is_string($input))
$form.='<tr><td colspan="2">'.$input.'</td></tr>';
else{
$elem='<input ';
foreach($input as $name=>$value)
if($name!='prompt')
$elem.=" {$name}=\"{$value}\" ";
$elem.=' />';
if(array_key_exists('prompt',$input))$form.="<tr><td>{$input['prompt']}<td>$elem</td></tr>";
else $form.="<tr><td colspan='2'>$elem</td></tr>";
}
}
$form.='</table></form>';
return $form;
}
function authError(){
logout();
die('Authentication error.');
}
function hashEquals($a,$b){//Compares the *hashes* of two variables to mess with timing attacks.
if(!val('s',$a,$b))return false;
$m=microtime();
return sha1($a.$m.$b)==sha1($b.$m.$a);
}
/*
userAccess()
Returns whether your user has permission to access this page, given the minimum access level in the hierarchy required to get in.
//In order of precedence:
//a=admin
//c=captain
//u=regular user
//x=not logged in
*/
//$_SESSION["user_verification_code"]='';
function userAccess($minPrivilegeLevel){
$minPrivilegeLevel=strtolower($minPrivilegeLevel);
if(sessioned('permissions'))$_SESSION['permissions']=strtolower($_SESSION['permissions']);
else $_SESSION['permissions']='x';
$hierarchy='xuca';//hierarchy, from lowest to highest
if(count($minPrivilegeLevel)!==1)err("Invalid permission level '$minPrivilegeLevel'");
if(!sessioned('email'))$nUser=0;
else $nUser=strpos($hierarchy,$_SESSION['permissions']);
$nAllowed=strpos($hierarchy,$minPrivilegeLevel);
if($nUser===false)err("Invalid session permission level '{$_SESSION["permissions"]}'");
if($nAllowed===false)err("Invalid input permission level '$minPrivilegeLevel'");
else return $nUser>=$nAllowed;
}
function restrictAccess($minPrivilegeLevel){
global $USER_LOGIN_REQUIRED;
if($minPrivilegeLevel=='u' && !$USER_LOGIN_REQUIRED)
$minPrivilegeLevel='x';
if(!userAccess($minPrivilegeLevel))
forceLogin();
}
if(sessioned('user_v')&&(!array_key_exists('v',$_COOKIE)||$_COOKIE['v']!=$_SESSION['user_v']))authError();
function loginEmailPass($email,$pass){
if(!filter_var($email, FILTER_VALIDATE_EMAIL))return false;
$q=DB::queryFirstRow('SELECT email, passhash, permissions, salt FROM users WHERE email=%s',$email);
if(!$q)return false;
$passhash=saltyStretchyHash($pass,$q['salt']);
if(!hashEquals($q['passhash'],$passhash))return false;
$_SESSION['email']=$q['email'];
$_SESSION['permissions']=$q['permissions'];
$_SESSION['user_v']=genRandStr();
setcookie('v',$_SESSION['user_v']);//passed back and forth and verified above.
return true;
}
function forceLogin(){
global $DOEQS_URL;
session_total_reset();
alert('Oops, you need to log in to access <i>'.basename($_SERVER['REQUEST_URI']).'</i>.',-1,'login.php');
$_SESSION['login_redirect_back']=$_SERVER['REQUEST_URI'];
header('Location: '.$DOEQS_URL.'login.php');
die();
}
function logout(){//--todo--uhhhhhh that's it? shouldn't it be whitelist-style erasure? idk, since $_SESSION['attempts_*'] needs to stay alive
foreach($_SESSION as $name=>$val)
if(strpos($name,'attempt_')===false)unset($_SESSION[$name]);
}
function saltyStretchyHash($pass,$salt){//WAAAY overdoing it. Messing with any sort of brute force attack.
if(!$salt){err('Needs salt');return;}
$universalSalt='sGh,mGo%Js(Kv/8o"xxN;}tPXR+*RW27FhgT<59R`AoaRP=)(pos3{<i%Yj#R^DSaei~sx"8#y7|&fx[EiLi$M{,n+V=?)T~gNky{(w|H|=+F\FQmo~-Gojg9<lB@+';
$hash='';
for($i=0;$i<274;$i++)$hash=hash('whirlpool',$universalSalt.hash('sha512',$i.$pass.$hash.$salt));
usleep(mt_rand(0,10000));//Messing up timing attacks :P
return $hash;
}
/*From MySQL Docs:
Passwords or other sensitive values supplied as arguments to encryption functions are sent in plaintext
to the MySQL server unless an SSL connection is used. Also, such values will appear in any MySQL logs
to which they are written. To avoid these types of exposure, applications can encrypt sensitive values
on the client side before sending them to the server. The same considerations apply to encryption keys.
To avoid exposing these, applications can use stored procedures to encrypt and decrypt values on the server side.
Therefore, always use PHP-side hashing unless it doesn't matter cryptographically. (we don't use SSL, unfortunately, shhhhh)
*/
/*
newProfileError
Creates a new profile with $email, $pass/$confpass. Initated to permissions 'u', regular user.
Returns false if there were no errors, and the text of the error if there were errors.
*/
function newProfileError($email,$pass,$confpass){
if(!filter_var($email, FILTER_VALIDATE_EMAIL))return 'Invalid email.';
if($pass!==$confpass)return 'Passwords do not match.';
if(strlen($pass)<8)return 'Password too short (must be at least 8 characters).';
if(DB::queryFirstRow('SELECT 1 from users WHERE email=%s LIMIT 1',$email))
return 'That email already exists.';
$permissions='u';//regular user
$salt=genRandStr();
$passhash=saltyStretchyHash($pass,$salt);//All of this is for nothing without end-to-end SSL.
DB::insert('users',array(
'email'=>$email,
'passhash'=>$passhash,
'permissions'=>$permissions,
'salt'=>$salt
));
return false;//'no error'
}
function chkCaptcha(){
require_once('classes/recaptchalib.php');
global $RECAPTCHA_privatekey;
$resp = recaptcha_check_answer ($RECAPTCHA_privatekey,
$_SERVER["REMOTE_ADDR"],
$_POST["recaptcha_challenge_field"],
$_POST["recaptcha_response_field"]);
return $resp->is_valid;
}
function getCaptcha(){
require_once('classes/recaptchalib.php');
global $RECAPTCHA_publickey;
return recaptcha_get_html($RECAPTCHA_publickey);
}
?>