Browse Source

Add Two-Factor Authentication

2FA class stolen from RobThree and stripped down.
Works best with qrencode utility installed, but will fall back to using
google charts if qrencode is missing.
spaghetti 8 years ago
parent
commit
1350784f87

+ 153
- 0
classes/twofa.class.php View File

1
+<?php
2
+
3
+class TwoFactorAuth {
4
+  private $algorithm;
5
+  private $period;
6
+  private $digits;
7
+  private $issuer;
8
+  private static $_base32dict = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=';
9
+  private static $_base32;
10
+  private static $_base32lookup = array();
11
+  private static $_supportedalgos = array('sha1', 'sha256', 'sha512', 'md5');
12
+
13
+  function __construct($issuer = null, $digits = 6, $period = 60, $algorithm = 'sha1') {
14
+    $this->issuer = $issuer;
15
+    $this->digits = $digits;
16
+    $this->period = $period;
17
+
18
+    $algorithm = strtolower(trim($algorithm));
19
+    if (!in_array($algorithm, self::$_supportedalgos)){
20
+      $algorithm = 'sha1';
21
+    }
22
+    $this->algorithm = $algorithm;
23
+
24
+    self::$_base32 = str_split(self::$_base32dict);
25
+    self::$_base32lookup = array_flip(self::$_base32);
26
+  }
27
+
28
+  /**
29
+   * Create a new secret
30
+   */
31
+  public function createSecret($bits = 80) {
32
+    $secret = '';
33
+    $bytes = ceil($bits / 5);   //We use 5 bits of each byte (since we have a 32-character 'alphabet' / BASE32)
34
+    $rnd = random_bytes($bytes);
35
+    for ($i = 0; $i < $bytes; $i++)
36
+      $secret .= self::$_base32[ord($rnd[$i]) & 31];  //Mask out left 3 bits for 0-31 values
37
+    return $secret;
38
+  }
39
+
40
+  /**
41
+   * Calculate the code with given secret and point in time
42
+   */
43
+  public function getCode($secret, $time = null) {
44
+    $secretkey = $this->base32Decode($secret);
45
+
46
+    $timestamp = "\0\0\0\0" . pack('N*', $this->getTimeSlice($this->getTime($time)));  // Pack time into binary string
47
+    $hashhmac = hash_hmac($this->algorithm, $timestamp, $secretkey, true);             // Hash it with users secret key
48
+    $hashpart = substr($hashhmac, ord(substr($hashhmac, -1)) & 0x0F, 4);               // Use last nibble of result as index/offset and grab 4 bytes of the result
49
+    $value = unpack('N', $hashpart);                                                   // Unpack binary value
50
+    $value = $value[1] & 0x7FFFFFFF;                                                   // Drop MSB, keep only 31 bits
51
+
52
+    return str_pad($value % pow(10, $this->digits), $this->digits, '0', STR_PAD_LEFT);
53
+  }
54
+
55
+  /**
56
+   * Check if the code is correct. This will accept codes starting from ($discrepancy * $period) sec ago to ($discrepancy * period) sec from now
57
+   */
58
+  public function verifyCode($secret, $code, $discrepancy = 1, $time = null) {
59
+    $result = false;
60
+    $timetamp = $this->getTime($time);
61
+
62
+    // To keep safe from timing-attachs we iterate *all* possible codes even though we already may have verified a code is correct
63
+    for ($i = -$discrepancy; $i <= $discrepancy; $i++)
64
+      $result |= $this->codeEquals($this->getCode($secret, $timetamp + ($i * $this->period)), $code);
65
+
66
+    return (bool)$result;
67
+  }
68
+
69
+  /**
70
+   * Timing-attack safe comparison of 2 codes (see http://blog.ircmaxell.com/2014/11/its-all-about-time.html)
71
+   */
72
+  private function codeEquals($safe, $user) {
73
+    if (function_exists('hash_equals')) {
74
+      return hash_equals($safe, $user);
75
+    } else {
76
+      // In general, it's not possible to prevent length leaks. So it's OK to leak the length. The important part is that
77
+      // we don't leak information about the difference of the two strings.
78
+      if (strlen($safe)===strlen($user)) {
79
+        $result = 0;
80
+        for ($i = 0; $i < strlen($safe); $i++)
81
+          $result |= (ord($safe[$i]) ^ ord($user[$i]));
82
+        return $result === 0;
83
+      }
84
+    }
85
+    return false;
86
+  }
87
+
88
+  /**
89
+   * Get data-uri of QRCode
90
+   */
91
+  public function getQRCodeImageAsDataUri($label, $secret, $size = 300) {
92
+
93
+    if (exec('which qrencode')) {
94
+      $QRCodeImage = shell_exec("qrencode -s ".(int)($size/40)." -m 3 -o - '".$this->getQRText($label, $secret)."'");
95
+    } else {
96
+      $curlhandle = curl_init();
97
+
98
+      curl_setopt_array($curlhandle, array(
99
+        CURLOPT_URL => 'https://chart.googleapis.com/chart?cht=qr&chs='.$size.'x'.$size.'&chld=L|1&chl='.rawurlencode($this->getQRText($label, $secret)),
100
+        CURLOPT_RETURNTRANSFER => true,
101
+        CURLOPT_CONNECTTIMEOUT => 10,
102
+        CURLOPT_DNS_CACHE_TIMEOUT => 10,
103
+        CURLOPT_TIMEOUT => 10,
104
+        CURLOPT_SSL_VERIFYPEER => false,
105
+        CURLOPT_USERAGENT => 'TwoFactorAuth'
106
+      ));
107
+
108
+      $QRCodeImage = curl_exec($curlhandle);
109
+      curl_close($curlhandle);
110
+    }
111
+
112
+    return 'data:image/png;base64,'.base64_encode($QRCodeImage);
113
+  }
114
+
115
+  private function getTime($time) {
116
+    return ($time === null) ? time() : $time;
117
+  }
118
+
119
+  private function getTimeSlice($time = null, $offset = 0) {
120
+    return (int)floor($time / $this->period) + ($offset * $this->period);
121
+  }
122
+
123
+  /**
124
+   * Builds a string to be encoded in a QR code
125
+   */
126
+  public function getQRText($label, $secret) {
127
+    return 'otpauth://totp/' . rawurlencode($label)
128
+      . '?secret=' . rawurlencode($secret)
129
+      . '&issuer=' . rawurlencode($this->issuer)
130
+      . '&period=' . intval($this->period)
131
+      . '&algorithm=' . rawurlencode(strtoupper($this->algorithm))
132
+      . '&digits=' . intval($this->digits);
133
+  }
134
+
135
+  private function base32Decode($value) {
136
+    if (strlen($value)==0) { return ''; }
137
+
138
+    $buffer = '';
139
+    foreach (str_split($value) as $char) {
140
+      if ($char !== '=') {
141
+        $buffer .= str_pad(decbin(self::$_base32lookup[$char]), 5, 0, STR_PAD_LEFT);
142
+      }
143
+    }
144
+    $length = strlen($buffer);
145
+    $blocks = trim(chunk_split(substr($buffer, 0, $length - ($length % 8)), 8, ' '));
146
+
147
+    $output = '';
148
+    foreach (explode(' ', $blocks) as $block)
149
+      $output .= chr(bindec(str_pad($block, 8, 0, STR_PAD_RIGHT)));
150
+
151
+    return $output;
152
+  }
153
+}

