Browse Source

2FA Changes

- Default interval now 30 seconds (breaking!)
- Don't add default parameters to QR code (smaller)

Fixes #22
spaghetti 8 years ago
parent
commit
f1e0c93d42
2 changed files with 22 additions and 49 deletions
  1. 21
    48
      classes/twofa.class.php
  2. 1
    1
      sections/user/edit.php

+ 21
- 48
classes/twofa.class.php View File

10
   private static $_base32lookup = array();
10
   private static $_base32lookup = array();
11
   private static $_supportedalgos = array('sha1', 'sha256', 'sha512', 'md5');
11
   private static $_supportedalgos = array('sha1', 'sha256', 'sha512', 'md5');
12
 
12
 
13
-  function __construct($issuer = null, $digits = 6, $period = 60, $algorithm = 'sha1') {
13
+  function __construct($issuer = null, $digits = 6, $period = 30, $algorithm = 'sha1') {
14
     $this->issuer = $issuer;
14
     $this->issuer = $issuer;
15
     $this->digits = $digits;
15
     $this->digits = $digits;
16
     $this->period = $period;
16
     $this->period = $period;
25
     self::$_base32lookup = array_flip(self::$_base32);
25
     self::$_base32lookup = array_flip(self::$_base32);
26
   }
26
   }
27
 
27
 
28
-  /**
29
-   * Create a new secret
30
-   */
31
   public function createSecret($bits = 80) {
28
   public function createSecret($bits = 80) {
32
     $secret = '';
29
     $secret = '';
33
-    $bytes = ceil($bits / 5);   //We use 5 bits of each byte (since we have a 32-character 'alphabet' / BASE32)
30
+    $bytes = ceil($bits / 5); //We use 5 bits of each byte (since we have a 32-character 'alphabet' / BASE32)
34
     $rnd = random_bytes($bytes);
31
     $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
32
+    for ($i = 0; $i < $bytes; $i++) {
33
+      $secret .= self::$_base32[ord($rnd[$i]) & 31]; //Mask out left 3 bits for 0-31 values
34
+    }
37
     return $secret;
35
     return $secret;
38
   }
36
   }
39
 
37
 
40
-  /**
41
-   * Calculate the code with given secret and point in time
42
-   */
38
+  // Calculate the code with given secret and point in time
43
   public function getCode($secret, $time = null) {
39
   public function getCode($secret, $time = null) {
44
     $secretkey = $this->base32Decode($secret);
40
     $secretkey = $this->base32Decode($secret);
45
 
41
 
52
     return str_pad($value % pow(10, $this->digits), $this->digits, '0', STR_PAD_LEFT);
48
     return str_pad($value % pow(10, $this->digits), $this->digits, '0', STR_PAD_LEFT);
53
   }
49
   }
54
 
50
 
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
-   */
51
+  // Check if the code is correct. This will accept codes starting from now +/- ($discrepancy * period) seconds
58
   public function verifyCode($secret, $code, $discrepancy = 1, $time = null) {
52
   public function verifyCode($secret, $code, $discrepancy = 1, $time = null) {
59
     $result = false;
53
     $result = false;
60
     $timetamp = $this->getTime($time);
54
     $timetamp = $this->getTime($time);
61
 
55
 
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);
56
+    // Always iterate all possible codes to prevent timing-attacks
57
+    for ($i = -$discrepancy; $i <= $discrepancy; $i++) {
58
+      $result |= hash_equals($this->getCode($secret, $timetamp + ($i * $this->period)), $code);
59
+    }
65
 
60
 
66
     return (bool)$result;
61
     return (bool)$result;
67
   }
62
   }
68
 
63
 
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
-   */
64
+  // Get data-uri of QRCode
91
   public function getQRCodeImageAsDataUri($label, $secret, $size = 300) {
65
   public function getQRCodeImageAsDataUri($label, $secret, $size = 300) {
92
 
66
 
93
     if (exec('which qrencode')) {
67
     if (exec('which qrencode')) {
120
     return (int)floor($time / $this->period) + ($offset * $this->period);
94
     return (int)floor($time / $this->period) + ($offset * $this->period);
121
   }
95
   }
122
 
96
 
123
-  /**
124
-   * Builds a string to be encoded in a QR code
125
-   */
97
+  // Builds a string to be encoded in a QR code
126
   public function getQRText($label, $secret) {
98
   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);
99
+    $QRText = 'otpauth://totp/'.rawurlencode($label).'?secret='.rawurlencode($secret);
100
+    $QRText .= ($this->issuer) ? '&issuer='.rawurlencode($this->issuer) : '';
101
+    $QRText .= ($this->period != 30) ? '&period='.intval($this->period) : '';
102
+    $QRText .= ($this->algorithm != 'sha1') ? '&algorithm='.rawurlencode(strtoupper($this->algorithm)) : '';
103
+    $QRText .= ($this->digits != 6) ? '&digits='.intval($this->digits) : '';
104
+    return $QRText;
133
   }
105
   }
134
 
106
 
135
   private function base32Decode($value) {
107
   private function base32Decode($value) {
145
     $blocks = trim(chunk_split(substr($buffer, 0, $length - ($length % 8)), 8, ' '));
117
     $blocks = trim(chunk_split(substr($buffer, 0, $length - ($length % 8)), 8, ' '));
146
 
118
 
147
     $output = '';
119
     $output = '';
148
-    foreach (explode(' ', $blocks) as $block)
120
+    foreach (explode(' ', $blocks) as $block) {
149
       $output .= chr(bindec(str_pad($block, 8, 0, STR_PAD_RIGHT)));
121
       $output .= chr(bindec(str_pad($block, 8, 0, STR_PAD_RIGHT)));
122
+    }
150
 
123
 
151
     return $output;
124
     return $output;
152
   }
125
   }

+ 1
- 1
sections/user/edit.php View File

27
   WHERE m.ID = '".db_string($UserID)."'");
27
   WHERE m.ID = '".db_string($UserID)."'");
28
 list($Username, $TwoFactor, $Email, $IRCKey, $Paranoia, $Info, $Avatar, $StyleID, $StyleURL, $SiteOptions, $UnseededAlerts, $DownloadAlt, $Class, $InfoTitle) = $DB->next_record(MYSQLI_NUM, array(4, 9));
28
 list($Username, $TwoFactor, $Email, $IRCKey, $Paranoia, $Info, $Avatar, $StyleID, $StyleURL, $SiteOptions, $UnseededAlerts, $DownloadAlt, $Class, $InfoTitle) = $DB->next_record(MYSQLI_NUM, array(4, 9));
29
 
29
 
30
-$TwoFA = new TwoFactorAuth(SITE_NAME);
30
+$TwoFA = new TwoFactorAuth();
31
 
31
 
32
 $Email = apc_exists('DBKEY') ? DBCrypt::decrypt($Email) : '[Encrypted]';
32
 $Email = apc_exists('DBKEY') ? DBCrypt::decrypt($Email) : '[Encrypted]';
33
 
33
 

Loading…
Cancel
Save