+ 1
- 0
gazelle.sql View File

1519
   `Username` varchar(20) NOT NULL,
1519
   `Username` varchar(20) NOT NULL,
1520
   `Email` varchar(255) NOT NULL,
1520
   `Email` varchar(255) NOT NULL,
1521
   `PassHash` varchar(60) NOT NULL,
1521
   `PassHash` varchar(60) NOT NULL,
1522
+  `TwoFactor` varchar(255) DEFAULT NULL,
1522
   `IRCKey` char(32) DEFAULT NULL,
1523
   `IRCKey` char(32) DEFAULT NULL,
1523
   `LastLogin` datetime,
1524
   `LastLogin` datetime,
1524
   `LastAccess` datetime,
1525
   `LastAccess` datetime,

+ 102
- 92
sections/login/index.php View File

19
   error('Your IP address has been banned.');
19
   error('Your IP address has been banned.');
20
 }
20
 }
21
 
21
 
22
-require(SERVER_ROOT.'/classes/validate.class.php');
23
-$Validate = NEW VALIDATE;
22
+require_once SERVER_ROOT.'/classes/twofa.class.php';
23
+require_once SERVER_ROOT.'/classes/validate.class.php';
24
+
25
+$Validate = new VALIDATE;
26
+$TwoFA = new TwoFactorAuth(SITE_NAME);
24
 
27
 
25
 if (array_key_exists('action', $_GET) && $_GET['action'] == 'disabled') {
28
 if (array_key_exists('action', $_GET) && $_GET['action'] == 'disabled') {
26
   require('disabled.php');
29
   require('disabled.php');
231
           PermissionID,
234
           PermissionID,
232
           CustomPermissions,
235
           CustomPermissions,
233
           PassHash,
236
           PassHash,
237
+          TwoFactor,
234
           Enabled
238
           Enabled
235
         FROM users_main
239
         FROM users_main
236
         WHERE Username = '".db_string($_POST['username'])."'
240
         WHERE Username = '".db_string($_POST['username'])."'
237
           AND Username != ''");
241
           AND Username != ''");
238
-      list($UserID, $PermissionID, $CustomPermissions, $PassHash, $Enabled) = $DB->next_record(MYSQLI_NUM, array(2));
242
+      list($UserID, $PermissionID, $CustomPermissions, $PassHash, $TwoFactor, $Enabled) = $DB->next_record(MYSQLI_NUM, array(2));
239
       if (!$Banned) {
243
       if (!$Banned) {
240
         if ($UserID && Users::check_password($_POST['password'], $PassHash)) {
244
         if ($UserID && Users::check_password($_POST['password'], $PassHash)) {
241
           // Update hash if better algorithm available
245
           // Update hash if better algorithm available
245
               SET PassHash = '".make_sec_hash($_POST['password'])."'
249
               SET PassHash = '".make_sec_hash($_POST['password'])."'
246
               WHERE Username = '".db_string($_POST['username'])."'");
250
               WHERE Username = '".db_string($_POST['username'])."'");
247
           }
251
           }
248
-          if ($Enabled == 1) {
249
 
252
 
250
-            // Check if the current login attempt is from a location previously logged in from
251
-            if (apc_exists('DBKEY')) {
252
-              $DB->query("
253
-                SELECT IP
254
-                FROM users_history_ips
255
-                WHERE UserID = $UserID");
256
-              $IPs = $DB->to_array(false, MYSQLI_NUM);
257
-              $QueryParts = array();
258
-              foreach ($IPs as $i => $IP) {
259
-                $IPs[$i] = DBCrypt::decrypt($IP[0]);
260
-              }
261
-              $IPs = array_unique($IPs);
262
-              if (count($IPs) > 0) { // Always allow first login
263
-                foreach ($IPs as $IP) {
264
-                  $QueryParts[] = "(StartIP<=INET6_ATON('$IP') AND EndIP>=INET6_ATON('$IP'))";
253
+          if (empty($TwoFactor) || $TwoFA->verifyCode($TwoFactor, $_POST['twofa'])) {
254
+            if ($Enabled == 1) {
255
+
256
+              // Check if the current login attempt is from a location previously logged in from
257
+              if (apc_exists('DBKEY')) {
258
+                $DB->query("
259
+                  SELECT IP
260
+                  FROM users_history_ips
261
+                  WHERE UserID = $UserID");
262
+                $IPs = $DB->to_array(false, MYSQLI_NUM);
263
+                $QueryParts = array();
264
+                foreach ($IPs as $i => $IP) {
265
+                  $IPs[$i] = DBCrypt::decrypt($IP[0]);
265
                 }
266
                 }
266
-                $DB->query('SELECT ASN FROM geoip_asn WHERE '.implode(' OR ', $QueryParts));
267
-                $PastASNs = array_column($DB->to_array(false, MYSQLI_NUM), 0);
268
-                $DB->query("SELECT ASN FROM geoip_asn WHERE StartIP<=INET6_ATON('$_SERVER[REMOTE_ADDR]') AND EndIP>=INET6_ATON('$_SERVER[REMOTE_ADDR]')");
269
-                list($CurrentASN) = $DB->next_record();
270
-
271
-                if (!in_array($CurrentASN, $PastASNs)) {
272
-                  // Never logged in from this location before
273
-                  if ($Cache->get_value('new_location_'.$UserID.'_'.$CurrentASN) !== true) {
274
-                    $DB->query("
275
-                      SELECT
276
-                        UserName,
277
-                        Email
278
-                      FROM users_main
279
-                      WHERE ID = $UserID");
280
-                    list($Username, $Email) = $DB->next_record();
281
-                    Users::auth_location($UserID, $Username, $CurrentASN, DBCrypt::decrypt($Email));
282
-                    require('newlocation.php');
283
-                    die();
267
+                $IPs = array_unique($IPs);
268
+                if (count($IPs) > 0) { // Always allow first login
269
+                  foreach ($IPs as $IP) {
270
+                    $QueryParts[] = "(StartIP<=INET6_ATON('$IP') AND EndIP>=INET6_ATON('$IP'))";
271
+                  }
272
+                  $DB->query('SELECT ASN FROM geoip_asn WHERE '.implode(' OR ', $QueryParts));
273
+                  $PastASNs = array_column($DB->to_array(false, MYSQLI_NUM), 0);
274
+                  $DB->query("SELECT ASN FROM geoip_asn WHERE StartIP<=INET6_ATON('$_SERVER[REMOTE_ADDR]') AND EndIP>=INET6_ATON('$_SERVER[REMOTE_ADDR]')");
275
+                  list($CurrentASN) = $DB->next_record();
276
+
277
+                  if (!in_array($CurrentASN, $PastASNs)) {
278
+                    // Never logged in from this location before
279
+                    if ($Cache->get_value('new_location_'.$UserID.'_'.$CurrentASN) !== true) {
280
+                      $DB->query("
281
+                        SELECT
282
+                          UserName,
283
+                          Email
284
+                        FROM users_main
285
+                        WHERE ID = $UserID");
286
+                      list($Username, $Email) = $DB->next_record();
287
+                      Users::auth_location($UserID, $Username, $CurrentASN, DBCrypt::decrypt($Email));
288
+                      require('newlocation.php');
289
+                      die();
290
+                    }
284
                   }
291
                   }
285
                 }
292
                 }
286
               }
293
               }
287
-            }
288
 
294
 
289
-            $SessionID = Users::make_secret(64);
290
-            $KeepLogged = ($_POST['keeplogged'] ?? false) ? 1 : 0;
291
-            setcookie('session', $SessionID, (time()+60*60*24*365)*$KeepLogged, '/', '', true, true);
292
-            setcookie('userid', $UserID, (time()+60*60*24*365)*$KeepLogged, '/', '', true, true);
293
-
294
-            // Because we <3 our staff
295
-            $Permissions = Permissions::get_permissions($PermissionID);
296
-            $CustomPermissions = unserialize($CustomPermissions);
297
-            if (isset($Permissions['Permissions']['site_disable_ip_history'])
298
-              || isset($CustomPermissions['site_disable_ip_history'])
299
-            ) {
300
-              $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
301
-            }
295
+              $SessionID = Users::make_secret(64);
296
+              $KeepLogged = ($_POST['keeplogged'] ?? false) ? 1 : 0;
297
+              setcookie('session', $SessionID, (time()+60*60*24*365)*$KeepLogged, '/', '', true, true);
298
+              setcookie('userid', $UserID, (time()+60*60*24*365)*$KeepLogged, '/', '', true, true);
299
+
300
+              // Because we <3 our staff
301
+              $Permissions = Permissions::get_permissions($PermissionID);
302
+              $CustomPermissions = unserialize($CustomPermissions);
303
+              if (isset($Permissions['Permissions']['site_disable_ip_history'])
304
+                || isset($CustomPermissions['site_disable_ip_history'])
305
+              ) {
306
+                $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
307
+              }
302
 
308
 
303
-            $DB->query("
304
-              INSERT INTO users_sessions
305
-                (UserID, SessionID, KeepLogged, Browser, OperatingSystem, IP, LastUpdate, FullUA)
306
-              VALUES
307
-                ('$UserID', '".db_string($SessionID)."', '$KeepLogged', '$Browser', '$OperatingSystem', '".db_string(apc_exists('DBKEY')?DBCrypt::encrypt($_SERVER['REMOTE_ADDR']):'0.0.0.0')."', '".sqltime()."', '".db_string($_SERVER['HTTP_USER_AGENT'])."')");
308
-
309
-            $Cache->begin_transaction("users_sessions_$UserID");
310
-            $Cache->insert_front($SessionID, array(
311
-                'SessionID' => $SessionID,
312
-                'Browser' => $Browser,
313
-                'OperatingSystem' => $OperatingSystem,
314
-                'IP' => (apc_exists('DBKEY')?DBCrypt::encrypt($_SERVER['REMOTE_ADDR']):'0.0.0.0'),
315
-                'LastUpdate' => sqltime()
316
-                ));
317
-            $Cache->commit_transaction(0);
318
-
319
-            $Sql = "
320
-              UPDATE users_main
321
-              SET
322
-                LastLogin = '".sqltime()."',
323
-                LastAccess = '".sqltime()."'
324
-              WHERE ID = '".db_string($UserID)."'";
325
-
326
-            $DB->query($Sql);
327
-
328
-            if (!empty($_COOKIE['redirect'])) {
329
-              $URL = $_COOKIE['redirect'];
330
-              setcookie('redirect', '', time() - 60 * 60 * 24, '/', '', false);
331
-              header("Location: $URL");
332
-              die();
309
+              $DB->query("
310
+                INSERT INTO users_sessions
311
+                  (UserID, SessionID, KeepLogged, Browser, OperatingSystem, IP, LastUpdate, FullUA)
312
+                VALUES
313
+                  ('$UserID', '".db_string($SessionID)."', '$KeepLogged', '$Browser', '$OperatingSystem', '".db_string(apc_exists('DBKEY')?DBCrypt::encrypt($_SERVER['REMOTE_ADDR']):'0.0.0.0')."', '".sqltime()."', '".db_string($_SERVER['HTTP_USER_AGENT'])."')");
314
+
315
+              $Cache->begin_transaction("users_sessions_$UserID");
316
+              $Cache->insert_front($SessionID, array(
317
+                  'SessionID' => $SessionID,
318
+                  'Browser' => $Browser,
319
+                  'OperatingSystem' => $OperatingSystem,
320
+                  'IP' => (apc_exists('DBKEY')?DBCrypt::encrypt($_SERVER['REMOTE_ADDR']):'0.0.0.0'),
321
+                  'LastUpdate' => sqltime()
322
+                  ));
323
+              $Cache->commit_transaction(0);
324
+
325
+              $Sql = "
326
+                UPDATE users_main
327
+                SET
328
+                  LastLogin = '".sqltime()."',
329
+                  LastAccess = '".sqltime()."'
330
+                WHERE ID = '".db_string($UserID)."'";
331
+
332
+              $DB->query($Sql);
333
+
334
+              if (!empty($_COOKIE['redirect'])) {
335
+                $URL = $_COOKIE['redirect'];
336
+                setcookie('redirect', '', time() - 60 * 60 * 24, '/', '', false);
337
+                header("Location: $URL");
338
+                die();
339
+              } else {
340
+                header('Location: index.php');
341
+                die();
342
+              }
333
             } else {
343
             } else {
334
-              header('Location: index.php');
335
-              die();
344
+              log_attempt();
345
+              if ($Enabled == 2) {
346
+
347
+                // Save the username in a cookie for the disabled page
348
+                setcookie('username', db_string($_POST['username']), time() + 60 * 60, '/', '', false);
349
+                header('Location: login.php?action=disabled');
350
+              } elseif ($Enabled == 0) {
351
+                $Err = 'Your account has not been confirmed.<br />Please check your email.';
352
+              }
353
+              setcookie('keeplogged', '', time() + 60 * 60 * 24 * 365, '/', '', false);
336
             }
354
             }
337
           } else {
355
           } else {
338
             log_attempt();
356
             log_attempt();
339
-            if ($Enabled == 2) {
340
-
341
-              // Save the username in a cookie for the disabled page
342
-              setcookie('username', db_string($_POST['username']), time() + 60 * 60, '/', '', false);
343
-              header('Location: login.php?action=disabled');
344
-            } elseif ($Enabled == 0) {
345
-              $Err = 'Your account has not been confirmed.<br />Please check your email.';
346
-            }
357
+            $Err = 'Two-factor authentication failed.';
347
             setcookie('keeplogged', '', time() + 60 * 60 * 24 * 365, '/', '', false);
358
             setcookie('keeplogged', '', time() + 60 * 60 * 24 * 365, '/', '', false);
348
           }
359
           }
349
         } else {
360
         } else {
350
           log_attempt();
361
           log_attempt();
351
-
352
           $Err = 'Your username or password was incorrect.';
362
           $Err = 'Your username or password was incorrect.';
353
           setcookie('keeplogged', '', time() + 60 * 60 * 24 * 365, '/', '', false);
363
           setcookie('keeplogged', '', time() + 60 * 60 * 24 * 365, '/', '', false);
354
         }
364
         }

+ 14
- 1
sections/login/login.php View File

27
         <input type="password" name="password" id="password" class="inputtext" required="required" maxlength="307200" pattern=".{6,307200}" placeholder="Password" />
27
         <input type="password" name="password" id="password" class="inputtext" required="required" maxlength="307200" pattern=".{6,307200}" placeholder="Password" />
28
       </td>
28
       </td>
29
     </tr>
29
     </tr>
30
+    <tr id="2fa_tr">
31
+      <td>Two-factor&nbsp;</td>
32
+      <td colspan="2">
33
+        <input type="text" name="twofa" id="twofa" class="inputtext" maxlength="6" pattern="[0-9]{6}" placeholder="2FA Verification Code" />
34
+      </td>
35
+    </tr>
36
+    <tr>
37
+      <td></td>
38
+      <td>
39
+        <input type="checkbox" id="keep2fa" name="keep2fa" value="0">
40
+        <label for="keep2fa">Show 2FA</label>
41
+      </td>
42
+    </tr>
30
     <tr>
43
     <tr>
31
       <td></td>
44
       <td></td>
32
       <td>
45
       <td>
33
-        <input type="checkbox" id="keeplogged" name="keeplogged" value="1"<?=(isset($_REQUEST['keeplogged']) && $_REQUEST['keeplogged']) ? ' checked="checked"' : ''?> />
46
+        <input type="checkbox" id="keeplogged" name="keeplogged" value="1"<?=(isset($_REQUEST['keeplogged']) && $_REQUEST['keeplogged']) ? ' checked="checked"' : ''?>>
34
         <label for="keeplogged">Remember me</label>
47
         <label for="keeplogged">Remember me</label>
35
       </td>
48
       </td>
36
       <td><input type="submit" name="login" value="Log in" class="submit" /></td>
49
       <td><input type="submit" name="login" value="Log in" class="submit" /></td>

+ 24
- 2
sections/user/edit.php View File

1
 <?
1
 <?
2
+require(SERVER_ROOT.'/classes/twofa.class.php');
2
 $UserID = $_REQUEST['userid'];
3
 $UserID = $_REQUEST['userid'];
3
 if (!is_number($UserID)) {
4
 if (!is_number($UserID)) {
4
   error(404);
5
   error(404);
7
 $DB->query("
8
 $DB->query("
8
   SELECT
9
   SELECT
9
     m.Username,
10
     m.Username,
11
+    m.TwoFactor,
10
     m.Email,
12
     m.Email,
11
     m.IRCKey,
13
     m.IRCKey,
12
     m.Paranoia,
14
     m.Paranoia,
23
     JOIN users_info AS i ON i.UserID = m.ID
25
     JOIN users_info AS i ON i.UserID = m.ID
24
     LEFT JOIN permissions AS p ON p.ID = m.PermissionID
26
     LEFT JOIN permissions AS p ON p.ID = m.PermissionID
25
   WHERE m.ID = '".db_string($UserID)."'");
27
   WHERE m.ID = '".db_string($UserID)."'");
26
-list($Username, $Email, $IRCKey, $Paranoia, $Info, $Avatar, $StyleID, $StyleURL, $SiteOptions, $UnseededAlerts, $DownloadAlt, $Class, $InfoTitle) = $DB->next_record(MYSQLI_NUM, array(3, 8));
28
+list($Username, $TwoFactor, $Email, $IRCKey, $Paranoia, $Info, $Avatar, $StyleID, $StyleURL, $SiteOptions, $UnseededAlerts, $DownloadAlt, $Class, $InfoTitle) = $DB->next_record(MYSQLI_NUM, array(3, 8));
29
+
30
+$TwoFA = new TwoFactorAuth(SITE_NAME);
27
 
31
 
28
 $Email = apc_exists('DBKEY') ? DBCrypt::decrypt($Email) : '[Encrypted]';
32
 $Email = apc_exists('DBKEY') ? DBCrypt::decrypt($Email) : '[Encrypted]';
29
 
33
 
774
           <p class="min_padding">When changing your email address, you must enter your current password in the "Current password" field before saving your changes.</p>
778
           <p class="min_padding">When changing your email address, you must enter your current password in the "Current password" field before saving your changes.</p>
775
         </td>
779
         </td>
776
       </tr>
780
       </tr>
781
+      <tr id="acc_2fa_tr">
782
+        <td class="label tooltip" title="This will let you enable 2-Factor Auth for your <?=SITE_NAME?> account. The use of your 2FA client will be required whenever you login after enabling it."><strong>2-Factor Auth</strong></td>
783
+        <td>
784
+          <? $TwoFASecret = empty($TwoFactor) ? $TwoFA->createSecret() : $TwoFactor; ?>
785
+          <div class="field_div">
786
+            <? if (!empty($TwoFactor)) { ?>
787
+            <p>2FA is enabled for this account with the following secret:</p>
788
+            <? } ?>
789
+            <img src="<?=$TwoFA->getQRCodeImageAsDataUri(SITE_NAME, $TwoFASecret)?>">
790
+            <input type="text" size="20" name="twofasecret" id="twofasecret" value="<?=$TwoFASecret?>" readonly><br>
791
+            <? if (empty($TwoFactor)) { ?>
792
+            <input type="text" size="20" maxlength="6" name="twofa" id="twofa" placeholder="Verification Code">
793
+            <p class="min_padding">To enable 2FA, scan the above QR code (or add the secret below it) to your 2FA client of choice, and enter a verification code it generates. Note that the verification code must not have expired when you save your profile.</p>
794
+            <p class="min_padding">When setting up 2FA, you must enter your current password in the "Current password" field before saving your changes.</p>
795
+            <? } ?>
796
+          </div>
797
+        </td>
798
+      </tr>
777
       <tr id="acc_password_tr">
799
       <tr id="acc_password_tr">
778
         <td class="label"><strong>Change password</strong></td>
800
         <td class="label"><strong>Change password</strong></td>
779
         <td>
801
         <td>
780
           <div class="field_div">
802
           <div class="field_div">
781
-            <label>Current password:<br />
803
+            <label>Current password:<br>
782
             <input type="password" size="40" name="cur_pass" id="cur_pass" maxlength="307200" value="" /></label>
804
             <input type="password" size="40" name="cur_pass" id="cur_pass" maxlength="307200" value="" /></label>
783
           </div>
805
           </div>
784
           <div class="field_div">
806
           <div class="field_div">

+ 30
- 2
sections/user/take_edit.php View File

29
 $Val->SetFields('collagecovers', 1, "number", "You forgot to select your collage option.");
29
 $Val->SetFields('collagecovers', 1, "number", "You forgot to select your collage option.");
30
 $Val->SetFields('avatar', 0, "regex", "You did not enter a valid avatar URL.", array('regex' => "/^".IMAGE_REGEX."$/i"));
30
 $Val->SetFields('avatar', 0, "regex", "You did not enter a valid avatar URL.", array('regex' => "/^".IMAGE_REGEX."$/i"));
31
 $Val->SetFields('email', 1, "email", "You did not enter a valid email address.");
31
 $Val->SetFields('email', 1, "email", "You did not enter a valid email address.");
32
+$Val->SetFields('twofa', 0, "regex", "You did not enter a valid 2FA verification code.", array('regex' => '/^[0-9]{6}$/'));
32
 $Val->SetFields('irckey', 0, "string", "You did not enter a valid IRC key. An IRC key must be between 6 and 32 characters long.", array('minlength' => 6, 'maxlength' => 32));
33
 $Val->SetFields('irckey', 0, "string", "You did not enter a valid IRC key. An IRC key must be between 6 and 32 characters long.", array('minlength' => 6, 'maxlength' => 32));
33
 $Val->SetFields('new_pass_1', 0, "regex", "You did not enter a valid password. A valid password is 6 characters or longer.", array('regex' => '/(?=^.{6,}$).*$/'));
34
 $Val->SetFields('new_pass_1', 0, "regex", "You did not enter a valid password. A valid password is 6 characters or longer.", array('regex' => '/(?=^.{6,}$).*$/'));
34
 $Val->SetFields('new_pass_2', 1, "compare", "Your passwords do not match.", array('comparefield' => 'new_pass_1'));
35
 $Val->SetFields('new_pass_2', 1, "compare", "Your passwords do not match.", array('comparefield' => 'new_pass_1'));
176
     header("Location: user.php?action=edit&userid=$UserID");
177
     header("Location: user.php?action=edit&userid=$UserID");
177
     die();
178
     die();
178
   }
179
   }
179
-
180
-
181
 }
180
 }
182
 //End email change
181
 //End email change
183
 
182
 
183
+//2FA activation
184
+if (isset($_POST['twofa'])) {
185
+  $DB->query("
186
+    SELECT TwoFactor, PassHash
187
+    FROM users_main
188
+    WHERE ID = $UserID");
189
+  list($TwoFactor, $PassHash) = $DB->next_record();
190
+  if (empty($TwoFactor)) {
191
+    if (!Users::check_password($_POST['cur_pass'], $PassHash)) {
192
+      error('You did not enter the correct password.');
193
+      header("Location: user.php?action=edit&userid=$UserID");
194
+      die();
195
+    }
196
+    require_once SERVER_ROOT.'/classes/twofa.class.php';
197
+    $TwoFA = new TwoFactorAuth(SITE_NAME);
198
+    if ($TwoFA->verifyCode($_POST['twofasecret'], $_POST['twofa'])) {
199
+      $DB->query("
200
+        UPDATE users_main
201
+        SET TwoFactor='".db_string($_POST['twofasecret'])."'
202
+        WHERE ID = $UserID");
203
+    } else {
204
+      error('Invalid 2FA verification code.');
205
+      header("Location: user.php?action=edit&userid=$UserID");
206
+      die();
207
+    }
208
+  }
209
+}
210
+//End 2FA
211
+
184
 if (!$Err && ($_POST['cur_pass'] || $_POST['new_pass_1'] || $_POST['new_pass_2'])) {
212
 if (!$Err && ($_POST['cur_pass'] || $_POST['new_pass_1'] || $_POST['new_pass_2'])) {
185
   $DB->query("
213
   $DB->query("
186
     SELECT PassHash
214
     SELECT PassHash

+ 16
- 9
static/functions/public.js View File

1
-if ($('#no-cookies')) {
2
-  cookie.set('cookie_test', 1, 1);
3
-  if (cookie.get('cookie_test') != null) {
4
-      cookie.del('cookie_test');
5
-  } else {
6
-      $('#no-cookies').gshow();
7
-  }
8
-}
9
-
10
 $(() => {
1
 $(() => {
11
   if ($('#bg_data')) {
2
   if ($('#bg_data')) {
12
     $('#content')[0].style.backgroundImage = "url(/misc/bg/"+$('#bg_data')[0].attributes.bg.value+")";
3
     $('#content')[0].style.backgroundImage = "url(/misc/bg/"+$('#bg_data')[0].attributes.bg.value+")";
13
   }
4
   }
5
+
6
+  if ($('#no-cookies')) {
7
+    cookie.set('cookie_test', 1, 1);
8
+    if (cookie.get('cookie_test') != null) {
9
+        cookie.del('cookie_test');
10
+    } else {
11
+        $('#no-cookies').gshow();
12
+    }
13
+  }
14
+
15
+  if ($('#keep2fa')) {
16
+    $('#2fa_tr').ghide()
17
+    $('#keep2fa')[0].onclick = (e) => {
18
+      $('#2fa_tr')[$('#keep2fa')[0].checked ? 'gshow' : 'ghide']()
19
+    }
20
+  }
14
 })
21
 })

Loading…
Cancel
Save