Browse Source

Add Universal Two Factor support

Also create seperate account security options page

This commit requires a new database table
spaghetti 7 years ago
parent
commit
b0ffed7857

+ 1
- 1
classes/twofa.class.php View File

@@ -25,7 +25,7 @@ class TwoFactorAuth {
25 25
     self::$_base32lookup = array_flip(self::$_base32);
26 26
   }
27 27
 
28
-  public function createSecret($bits = 80) {
28
+  public function createSecret($bits = 210) {
29 29
     $secret = '';
30 30
     $bytes = ceil($bits / 5); //We use 5 bits of each byte (since we have a 32-character 'alphabet' / BASE32)
31 31
     $rnd = random_bytes($bytes);

+ 474
- 0
classes/u2f.class.php View File

@@ -0,0 +1,474 @@
1
+<?php
2
+// Taken from https://github.com/Yubico/php-u2flib-server/blob/master/src/u2flib_server/U2F.php
3
+
4
+/* Copyright (c) 2014 Yubico AB
5
+ * All rights reserved.
6
+ *
7
+ * Redistribution and use in source and binary forms, with or without
8
+ * modification, are permitted provided that the following conditions are
9
+ * met:
10
+ *
11
+ *   * Redistributions of source code must retain the above copyright
12
+ *     notice, this list of conditions and the following disclaimer.
13
+ *
14
+ *   * Redistributions in binary form must reproduce the above
15
+ *     copyright notice, this list of conditions and the following
16
+ *     disclaimer in the documentation and/or other materials provided
17
+ *     with the distribution.
18
+ *
19
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ */
31
+
32
+namespace u2f;
33
+
34
+/** Constant for the version of the u2f protocol */
35
+const U2F_VERSION = "U2F_V2";
36
+
37
+/** Error for the authentication message not matching any outstanding
38
+ * authentication request */
39
+const ERR_NO_MATCHING_REQUEST = 1;
40
+
41
+/** Error for the authentication message not matching any registration */
42
+const ERR_NO_MATCHING_REGISTRATION = 2;
43
+
44
+/** Error for the signature on the authentication message not verifying with
45
+ * the correct key */
46
+const ERR_AUTHENTICATION_FAILURE = 3;
47
+
48
+/** Error for the challenge in the registration message not matching the
49
+ * registration challenge */
50
+const ERR_UNMATCHED_CHALLENGE = 4;
51
+
52
+/** Error for the attestation signature on the registration message not
53
+ * verifying */
54
+const ERR_ATTESTATION_SIGNATURE = 5;
55
+
56
+/** Error for the attestation verification not verifying */
57
+const ERR_ATTESTATION_VERIFICATION = 6;
58
+
59
+/** Error for not getting good random from the system */
60
+const ERR_BAD_RANDOM = 7;
61
+
62
+/** Error when the counter is lower than expected */
63
+const ERR_COUNTER_TOO_LOW = 8;
64
+
65
+/** Error decoding public key */
66
+const ERR_PUBKEY_DECODE = 9;
67
+
68
+/** Error user-agent returned error */
69
+const ERR_BAD_UA_RETURNING = 10;
70
+
71
+/** Error old OpenSSL version */
72
+const ERR_OLD_OPENSSL = 11;
73
+
74
+const PUBKEY_LEN = 65;
75
+
76
+class U2F {
77
+  private $appId;
78
+
79
+  private $attestDir;
80
+
81
+  private $FIXCERTS = array(
82
+    '349bca1031f8c82c4ceca38b9cebf1a69df9fb3b94eed99eb3fb9aa3822d26e8',
83
+    'dd574527df608e47ae45fbba75a2afdd5c20fd94a02419381813cd55a2a3398f',
84
+    '1d8764f0f7cd1352df6150045c8f638e517270e8b5dda1c63ade9c2280240cae',
85
+    'd0edc9a91a1677435a953390865d208c55b3183c6759c9b5a7ff494c322558eb',
86
+    '6073c436dcd064a48127ddbf6032ac1a66fd59a0c24434f070d4e564c124c897',
87
+    'ca993121846c464d666096d35f13bf44c1b05af205f9b4a1e00cf6cc10c5e511'
88
+  );
89
+
90
+  /**
91
+   * @param string $appId Application id for the running application
92
+   * @param string|null $attestDir Directory where trusted attestation roots may be found
93
+   * @throws Error If OpenSSL older than 1.0.0 is used
94
+   */
95
+  public function __construct($appId, $attestDir = null) {
96
+    if (OPENSSL_VERSION_NUMBER < 0x10000000) {
97
+      throw new Error('OpenSSL has to be at least version 1.0.0, this is ' . OPENSSL_VERSION_TEXT, ERR_OLD_OPENSSL);
98
+    }
99
+    $this->appId = $appId;
100
+    $this->attestDir = $attestDir;
101
+  }
102
+
103
+  /**
104
+   * Called to get a registration request to send to a user.
105
+   * Returns an array of one registration request and a array of sign requests.
106
+   *
107
+   * @param array $registrations List of current registrations for this
108
+   * user, to prevent the user from registering the same authenticator several
109
+   * times.
110
+   * @return array An array of two elements, the first containing a
111
+   * RegisterRequest the second being an array of SignRequest
112
+   * @throws Error
113
+   */
114
+  public function getRegisterData(array $registrations = []) {
115
+    $challenge = $this->createChallenge();
116
+    $request = new RegisterRequest($challenge, $this->appId);
117
+    $signs = $this->getAuthenticateData($registrations);
118
+    return [$request, $signs];
119
+  }
120
+
121
+  /**
122
+   * Called to verify and unpack a registration message.
123
+   *
124
+   * @param RegisterRequest $request this is a reply to
125
+   * @param object $response response from a user
126
+   * @param bool $includeCert set to true if the attestation certificate should be
127
+   * included in the returned Registration object
128
+   * @return Registration
129
+   * @throws Error
130
+   */
131
+  public function doRegister($request, $response, $includeCert = true) {
132
+    if (!is_object($request)) {
133
+      throw new \InvalidArgumentException('$request of doRegister() method only accepts object.');
134
+    }
135
+
136
+    if (!is_object($response)) {
137
+      throw new \InvalidArgumentException('$response of doRegister() method only accepts object.');
138
+    }
139
+
140
+    if (property_exists($response, 'errorCode') && $response->errorCode !== 0) {
141
+      throw new Error('User-agent returned error. Error code: ' . $response->errorCode, ERR_BAD_UA_RETURNING );
142
+    }
143
+
144
+    if (!is_bool($includeCert)) {
145
+      throw new \InvalidArgumentException('$include_cert of doRegister() method only accepts boolean.');
146
+    }
147
+
148
+    $rawReg = $this->base64u_decode($response->registrationData);
149
+    $regData = array_values(unpack('C*', $rawReg));
150
+    $clientData = $this->base64u_decode($response->clientData);
151
+    $cli = json_decode($clientData);
152
+
153
+    if ($cli->challenge !== $request->challenge) {
154
+      throw new Error('Registration challenge does not match', ERR_UNMATCHED_CHALLENGE );
155
+    }
156
+
157
+    $registration = new Registration();
158
+    $offs = 1;
159
+    $pubKey = substr($rawReg, $offs, PUBKEY_LEN);
160
+    $offs += PUBKEY_LEN;
161
+    // decode the pubKey to make sure it's good
162
+    $tmpKey = $this->pubkey_to_pem($pubKey);
163
+    if ($tmpKey === null) {
164
+      throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE );
165
+    }
166
+    $registration->publicKey = base64_encode($pubKey);
167
+    $khLen = $regData[$offs++];
168
+    $kh = substr($rawReg, $offs, $khLen);
169
+    $offs += $khLen;
170
+    $registration->keyHandle = $this->base64u_encode($kh);
171
+
172
+    // length of certificate is stored in byte 3 and 4 (excluding the first 4 bytes)
173
+    $certLen = 4;
174
+    $certLen += ($regData[$offs + 2] << 8);
175
+    $certLen += $regData[$offs + 3];
176
+
177
+    $rawCert = $this->fixSignatureUnusedBits(substr($rawReg, $offs, $certLen));
178
+    $offs += $certLen;
179
+    $pemCert  = "-----BEGIN CERTIFICATE-----\r\n";
180
+    $pemCert .= chunk_split(base64_encode($rawCert), 64);
181
+    $pemCert .= "-----END CERTIFICATE-----";
182
+    if ($includeCert) {
183
+      $registration->certificate = base64_encode($rawCert);
184
+    }
185
+    if ($this->attestDir) {
186
+      if (openssl_x509_checkpurpose($pemCert, -1, $this->get_certs()) !== true) {
187
+        throw new Error('Attestation certificate can not be validated', ERR_ATTESTATION_VERIFICATION );
188
+      }
189
+    }
190
+
191
+    if (!openssl_pkey_get_public($pemCert)) {
192
+      throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE );
193
+    }
194
+    $signature = substr($rawReg, $offs);
195
+
196
+    $dataToVerify  = chr(0);
197
+    $dataToVerify .= hash('sha256', $request->appId, true);
198
+    $dataToVerify .= hash('sha256', $clientData, true);
199
+    $dataToVerify .= $kh;
200
+    $dataToVerify .= $pubKey;
201
+
202
+    if (openssl_verify($dataToVerify, $signature, $pemCert, 'sha256') === 1) {
203
+      return $registration;
204
+    } else {
205
+      throw new Error('Attestation signature does not match', ERR_ATTESTATION_SIGNATURE );
206
+    }
207
+  }
208
+
209
+  /**
210
+   * Called to get an authentication request.
211
+   *
212
+   * @param array $registrations An array of the registrations to create authentication requests for.
213
+   * @return array An array of SignRequest
214
+   * @throws Error
215
+   */
216
+  public function getAuthenticateData(array $registrations) {
217
+    $sigs = array();
218
+    $challenge = $this->createChallenge();
219
+    foreach ($registrations as $reg) {
220
+      if (!is_object($reg)) {
221
+        throw new \InvalidArgumentException('$registrations of getAuthenticateData() method only accepts array of object.');
222
+      }
223
+
224
+      $sig = new SignRequest();
225
+      $sig->appId = $this->appId;
226
+      $sig->keyHandle = $reg->keyHandle;
227
+      $sig->challenge = $challenge;
228
+      $sigs[] = $sig;
229
+    }
230
+    return $sigs;
231
+  }
232
+
233
+  /**
234
+   * Called to verify an authentication response
235
+   *
236
+   * @param array $requests An array of outstanding authentication requests
237
+   * @param array $registrations An array of current registrations
238
+   * @param object $response A response from the authenticator
239
+   * @return Registration
240
+   * @throws Error
241
+   *
242
+   * The Registration object returned on success contains an updated counter
243
+   * that should be saved for future authentications.
244
+   * If the Error returned is ERR_COUNTER_TOO_LOW this is an indication of
245
+   * token cloning or similar and appropriate action should be taken.
246
+   */
247
+  public function doAuthenticate(array $requests, array $registrations, $response) {
248
+    if (!is_object($response)) {
249
+      throw new \InvalidArgumentException('$response of doAuthenticate() method only accepts object.');
250
+    }
251
+
252
+    if (property_exists($response, 'errorCode') && $response->errorCode !== 0) {
253
+      throw new Error('User-agent returned error. Error code: ' . $response->errorCode, ERR_BAD_UA_RETURNING );
254
+    }
255
+
256
+    $req = null;
257
+    $reg = null;
258
+
259
+    $clientData = $this->base64u_decode($response->clientData);
260
+    $decodedClient = json_decode($clientData);
261
+    foreach ($requests as $req) {
262
+      if (!is_object($req)) {
263
+        throw new \InvalidArgumentException('$requests of doAuthenticate() method only accepts array of object.');
264
+      }
265
+
266
+      if ($req->keyHandle === $response->keyHandle && $req->challenge === $decodedClient->challenge) {
267
+        break;
268
+      }
269
+
270
+      $req = null;
271
+    }
272
+    if ($req === null) {
273
+      throw new Error('No matching request found', ERR_NO_MATCHING_REQUEST );
274
+    }
275
+    foreach ($registrations as $reg) {
276
+      if (!is_object($reg)) {
277
+        throw new \InvalidArgumentException('$registrations of doAuthenticate() method only accepts array of object.');
278
+      }
279
+
280
+      if ($reg->keyHandle === $response->keyHandle) {
281
+        break;
282
+      }
283
+      $reg = null;
284
+    }
285
+    if ($reg === null) {
286
+      throw new Error('No matching registration found', ERR_NO_MATCHING_REGISTRATION );
287
+    }
288
+    $pemKey = $this->pubkey_to_pem($this->base64u_decode($reg->publicKey));
289
+    if ($pemKey === null) {
290
+      throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE );
291
+    }
292
+
293
+    $signData = $this->base64u_decode($response->signatureData);
294
+    $dataToVerify  = hash('sha256', $req->appId, true);
295
+    $dataToVerify .= substr($signData, 0, 5);
296
+    $dataToVerify .= hash('sha256', $clientData, true);
297
+    $signature = substr($signData, 5);
298
+
299
+    if (openssl_verify($dataToVerify, $signature, $pemKey, 'sha256') === 1) {
300
+      $ctr = unpack("Nctr", substr($signData, 1, 4));
301
+      $counter = $ctr['ctr'];
302
+      /* TODO: wrap-around should be handled somehow.. */
303
+      if ($counter > $reg->counter) {
304
+        $reg->counter = $counter;
305
+        return $reg;
306
+      } else {
307
+        throw new Error('Counter too low.', ERR_COUNTER_TOO_LOW );
308
+      }
309
+    } else {
310
+      throw new Error('Authentication failed', ERR_AUTHENTICATION_FAILURE );
311
+    }
312
+  }
313
+
314
+  /**
315
+   * @return array
316
+   */
317
+  private function get_certs() {
318
+    $files = array();
319
+    $dir = $this->attestDir;
320
+    if ($dir && $handle = opendir($dir)) {
321
+      while(false !== ($entry = readdir($handle))) {
322
+        if (is_file("$dir/$entry")) {
323
+          $files[] = "$dir/$entry";
324
+        }
325
+      }
326
+      closedir($handle);
327
+    }
328
+    return $files;
329
+  }
330
+
331
+  /**
332
+   * @param string $data
333
+   * @return string
334
+   */
335
+  private function base64u_encode($data) {
336
+    return trim(strtr(base64_encode($data), '+/', '-_'), '=');
337
+  }
338
+
339
+  /**
340
+   * @param string $data
341
+   * @return string
342
+   */
343
+  private function base64u_decode($data) {
344
+    return base64_decode(strtr($data, '-_', '+/'));
345
+  }
346
+
347
+  /**
348
+   * @param string $key
349
+   * @return null|string
350
+   */
351
+  private function pubkey_to_pem($key) {
352
+    if (strlen($key) !== PUBKEY_LEN || $key[0] !== "\x04") {
353
+      return null;
354
+    }
355
+
356
+    /*
357
+     * Convert the public key to binary DER format first
358
+     * Using the ECC SubjectPublicKeyInfo OIDs from RFC 5480
359
+     *
360
+     *  SEQUENCE(2 elem)                        30 59
361
+     *   SEQUENCE(2 elem)                       30 13
362
+     *    OID1.2.840.10045.2.1 (id-ecPublicKey) 06 07 2a 86 48 ce 3d 02 01
363
+     *    OID1.2.840.10045.3.1.7 (secp256r1)    06 08 2a 86 48 ce 3d 03 01 07
364
+     *   BIT STRING(520 bit)                    03 42 ..key..
365
+     */
366
+    $der  = "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01";
367
+    $der .= "\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07\x03\x42";
368
+    $der .= "\0".$key;
369
+
370
+    $pem  = "-----BEGIN PUBLIC KEY-----\r\n";
371
+    $pem .= chunk_split(base64_encode($der), 64);
372
+    $pem .= "-----END PUBLIC KEY-----";
373
+
374
+    return $pem;
375
+  }
376
+
377
+  /**
378
+   * @return string
379
+   * @throws Error
380
+   */
381
+  private function createChallenge() {
382
+    $challenge = openssl_random_pseudo_bytes(32, $crypto_strong );
383
+    if ($crypto_strong !== true) {
384
+      throw new Error('Unable to obtain a good source of randomness', ERR_BAD_RANDOM);
385
+    }
386
+
387
+    $challenge = $this->base64u_encode( $challenge );
388
+
389
+    return $challenge;
390
+  }
391
+
392
+  /**
393
+   * Fixes a certificate where the signature contains unused bits.
394
+   *
395
+   * @param string $cert
396
+   * @return mixed
397
+   */
398
+  private function fixSignatureUnusedBits($cert) {
399
+    if (in_array(hash('sha256', $cert), $this->FIXCERTS)) {
400
+      $cert[strlen($cert) - 257] = "\0";
401
+    }
402
+    return $cert;
403
+  }
404
+}
405
+
406
+/**
407
+ * Class for building a registration request
408
+ *
409
+ * @package u2flib_server
410
+ */
411
+class RegisterRequest {
412
+  public $version = U2F_VERSION;
413
+
414
+  public $challenge;
415
+
416
+  public $appId;
417
+
418
+  /**
419
+   * @param string $challenge
420
+   * @param string $appId
421
+   * @internal
422
+   */
423
+  public function __construct($challenge, $appId) {
424
+    $this->challenge = $challenge;
425
+    $this->appId = $appId;
426
+  }
427
+}
428
+
429
+/**
430
+ * Class for building up an authentication request
431
+ *
432
+ * @package u2flib_server
433
+ */
434
+class SignRequest {
435
+  public $version = U2F_VERSION;
436
+
437
+  public $challenge;
438
+
439
+  public $keyHandle;
440
+
441
+  public $appId;
442
+}
443
+
444
+/**
445
+ * Class returned for successful registrations
446
+ *
447
+ * @package u2flib_server
448
+ */
449
+class Registration {
450
+  public $keyHandle;
451
+
452
+  public $publicKey;
453
+
454
+  public $certificate;
455
+
456
+  public $counter = -1;
457
+}
458
+
459
+/**
460
+ * Error class, returned on errors
461
+ *
462
+ * @package u2flib_server
463
+ */
464
+class Error extends \Exception {
465
+  /**
466
+   * Override constructor and make message and code mandatory
467
+   * @param string $message
468
+   * @param int $code
469
+   * @param \Exception|null $previous
470
+   */
471
+  public function __construct($message, $code, \Exception $previous = null) {
472
+    parent::__construct($message, $code, $previous);
473
+  }
474
+}

+ 3
- 3
design/privateheader.php View File

@@ -533,12 +533,12 @@ if (!empty($Alerts) || !empty($ModBar)) { ?>
533 533
 <?
534 534
   }
535 535
   if (!empty($ModBar)) { ?>
536
-        <div class="alertbar blend">
537
-          <?=implode(' | ', $ModBar); echo "\n"?>
536
+        <div class="alertbar modbar">
537
+          <?=implode(' ', $ModBar); echo "\n"?>
538 538
         </div>
539 539
 <?  }
540 540
 if (check_perms('site_debug') && !apcu_exists('DBKEY')) { ?>
541
-        <div class="alertbar" style="color: white; background: #B53939;">
541
+        <div class="alertbar error">
542 542
           Warning: <a href="tools.php?action=database_key">no DB key</a>
543 543
         </div>
544 544
 <?  } ?>

+ 1
- 1
design/publicheader.php View File

@@ -11,7 +11,7 @@ define('FOOTER_FILE',SERVER_ROOT.'/design/publicfooter.php');
11 11
   <link rel="shortcut icon" href="favicon.ico?v=<?=md5_file('favicon.ico');?>" />
12 12
   <link href="<?=STATIC_SERVER ?>styles/public/style.css?v=<?=filemtime(SERVER_ROOT.'/static/styles/public/style.css')?>" rel="stylesheet" type="text/css" />
13 13
 <?
14
-  $Scripts = ['jquery', 'global', 'ajax.class', 'cookie.class', 'storage.class', 'public'];
14
+  $Scripts = ['jquery', 'global', 'ajax.class', 'cookie.class', 'storage.class', 'public', 'u2f'];
15 15
   foreach($Scripts as $Script) {
16 16
     if (($ScriptStats = G::$Cache->get_value("script_stats_$Script")) === false || $ScriptStats['mtime'] != filemtime(SERVER_ROOT.STATIC_SERVER."functions/$Script.js")) {
17 17
       $ScriptStats['mtime'] = filemtime(SERVER_ROOT.STATIC_SERVER."functions/$Script.js");

+ 10
- 0
gazelle.sql View File

@@ -1254,6 +1254,16 @@ CREATE TABLE `torrents_tags_votes` (
1254 1254
   PRIMARY KEY (`GroupID`,`TagID`,`UserID`,`Way`)
1255 1255
 ) ENGINE=InnoDB CHARSET=utf8;
1256 1256
 
1257
+CREATE TABLE `u2f` (
1258
+  `UserID` int(10) NOT NULL,
1259
+  `KeyHandle` varchar(255) NOT NULL,
1260
+  `PublicKey` varchar(255) NOT NULL,
1261
+  `Certificate` text,
1262
+  `Counter` int(10) NOT NULL DEFAULT '-1',
1263
+  `Valid` enum('0','1') NOT NULL DEFAULT '1',
1264
+  PRIMARY KEY (`UserID`,`KeyHandle`)
1265
+) ENGINE=InnoDB CHARSET=utf8;
1266
+
1257 1267
 CREATE TABLE `user_questions` (
1258 1268
   `ID` int(10) NOT NULL AUTO_INCREMENT,
1259 1269
   `Question` mediumtext,

+ 87
- 37
sections/login/index.php View File

@@ -20,10 +20,12 @@ if (Tools::site_ban_ip($_SERVER['REMOTE_ADDR'])) {
20 20
 }
21 21
 
22 22
 require_once SERVER_ROOT.'/classes/twofa.class.php';
23
+require_once SERVER_ROOT.'/classes/u2f.class.php';
23 24
 require_once SERVER_ROOT.'/classes/validate.class.php';
24 25
 
25 26
 $Validate = new VALIDATE;
26 27
 $TwoFA = new TwoFactorAuth(SITE_NAME);
28
+$U2F = new u2f\U2F('https://'.SITE_DOMAIN);
27 29
 
28 30
 if (array_key_exists('action', $_GET) && $_GET['action'] == 'disabled') {
29 31
   require('disabled.php');
@@ -293,52 +295,100 @@ else {
293 295
                 }
294 296
               }
295 297
 
296
-              $SessionID = Users::make_secret(64);
297
-              setcookie('session', $SessionID, (time()+60*60*24*365), '/', '', true, true);
298
-              setcookie('userid', $UserID, (time()+60*60*24*365), '/', '', 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';
298
+              $U2FRegs = [];
299
+              $DB->query("
300
+                SELECT KeyHandle, PublicKey, Certificate, Counter, Valid
301
+                FROM u2f
302
+                WHERE UserID = $UserID");
303
+              // Needs to be an array of objects, so we can't use to_array()
304
+              while (list($KeyHandle, $PublicKey, $Certificate, $Counter, $Valid) = $DB->next_record()) {
305
+                $U2FRegs[] = (object)['keyHandle'=>$KeyHandle, 'publicKey'=>$PublicKey, 'certificate'=>$Certificate, 'counter'=>$Counter, 'valid'=>$Valid];
307 306
               }
308 307
 
309
-              $DB->query("
310
-                INSERT INTO users_sessions
311
-                  (UserID, SessionID, KeepLogged, Browser, OperatingSystem, IP, LastUpdate, FullUA)
312
-                VALUES
313
-                  ('$UserID', '".db_string($SessionID)."', '1', '$Browser', '$OperatingSystem', '".db_string(apcu_exists('DBKEY')?DBCrypt::encrypt($_SERVER['REMOTE_ADDR']):'0.0.0.0')."', '".sqltime()."', '".db_string($_SERVER['HTTP_USER_AGENT'])."')");
308
+              if (sizeof($U2FRegs) > 0) {
309
+                // U2F is enabled for this account
310
+                if (isset($_POST['u2f-request']) && isset($_POST['u2f-response'])) {
311
+                  // Data from the U2F login page is present. Verify it.
312
+                  try {
313
+                    $U2FReg = $U2F->doAuthenticate(json_decode($_POST['u2f-request']), $U2FRegs, json_decode($_POST['u2f-response']));
314
+                    if ($U2FReg->valid != '1') throw new Exception('Token disabled.');
315
+                    $DB->query("UPDATE u2f
316
+                                SET Counter = ".($U2FReg->counter)."
317
+                                WHERE KeyHandle = '".db_string($U2FReg->keyHandle)."'
318
+                                AND UserID = $UserID");
319
+                  } catch (Exception $e) {
320
+                    $U2FErr = 'U2F key invalid. Error: '.($e->getMessage());
321
+                    if ($e->getMessage() == 'Token disabled.') {
322
+                      $U2FErr = 'This token was disabled due to suspected cloning. Contact staff for assistance';
323
+                    }
324
+                    if ($e->getMessage() == 'Counter too low.') {
325
+                      $BadHandle = json_decode($_POST['u2f-response'], true)['keyHandle'];
326
+                      $DB->query("UPDATE u2f
327
+                                  SET Valid = '0'
328
+                                  WHERE KeyHandle = '".db_string($BadHandle)."'
329
+                                  AND UserID = $UserID");
330
+                      $U2FErr = 'U2F counter too low. This token has been disabled due to suspected cloning. Contact staff for assistance.';
331
+                    }
332
+                  }
333
+                } else {
334
+                  // Data from the U2F login page is not present. Go there
335
+                  require('u2f.php');
336
+                  die();
337
+                }
338
+              }
339
+
340
+              if (sizeof($U2FRegs) == 0 || !isset($U2FErr)) {
341
+                $SessionID = Users::make_secret(64);
342
+                setcookie('session', $SessionID, (time()+60*60*24*365), '/', '', true, true);
343
+                setcookie('userid', $UserID, (time()+60*60*24*365), '/', '', true, true);
344
+
345
+                // Because we <3 our staff
346
+                $Permissions = Permissions::get_permissions($PermissionID);
347
+                $CustomPermissions = unserialize($CustomPermissions);
348
+                if (isset($Permissions['Permissions']['site_disable_ip_history'])
349
+                  || isset($CustomPermissions['site_disable_ip_history'])
350
+                ) {
351
+                  $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
352
+                }
314 353
 
315
-              $Cache->begin_transaction("users_sessions_$UserID");
316
-              $Cache->insert_front($SessionID, array(
354
+                $DB->query("
355
+                  INSERT INTO users_sessions
356
+                    (UserID, SessionID, KeepLogged, Browser, OperatingSystem, IP, LastUpdate, FullUA)
357
+                  VALUES
358
+                    ('$UserID', '".db_string($SessionID)."', '1', '$Browser', '$OperatingSystem', '".db_string(apcu_exists('DBKEY')?DBCrypt::encrypt($_SERVER['REMOTE_ADDR']):'0.0.0.0')."', '".sqltime()."', '".db_string($_SERVER['HTTP_USER_AGENT'])."')");
359
+
360
+                $Cache->begin_transaction("users_sessions_$UserID");
361
+                $Cache->insert_front($SessionID, [
317 362
                   'SessionID' => $SessionID,
318 363
                   'Browser' => $Browser,
319 364
                   'OperatingSystem' => $OperatingSystem,
320 365
                   'IP' => (apcu_exists('DBKEY')?DBCrypt::encrypt($_SERVER['REMOTE_ADDR']):'0.0.0.0'),
321 366
                   '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();
367
+                ]);
368
+                $Cache->commit_transaction(0);
369
+
370
+                $Sql = "
371
+                  UPDATE users_main
372
+                  SET
373
+                    LastLogin = '".sqltime()."',
374
+                    LastAccess = '".sqltime()."'
375
+                  WHERE ID = '".db_string($UserID)."'";
376
+
377
+                $DB->query($Sql);
378
+
379
+                if (!empty($_COOKIE['redirect'])) {
380
+                  $URL = $_COOKIE['redirect'];
381
+                  setcookie('redirect', '', time() - 60 * 60 * 24, '/', '', false);
382
+                  header("Location: $URL");
383
+                  die();
384
+                } else {
385
+                  header('Location: index.php');
386
+                  die();
387
+                }
339 388
               } else {
340
-                header('Location: index.php');
341
-                die();
389
+                log_attempt();
390
+                $Err = $U2FErr;
391
+                setcookie('keeplogged', '', time() + 60 * 60 * 24 * 365, '/', '', false);
342 392
               }
343 393
             } else {
344 394
               log_attempt();

+ 5
- 5
sections/login/newlocation.php View File

@@ -9,16 +9,16 @@ View::show_header('Authorize Location');
9 9
 if (isset($_REQUEST['act'])) {
10 10
 ?>
11 11
 
12
-Your location is now authorized to access this account.<br /><br />
12
+Your location is now authorized to access this account.<br><br>
13 13
 Click <a href="login.php">here</a> to login again.
14 14
 
15 15
 <? } else { ?>
16 16
 
17
-This appears to be the first time you've logged in from this location.<br /><br />
17
+This appears to be the first time you've logged in from this location.<br><br>
18 18
 
19
-As a security measure to ensure that you are really the owner of this account,<br />
20
-an email has been sent to the address in your profile settings. Please<br />
21
-click the link contained in that email to allow access from<br />
19
+As a security measure to ensure that you are really the owner of this account,<br>
20
+an email has been sent to the address in your profile settings. Please<br>
21
+click the link contained in that email to allow access from<br>
22 22
 your location, and then log in again.
23 23
 
24 24
 <? }

+ 24
- 0
sections/login/u2f.php View File

@@ -0,0 +1,24 @@
1
+<?
2
+if (!empty($LoggedUser['ID'])) {
3
+  header('Location: login.php');
4
+  die();
5
+}
6
+if (!isset($_POST['username']) || !isset($_POST['password']) || !isset($U2FRegs)) {
7
+  header('Location: login.php');
8
+  die();
9
+}
10
+
11
+$U2FReq = json_encode($U2F->getAuthenticateData($U2FRegs));
12
+
13
+View::show_header('U2F Authentication'); ?>
14
+
15
+<form id="u2f_sign_form" action="login.php" method="post">
16
+  <input type="hidden" name="username" value="<?=$_POST['username']?>">
17
+  <input type="hidden" name="password" value="<?=$_POST['password']?>">
18
+  <input type="hidden" name="u2f-request" value='<?=$U2FReq?>'>
19
+  <input type="hidden" name="u2f-response">
20
+</form>
21
+
22
+This account is protected by a Universal Two Factor token. To continue logging in, please insert your U2F token and press it if necessary.
23
+
24
+<? View::show_footer(); ?>

+ 176
- 0
sections/user/2fa.php View File

@@ -0,0 +1,176 @@
1
+<?
2
+require(SERVER_ROOT.'/classes/twofa.class.php');
3
+require(SERVER_ROOT.'/classes/u2f.class.php');
4
+
5
+$TwoFA = new TwoFactorAuth(SITE_NAME);
6
+$U2F = new u2f\U2F('https://'.SITE_DOMAIN);
7
+if ($Type = $_POST['type'] ?? false) {
8
+  if ($Type == 'PGP') {
9
+    $DB->query("
10
+      UPDATE users_main
11
+      SET PublicKey = '".db_string($_POST['publickey'])."'
12
+      WHERE ID = $UserID");
13
+    $Message = 'Public key '.(empty($_POST['publickey']) ? 'removed' : 'updated') ;
14
+  }
15
+  if ($Type == '2FA-E') {
16
+    if ($TwoFA->verifyCode($_POST['twofasecret'], $_POST['twofa'])) {
17
+      $DB->query("
18
+        UPDATE users_main
19
+        SET TwoFactor='".db_string($_POST['twofasecret'])."'
20
+        WHERE ID = $UserID");
21
+      $Message = "Two Factor Authentication enabled";
22
+    } else {
23
+      $Error = "Invalid 2FA verification code";
24
+    }
25
+  }
26
+  if ($Type == '2FA-D') {
27
+    $DB->query("
28
+      UPDATE users_main
29
+      SET TwoFactor = NULL
30
+      WHERE ID = $UserID");
31
+    $Message = "Two Factor Authentication disabled";
32
+  }
33
+  if ($Type == 'U2F-E') {
34
+    try {
35
+      $U2FReg = $U2F->doRegister(json_decode($_POST['u2f-request']), json_decode($_POST['u2f-response']));
36
+      $DB->query("
37
+      INSERT INTO u2f
38
+      (UserID, KeyHandle, PublicKey, Certificate, Counter, Valid)
39
+      Values ($UserID, '".db_string($U2FReg->keyHandle)."', '".db_string($U2FReg->publicKey)."', '".db_string($U2FReg->certificate)."', '".db_string($U2FReg->counter)."', '1')");
40
+      $Message = "U2F token registered";
41
+    } catch(Exception $e) {
42
+      $Error = "Failed to register U2F token";
43
+    }
44
+  }
45
+  if ($Type == 'U2F-D') {
46
+    $DB->query("
47
+      DELETE FROM u2f
48
+      WHERE UserID = $UserID");
49
+    $Message = 'U2F tokens deregistered';
50
+  }
51
+}
52
+
53
+$U2FRegs = [];
54
+$DB->query("
55
+  SELECT KeyHandle, PublicKey, Certificate, Counter
56
+  FROM u2f
57
+  WHERE UserID = $UserID");
58
+// Needs to be an array of objects, so we can't use to_array()
59
+while (list($KeyHandle, $PublicKey, $Certificate, $Counter) = $DB->next_record()) {
60
+  $U2FRegs[] = (object)['keyHandle'=>$KeyHandle, 'publicKey'=>$PublicKey, 'certificate'=>$Certificate, 'counter'=>$Counter];
61
+}
62
+
63
+$DB->query("
64
+  SELECT PublicKey, TwoFactor
65
+  FROM users_main
66
+  WHERE ID = $UserID");
67
+list($PublicKey, $TwoFactor) = $DB->next_record();
68
+
69
+list($U2FRequest, $U2FSigs) = $U2F->getRegisterData($U2FRegs);
70
+
71
+View::show_header("Two-factor Authentication Settings", 'u2f');
72
+?>
73
+<h2>Additional Account Security Options</h2>
74
+<div class="thin">
75
+<? if (isset($Message)) { ?>
76
+    <div class="alertbar"><?=$Message?></div>
77
+<? }
78
+   if (isset($Error)) { ?>
79
+    <div class="alertbar error"><?=$Error?></div>
80
+<? } ?>
81
+    <div class="box">
82
+      <div class="head">
83
+        <strong>PGP Public Key</strong>
84
+      </div>
85
+      <div class="pad">
86
+  <? if (empty($PublicKey)) {
87
+       if (!empty($TwoFactor) || sizeof($U2FRegs) > 0) { ?>
88
+        <strong class="important_text">You have a form of 2FA enabled but no PGP key associated with your account. If you lose access to your 2FA device, you will permanently lose access to your account.</strong>
89
+  <?   } ?>
90
+        <p>When setting up any form of second factor authentication, it is strongly recommended that you add your PGP public key as a form of secure recovery in the event that you lose access to your second factor device.</p>
91
+        <p>After adding a PGP public key to your account, you will be able to disable your account's second factor protection by solving a challenge that only someone with your private key could solve.</p>
92
+        <p>Additionally, being able to solve such a challenge when given manually by staff will suffice to provide proof of ownership of your account, provided no revocation certificate has been published for your key.</p>
93
+        <p>Before adding your PGP public key, please make sure that you have taken the necessary precautions to protect it from loss (backup) or theft (revocation certificate).</p>
94
+  <? } else { ?>
95
+        <p>The PGP public key associated with your account is shown below.</p>
96
+        <p>This key can be used to create challenges that are only solvable by the holder of the related private key. Successfully solving these challenges is necessary for disabling any form of second factor authentication or proving ownership of this account to staff when you are unable to login.</p>
97
+  <? } ?>
98
+        <form method="post">
99
+          <input type="hidden" name="type" value="PGP">
100
+          Public Key:
101
+          <br>
102
+          <textarea name="publickey" id="publickey" spellcheck="false" cols="64" rows="8"><?=display_str($PublicKey)?></textarea>
103
+          <br>
104
+          <button type="submit" name="type" value="PGP">Update Public Key</button>
105
+        </form>
106
+      </div>
107
+    </div>
108
+    <div class="box">
109
+      <div class="head">
110
+        <strong>Two-Factor Authentication (2FA-TOTP)</strong>
111
+      </div>
112
+      <div class="pad">
113
+<?    $TwoFASecret = empty($TwoFactor) ? $TwoFA->createSecret() : $TwoFactor;
114
+      if (empty($TwoFactor)) {
115
+        if (sizeof($U2FRegs) == 0) { ?>
116
+          <p>Two Factor Authentication is not currently enabled for this account.</p>
117
+          <p>To enable it, add the secret key below to your 2FA client either manually or by scanning the QR code, then enter a verification code generated by your 2FA client and click the "Enable 2FA" button.</p>
118
+          <form method="post">
119
+            <input type="text" size="60" name="twofasecret" id="twofasecret" value="<?=$TwoFASecret?>" readonly><br>
120
+            <img src="<?=$TwoFA->getQRCodeImageAsDataUri(SITE_NAME, $TwoFASecret)?>"><br>
121
+            <input type="text" size="20" maxlength="6" pattern="[0-9]{0,6}" name="twofa" id="twofa" placeholder="Verification Code" autocomplete="off"><br><br>
122
+            <button type="submit" name="type" value="2FA-E">Enable 2FA</button>
123
+          </form>
124
+<?      } else { ?>
125
+          <p>Two Factor Authentication is not currently enabled for this account.</p>
126
+          <p>To enable 2FA, you must first disable U2F below.</p>
127
+<?      }
128
+      } else {?>
129
+        <form method="post">
130
+          <input type="hidden" name="type" value="2FA-D">
131
+          <p>2FA is enabled for this account with the following secret:</p>
132
+          <input type="text" size="20" name="twofasecret" id="twofasecret" value="<?=$TwoFASecret?>" readonly><br>
133
+          <img src="<?=$TwoFA->getQRCodeImageAsDataUri(SITE_NAME, $TwoFASecret)?>"><br><br>
134
+          <p>To disable 2FA, click the button below.</p>
135
+          <button type="submit" name="type" value="2FA-D">Disable 2FA</button>
136
+        </form>
137
+<?    } ?>
138
+      </div>
139
+    </div>
140
+    <div class="box">
141
+      <div class="head">
142
+        <strong>Universal Two Factor (FIDO U2F)</strong>
143
+      </div>
144
+      <div class="pad">
145
+<?    if (sizeof($U2FRegs) == 0) { ?>
146
+<?      if (empty($TwoFactor)) { ?>
147
+          <form method="post" id="u2f_register_form">
148
+            <input type="hidden" name="u2f-request" value='<?=json_encode($U2FRequest)?>'>
149
+            <input type="hidden" name="u2f-sigs" value='<?=json_encode($U2FSigs)?>'>
150
+            <input type="hidden" name="u2f-response">
151
+            <input type="hidden" value="U2F-E">
152
+          </form>
153
+          <p>Universal Two Factor is not currently enabled for this account.</p>
154
+          <p>To enable Universal Two Factor, plug in your U2F token and press the button on it.</p>
155
+<?      } else { ?>
156
+          <p>Universal Two Factor is not currently enabled for this account.</p>
157
+          <p>To enable Universal Two Factor, you must first disable normal 2FA above.</p>
158
+<?      } ?>
159
+<?    } else { ?>
160
+        <form method="post" id="u2f_register_form">
161
+          <input type="hidden" name="u2f-request" value='<?=json_encode($U2FRequest)?>'>
162
+          <input type="hidden" name="u2f-sigs" value='<?=json_encode($U2FSigs)?>'>
163
+          <input type="hidden" name="u2f-response">
164
+          <input type="hidden" value="U2F-E">
165
+        </form>
166
+        <p>Universal Two Factor is enabled.</p>
167
+        <p>To add an additional U2F token, plug it in and press the button on it</p>
168
+        <p>To disable U2F completely and deregister all tokens, press the button below</p>
169
+        <button type="submit" name="type" value="U2F-D">Disable U2F</button>
170
+<?    } ?>
171
+      </div>
172
+    </div>
173
+</div>
174
+<?
175
+View::show_footer();
176
+?>

+ 6
- 31
sections/user/edit.php View File

@@ -84,7 +84,7 @@ echo $Val->GenerateJS('userform');
84 84
   <div class="header">
85 85
     <h2><?=Users::format_username($UserID, false, false, false)?> &gt; Settings</h2>
86 86
   </div>
87
-  <form class="edit_form" name="user" id="userform" action="" method="post" autocomplete="off">
87
+  <form class="edit_form" name="user" id="userform" method="post" autocomplete="off">
88 88
   <div class="sidebar settings_sidebar">
89 89
     <div class="box box2" id="settings_sections">
90 90
       <div class="head">
@@ -739,13 +739,17 @@ list($ArtistsAdded) = $DB->next_record();
739 739
           <strong>Access Settings</strong>
740 740
         </td>
741 741
       </tr>
742
+      <tr id="acc_2fa_tr">
743
+        <td class="label tooltip" title="This page contains 2FA, U2F, and PGP settings"><strong>Account Security</strong></td>
744
+        <td><a href="user.php?action=2fa">Click here to view additional account security options</a></td>
745
+      </tr>
742 746
       <tr id="acc_currentpassword_tr">
743 747
         <td class="label"><strong>Current Password</strong></td>
744 748
         <td>
745 749
           <div class="field_div">
746 750
             <input type="password" size="40" name="cur_pass" id="cur_pass" maxlength="307200" value="" />
747 751
           </div>
748
-          <strong class="important_text">When changing any of the settings in this section, you must enter your current password in this field before saving your changes</strong>
752
+          <strong class="important_text">When changing any of the settings below, you must enter your current password in this field before saving your changes</strong>
749 753
         </td>
750 754
       </tr>
751 755
       <tr id="acc_resetpk_tr">
@@ -779,35 +783,6 @@ list($ArtistsAdded) = $DB->next_record();
779 783
           </div>
780 784
         </td>
781 785
       </tr>
782
-      <tr id="acc_publickey_tr">
783
-        <td class="label tooltip" title="This is your PGP public key. The matching private key can be used to prove ownership of your account should you lose access to your password or 2FA key. Only add a PGP key if you have taken proper precautions like creating a revocation certificate"><strong>PGP Public Key</strong></td>
784
-        <td>
785
-          <div class="field_div">
786
-            <textarea name="publickey" id="publickey" cols="64" rows="8"><?=display_str($PublicKey)?></textarea>
787
-          </div>
788
-        </td>
789
-      </tr>
790
-      <tr id="acc_2fa_tr">
791
-        <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>
792
-        <td>
793
-          <? $TwoFASecret = empty($TwoFactor) ? $TwoFA->createSecret() : $TwoFactor; ?>
794
-          <div class="field_div">
795
-            <? if (!empty($TwoFactor)) { ?>
796
-            <p class="min_padding">2FA is enabled for this account with the following secret:</p>
797
-            <? } ?>
798
-            <img src="<?=$TwoFA->getQRCodeImageAsDataUri(SITE_NAME, $TwoFASecret)?>"><br>
799
-            <input type="text" size="20" name="twofasecret" id="twofasecret" value="<?=$TwoFASecret?>" readonly><br>
800
-            <? if (empty($TwoFactor)) { ?>
801
-            <input type="text" size="20" maxlength="6" name="twofa" id="twofa" placeholder="Verification Code">
802
-            <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>
803
-            <p class="min_padding"><strong class="important_text">WARNING</strong>: Losing your 2FA key can make your account unrecoverable. Only enable it if you're sure you can handle it.
804
-            <? } else { ?>
805
-            <label><input type="checkbox" name="disable2fa" id="disable2fa" />
806
-            Disable 2FA</label>
807
-            <? } ?>
808
-          </div>
809
-        </td>
810
-      </tr>
811 786
       <tr id="acc_password_tr">
812 787
         <td class="label"><strong>Password</strong></td>
813 788
         <td>

+ 4
- 1
sections/user/index.php View File

@@ -45,8 +45,11 @@ switch ($_REQUEST['action']) {
45 45
   case 'take_edit':
46 46
     include('take_edit.php');
47 47
     break;
48
+  case '2fa':
49
+    include('2fa.php');
50
+    break;
48 51
   case 'invitetree':
49
-    include(SERVER_ROOT.'/sections/user/invitetree.php');
52
+    include('invitetree.php');
50 53
     break;
51 54
   case 'invite':
52 55
     include('invite.php');

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

@@ -29,7 +29,6 @@ $Val->SetFields('postsperpage', 1, "number", "You forgot to select your posts pe
29 29
 $Val->SetFields('collagecovers', 1, "number", "You forgot to select your collage option.");
30 30
 $Val->SetFields('avatar', 0, "regex", "You did not enter a valid avatar URL.", ['regex' => "/^".IMAGE_REGEX."$/i"]);
31 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.", ['regex' => '/^[0-9]{6}$/']);
33 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.", ['minlength' => 6, 'maxlength' => 32]);
34 33
 $Val->SetFields('new_pass_1', 0, "regex", "You did not enter a valid password. A valid password is 6 characters or longer.", ['regex' => '/(?=^.{6,}$).*$/']);
35 34
 $Val->SetFields('new_pass_2', 1, "compare", "Your passwords do not match.", ['comparefield' => 'new_pass_1']);
@@ -134,10 +133,10 @@ if (isset($_POST['p_donor_stats'])) {
134 133
 // End building $Paranoia
135 134
 
136 135
 $DB->query("
137
-  SELECT Email, PassHash, TwoFactor, PublicKey, IRCKey
136
+  SELECT Email, PassHash, IRCKey
138 137
   FROM users_main
139 138
   WHERE ID = $UserID");
140
-list($CurEmail, $CurPassHash, $CurTwoFA, $CurPublicKey, $CurIRCKey) = $DB->next_record();
139
+list($CurEmail, $CurPassHash, $CurIRCKey) = $DB->next_record();
141 140
 
142 141
 function require_password($Setting = false) {
143 142
   global $CurPassHash;
@@ -175,39 +174,6 @@ if ($CurEmail != $_POST['email']) {
175 174
 
176 175
 }
177 176
 
178
-// PGP Key
179
-if ($CurPublicKey != $_POST['publickey']) {
180
-  require_password("Change Public Key");
181
-  $DB->query("
182
-    UPDATE users_main
183
-    SET PublicKey = '".db_string($_POST['publickey'])."'
184
-    WHERE ID = $UserID");
185
-}
186
-
187
-// 2FA activation
188
-if (!empty($_POST['twofa']) && empty($CurTwoFA)) {
189
-  require_password("Enable 2-Factor");
190
-  require_once SERVER_ROOT.'/classes/twofa.class.php';
191
-  $TwoFA = new TwoFactorAuth(SITE_NAME);
192
-  if ($TwoFA->verifyCode($_POST['twofasecret'], $_POST['twofa'])) {
193
-    $DB->query("
194
-      UPDATE users_main
195
-      SET TwoFactor='".db_string($_POST['twofasecret'])."'
196
-      WHERE ID = $UserID");
197
-  } else {
198
-    error('Invalid 2FA verification code.');
199
-  }
200
-}
201
-
202
-// 2FA deactivation
203
-if (isset($_POST['disable2fa'])) {
204
-  require_password("Disable 2-Factor");
205
-  $DB->query("
206
-    UPDATE users_main
207
-    SET TwoFactor = NULL
208
-    WHERE ID = $UserID");
209
-}
210
-
211 177
 if (!empty($_POST['new_pass_1']) && !empty($_POST['new_pass_2'])) {
212 178
   require_password("Change Password");
213 179
   $ResetPassword = true;

+ 500
- 0
static/functions/u2f.js View File

@@ -0,0 +1,500 @@
1
+// Modified U2F client API by Google (https://demo.yubico.com/js/u2f-api.js)
2
+
3
+// Copyright 2014-2015 Google Inc. All rights reserved.
4
+//
5
+// Use of this source code is governed by a BSD-style
6
+// license that can be found in the LICENSE file or at
7
+// https://developers.google.com/open-source/licenses/bsd
8
+
9
+'use strict';
10
+
11
+// Namespace for the U2F api.
12
+var u2f = u2f || {};
13
+
14
+// The U2F extension id
15
+u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd';
16
+
17
+// Message types for messsages to/from the extension
18
+u2f.MessageTypes = {
19
+    'U2F_REGISTER_REQUEST': 'u2f_register_request',
20
+    'U2F_SIGN_REQUEST': 'u2f_sign_request',
21
+    'U2F_REGISTER_RESPONSE': 'u2f_register_response',
22
+    'U2F_SIGN_RESPONSE': 'u2f_sign_response'
23
+};
24
+
25
+// Response status codes
26
+u2f.ErrorCodes = {
27
+    'OK': 0,
28
+    'OTHER_ERROR': 1,
29
+    'BAD_REQUEST': 2,
30
+    'CONFIGURATION_UNSUPPORTED': 3,
31
+    'DEVICE_INELIGIBLE': 4,
32
+    'TIMEOUT': 5
33
+};
34
+
35
+// A message type for registration requests
36
+u2f.Request;
37
+
38
+// A message for registration responses
39
+u2f.Response;
40
+
41
+// An error object for responses
42
+u2f.Error;
43
+
44
+// Data object for a single sign request.
45
+u2f.SignRequest;
46
+
47
+// Data object for a sign response.
48
+u2f.SignResponse;
49
+
50
+// Data object for a registration request.
51
+u2f.RegisterRequest;
52
+
53
+// Data object for a registration response.
54
+u2f.RegisterResponse;
55
+
56
+
57
+// Low level MessagePort API support
58
+
59
+// Sets up a MessagePort to the U2F extension using the available mechanisms.
60
+u2f.getMessagePort = function(callback) {
61
+    if (typeof chrome != 'undefined' && chrome.runtime) {
62
+        // The actual message here does not matter, but we need to get a reply
63
+        // for the callback to run. Thus, send an empty signature request
64
+        // in order to get a failure response.
65
+        var msg = {
66
+            type: u2f.MessageTypes.U2F_SIGN_REQUEST,
67
+            signRequests: []
68
+        };
69
+        chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() {
70
+            if (!chrome.runtime.lastError) {
71
+                // We are on a whitelisted origin and can talk directly
72
+                // with the extension.
73
+                u2f.getChromeRuntimePort_(callback);
74
+            } else {
75
+                // chrome.runtime was available, but we couldn't message
76
+                // the extension directly, use iframe
77
+                u2f.getIframePort_(callback);
78
+            }
79
+        });
80
+    } else if (u2f.isAndroidChrome_()) {
81
+        u2f.getAuthenticatorPort_(callback);
82
+    } else {
83
+        // chrome.runtime was not available at all, which is normal
84
+        // when this origin doesn't have access to any extensions.
85
+        u2f.getIframePort_(callback);
86
+    }
87
+};
88
+
89
+// Detect chrome running on android based on the browser's useragent.
90
+u2f.isAndroidChrome_ = function() {
91
+    var userAgent = navigator.userAgent;
92
+    return userAgent.indexOf('Chrome') != -1 &&
93
+        userAgent.indexOf('Android') != -1;
94
+};
95
+
96
+// Connects directly to the extension via chrome.runtime.connect
97
+u2f.getChromeRuntimePort_ = function(callback) {
98
+    var port = chrome.runtime.connect(u2f.EXTENSION_ID,
99
+        {'includeTlsChannelId': true});
100
+    setTimeout(function() {
101
+        callback(new u2f.WrappedChromeRuntimePort_(port));
102
+    }, 0);
103
+};
104
+
105
+// Return a 'port' abstraction to the Authenticator app.
106
+u2f.getAuthenticatorPort_ = function(callback) {
107
+    setTimeout(function() {
108
+        callback(new u2f.WrappedAuthenticatorPort_());
109
+    }, 0);
110
+};
111
+
112
+// A wrapper for chrome.runtime.Port that is compatible with MessagePort.
113
+u2f.WrappedChromeRuntimePort_ = function(port) {
114
+    this.port_ = port;
115
+};
116
+
117
+// Format a return a sign request.
118
+u2f.WrappedChromeRuntimePort_.prototype.formatSignRequest_ =
119
+    function(signRequests, timeoutSeconds, reqId) {
120
+        return {
121
+            type: u2f.MessageTypes.U2F_SIGN_REQUEST,
122
+            signRequests: signRequests,
123
+            timeoutSeconds: timeoutSeconds,
124
+            requestId: reqId
125
+        };
126
+    };
127
+
128
+// Format a return a register request.
129
+u2f.WrappedChromeRuntimePort_.prototype.formatRegisterRequest_ =
130
+    function(signRequests, registerRequests, timeoutSeconds, reqId) {
131
+        return {
132
+            type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
133
+            signRequests: signRequests,
134
+            registerRequests: registerRequests,
135
+            timeoutSeconds: timeoutSeconds,
136
+            requestId: reqId
137
+        };
138
+    };
139
+
140
+// Posts a message on the underlying channel.
141
+u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) {
142
+    this.port_.postMessage(message);
143
+};
144
+
145
+// Emulates the HTML 5 addEventListener interface. Works only for the
146
+// onmessage event, which is hooked up to the chrome.runtime.Port.onMessage.
147
+u2f.WrappedChromeRuntimePort_.prototype.addEventListener =
148
+    function(eventName, handler) {
149
+        var name = eventName.toLowerCase();
150
+        if (name == 'message' || name == 'onmessage') {
151
+            this.port_.onMessage.addListener(function(message) {
152
+                // Emulate a minimal MessageEvent object
153
+                handler({'data': message});
154
+            });
155
+        } else {
156
+            console.error('WrappedChromeRuntimePort only supports onMessage');
157
+        }
158
+    };
159
+
160
+// Wrap the Authenticator app with a MessagePort interface.
161
+u2f.WrappedAuthenticatorPort_ = function() {
162
+    this.requestId_ = -1;
163
+    this.requestObject_ = null;
164
+}
165
+
166
+// Launch the Authenticator intent.
167
+u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) {
168
+    var intentLocation = /** @type {string} */ (message);
169
+    document.location = intentLocation;
170
+};
171
+
172
+// Emulates the HTML5 addEventListener interface.
173
+u2f.WrappedAuthenticatorPort_.prototype.addEventListener =
174
+    function(eventName, handler) {
175
+        var name = eventName.toLowerCase();
176
+        if (name == 'message') {
177
+            var self = this;
178
+            /* Register a callback to that executes when
179
+             * chrome injects the response. */
180
+            window.addEventListener(
181
+                'message', self.onRequestUpdate_.bind(self, handler), false);
182
+        } else {
183
+            console.error('WrappedAuthenticatorPort only supports message');
184
+        }
185
+    };
186
+
187
+// Callback invoked  when a response is received from the Authenticator.
188
+u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ =
189
+    function(callback, message) {
190
+        var messageObject = JSON.parse(message.data);
191
+        var intentUrl = messageObject['intentURL'];
192
+
193
+        var errorCode = messageObject['errorCode'];
194
+        var responseObject = null;
195
+        if (messageObject.hasOwnProperty('data')) {
196
+            responseObject = /** @type {Object} */ (
197
+                JSON.parse(messageObject['data']));
198
+            responseObject['requestId'] = this.requestId_;
199
+        }
200
+
201
+        /* Sign responses from the authenticator do not conform to U2F,
202
+         * convert to U2F here. */
203
+        responseObject = this.doResponseFixups_(responseObject);
204
+        callback({'data': responseObject});
205
+    };
206
+
207
+// Fixup the response provided by the Authenticator to conform with the U2F spec.
208
+u2f.WrappedAuthenticatorPort_.prototype.doResponseFixups_ =
209
+    function(responseObject) {
210
+        if (responseObject.hasOwnProperty('responseData')) {
211
+            return responseObject;
212
+        } else if (this.requestObject_['type'] != u2f.MessageTypes.U2F_SIGN_REQUEST) {
213
+            // Only sign responses require fixups.  If this is not a response
214
+            // to a sign request, then an internal error has occurred.
215
+            return {
216
+                'type': u2f.MessageTypes.U2F_REGISTER_RESPONSE,
217
+                'responseData': {
218
+                    'errorCode': u2f.ErrorCodes.OTHER_ERROR,
219
+                    'errorMessage': 'Internal error: invalid response from Authenticator'
220
+                }
221
+            };
222
+        }
223
+
224
+        /* Non-conformant sign response, do fixups. */
225
+        var encodedChallengeObject = responseObject['challenge'];
226
+        if (typeof encodedChallengeObject !== 'undefined') {
227
+            var challengeObject = JSON.parse(atob(encodedChallengeObject));
228
+            var serverChallenge = challengeObject['challenge'];
229
+            var challengesList = this.requestObject_['signData'];
230
+            var requestChallengeObject = null;
231
+            for (var i = 0; i < challengesList.length; i++) {
232
+                var challengeObject = challengesList[i];
233
+                if (challengeObject['keyHandle'] == responseObject['keyHandle']) {
234
+                    requestChallengeObject = challengeObject;
235
+                    break;
236
+                }
237
+            }
238
+        }
239
+        var responseData = {
240
+            'errorCode': responseObject['resultCode'],
241
+            'keyHandle': responseObject['keyHandle'],
242
+            'signatureData': responseObject['signature'],
243
+            'clientData': encodedChallengeObject
244
+        };
245
+        return {
246
+            'type': u2f.MessageTypes.U2F_SIGN_RESPONSE,
247
+            'responseData': responseData,
248
+            'requestId': responseObject['requestId']
249
+        }
250
+    };
251
+
252
+// Base URL for intents to Authenticator.
253
+u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ =
254
+    'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE';
255
+
256
+// Format a return a sign request.
257
+u2f.WrappedAuthenticatorPort_.prototype.formatSignRequest_ =
258
+    function(signRequests, timeoutSeconds, reqId) {
259
+        if (!signRequests || signRequests.length == 0) {
260
+            return null;
261
+        }
262
+        /* TODO(fixme): stash away requestId, as the authenticator app does
263
+         * not return it for sign responses. */
264
+        this.requestId_ = reqId;
265
+        /* TODO(fixme): stash away the signRequests, to deal with the legacy
266
+         * response format returned by the Authenticator app. */
267
+        this.requestObject_ = {
268
+            'type': u2f.MessageTypes.U2F_SIGN_REQUEST,
269
+            'signData': signRequests,
270
+            'requestId': reqId,
271
+            'timeout': timeoutSeconds
272
+        };
273
+
274
+        var appId = signRequests[0]['appId'];
275
+        var intentUrl =
276
+            u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
277
+            ';S.appId=' + encodeURIComponent(appId) +
278
+            ';S.eventId=' + reqId +
279
+            ';S.challenges=' +
280
+            encodeURIComponent(
281
+                JSON.stringify(this.getBrowserDataList_(signRequests))) + ';end';
282
+        return intentUrl;
283
+    };
284
+
285
+// Get the browser data objects from the challenge list
286
+u2f.WrappedAuthenticatorPort_
287
+    .prototype.getBrowserDataList_ = function(challenges) {
288
+    return challenges
289
+        .map(function(challenge) {
290
+            var browserData = {
291
+                'typ': 'navigator.id.getAssertion',
292
+                'challenge': challenge['challenge']
293
+            };
294
+            var challengeObject = {
295
+                'challenge' : browserData,
296
+                'keyHandle' : challenge['keyHandle']
297
+            };
298
+            return challengeObject;
299
+        });
300
+};
301
+
302
+// Format a return a register request.
303
+u2f.WrappedAuthenticatorPort_.prototype.formatRegisterRequest_ =
304
+    function(signRequests, enrollChallenges, timeoutSeconds, reqId) {
305
+        if (!enrollChallenges || enrollChallenges.length == 0) {
306
+            return null;
307
+        }
308
+        // Assume the appId is the same for all enroll challenges.
309
+        var appId = enrollChallenges[0]['appId'];
310
+        var registerRequests = [];
311
+        for (var i = 0; i < enrollChallenges.length; i++) {
312
+            var registerRequest = {
313
+                'challenge': enrollChallenges[i]['challenge'],
314
+                'version': enrollChallenges[i]['version']
315
+            };
316
+            if (enrollChallenges[i]['appId'] != appId) {
317
+                // Only include the appId when it differs from the first appId.
318
+                registerRequest['appId'] = enrollChallenges[i]['appId'];
319
+            }
320
+            registerRequests.push(registerRequest);
321
+        }
322
+        var registeredKeys = [];
323
+        if (signRequests) {
324
+            for (i = 0; i < signRequests.length; i++) {
325
+                var key = {
326
+                    'keyHandle': signRequests[i]['keyHandle'],
327
+                    'version': signRequests[i]['version']
328
+                };
329
+                // Only include the appId when it differs from the appId that's
330
+                // being registered now.
331
+                if (signRequests[i]['appId'] != appId) {
332
+                    key['appId'] = signRequests[i]['appId'];
333
+                }
334
+                registeredKeys.push(key);
335
+            }
336
+        }
337
+        var request = {
338
+            'type': u2f.MessageTypes.U2F_REGISTER_REQUEST,
339
+            'appId': appId,
340
+            'registerRequests': registerRequests,
341
+            'registeredKeys': registeredKeys,
342
+            'requestId': reqId,
343
+            'timeoutSeconds': timeoutSeconds
344
+        };
345
+        var intentUrl =
346
+            u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
347
+            ';S.request=' + encodeURIComponent(JSON.stringify(request)) +
348
+            ';end';
349
+        /* TODO(fixme): stash away requestId, this is is not necessary for
350
+         * register requests, but here to keep parity with sign.
351
+         */
352
+        this.requestId_ = reqId;
353
+        return intentUrl;
354
+    };
355
+
356
+
357
+// Sets up an embedded trampoline iframe, sourced from the extension.
358
+u2f.getIframePort_ = function(callback) {
359
+    // Create the iframe
360
+    var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID;
361
+    var iframe = document.createElement('iframe');
362
+    iframe.src = iframeOrigin + '/u2f-comms.html';
363
+    iframe.setAttribute('style', 'display:none');
364
+    document.body.appendChild(iframe);
365
+
366
+    var channel = new MessageChannel();
367
+    var ready = function(message) {
368
+        if (message.data == 'ready') {
369
+            channel.port1.removeEventListener('message', ready);
370
+            callback(channel.port1);
371
+        } else {
372
+            console.error('First event on iframe port was not "ready"');
373
+        }
374
+    };
375
+    channel.port1.addEventListener('message', ready);
376
+    channel.port1.start();
377
+
378
+    iframe.addEventListener('load', function() {
379
+        // Deliver the port to the iframe and initialize
380
+        iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]);
381
+    });
382
+};
383
+
384
+
385
+// High-level JS API
386
+
387
+// Default extension response timeout in seconds.
388
+u2f.EXTENSION_TIMEOUT_SEC = 30;
389
+
390
+// A singleton instance for a MessagePort to the extension.
391
+u2f.port_ = null;
392
+
393
+// Callbacks waiting for a port
394
+u2f.waitingForPort_ = [];
395
+
396
+// A counter for requestIds.
397
+u2f.reqCounter_ = 0;
398
+
399
+// A map from requestIds to client callbacks
400
+u2f.callbackMap_ = {};
401
+
402
+// Creates or retrieves the MessagePort singleton to use.
403
+u2f.getPortSingleton_ = function(callback) {
404
+    if (u2f.port_) {
405
+        callback(u2f.port_);
406
+    } else {
407
+        if (u2f.waitingForPort_.length == 0) {
408
+            u2f.getMessagePort(function(port) {
409
+                u2f.port_ = port;
410
+                u2f.port_.addEventListener('message',
411
+                    /** @type {function(Event)} */ (u2f.responseHandler_));
412
+
413
+                // Careful, here be async callbacks. Maybe.
414
+                while (u2f.waitingForPort_.length)
415
+                    u2f.waitingForPort_.shift()(u2f.port_);
416
+            });
417
+        }
418
+        u2f.waitingForPort_.push(callback);
419
+    }
420
+};
421
+
422
+// Handles response messages from the extension.
423
+u2f.responseHandler_ = function(message) {
424
+    var response = message.data;
425
+    var reqId = response['requestId'];
426
+    if (!reqId || !u2f.callbackMap_[reqId]) {
427
+        console.error('Unknown or missing requestId in response.');
428
+        return;
429
+    }
430
+    var cb = u2f.callbackMap_[reqId];
431
+    delete u2f.callbackMap_[reqId];
432
+    cb(response['responseData']);
433
+};
434
+
435
+// Dispatches an array of sign requests to available U2F tokens.
436
+u2f.sign = function(signRequests, callback, opt_timeoutSeconds) {
437
+    u2f.getPortSingleton_(function(port) {
438
+        var reqId = ++u2f.reqCounter_;
439
+        u2f.callbackMap_[reqId] = callback;
440
+        var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
441
+            opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
442
+        var req = port.formatSignRequest_(signRequests, timeoutSeconds, reqId);
443
+        port.postMessage(req);
444
+    });
445
+};
446
+
447
+// Dispatches register requests to available U2F tokens. An array of sign
448
+// requests identifies already registered tokens.
449
+u2f.register = function(registerRequests, signRequests,
450
+                        callback, opt_timeoutSeconds) {
451
+    u2f.getPortSingleton_(function(port) {
452
+        var reqId = ++u2f.reqCounter_;
453
+        u2f.callbackMap_[reqId] = callback;
454
+        var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
455
+            opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
456
+        var req = port.formatRegisterRequest_(
457
+            signRequests, registerRequests, timeoutSeconds, reqId);
458
+        port.postMessage(req);
459
+    });
460
+};
461
+
462
+// Everything below is Gazelle-specific and not part of the u2f API
463
+
464
+function initU2F() {
465
+  if (document.querySelector('#u2f_register_form') && document.querySelector('[name="u2f-request"]')) {
466
+    var req = JSON.parse($('[name="u2f-request"]').raw().value);
467
+    var sigs = JSON.parse($('[name="u2f-sigs"]').raw().value);
468
+    if (req) {
469
+      u2f.register([req], sigs, function(data) {
470
+        if (data.errorCode) {
471
+          console.log('U2F Register Error: ' + Object.keys(u2f.ErrorCodes).find(k => u2f.ErrorCodes[k] == data.errorCode));
472
+          return;
473
+        }
474
+        $('[name="u2f-response"]').raw().value = JSON.stringify(data);
475
+        $('[value="U2F-E"]').raw().name = 'type'
476
+        $('#u2f_register_form').submit();
477
+      }, 3600)
478
+    }
479
+  }
480
+
481
+  if (document.querySelector('#u2f_sign_form') && document.querySelector('[name="u2f-request"]')) {
482
+    var req = JSON.parse($('[name="u2f-request"]').raw().value);
483
+    if (req) {
484
+      u2f.sign(req, function(data) {
485
+        if (data.errorCode) {
486
+          console.log('U2F Sign Error: ' + Object.keys(u2f.ErrorCodes).find(k => u2f.ErrorCodes[k] == data.errorCode));
487
+          return;
488
+        }
489
+        $('[name="u2f-response"]').raw().value = JSON.stringify(data);
490
+        $('#u2f_sign_form').submit();
491
+      }, 3600)
492
+    }
493
+  }
494
+}
495
+
496
+if (document.readyState != 'loading') {
497
+  initU2F();
498
+} else {
499
+  document.addEventListener("DOMContentLoaded", initU2F);
500
+}

+ 13
- 5
static/styles/beluga/style.css View File

@@ -503,6 +503,14 @@ p.min_padding {
503 503
   vertical-align: middle
504 504
 }
505 505
 
506
+.alertbar.error {
507
+  background-color: #c73f3f;
508
+}
509
+
510
+.alertbar.warning {
511
+  background-color: #c58b09;
512
+}
513
+
506 514
 .alertbar a {
507 515
   display: inline-block;
508 516
   position: relative;
@@ -512,7 +520,11 @@ p.min_padding {
512 520
   text-decoration: underline
513 521
 }
514 522
 
515
-#alerts a, .alertbar a,.torrent_table .group a,.ui-state-hover,a.ui-state-hover,li.ui-state-hover {
523
+.alertbar.modbar > a {
524
+  margin: 0 5px;
525
+}
526
+
527
+.torrent_table .group a, .ui-state-hover, a.ui-state-hover, li.ui-state-hover {
516 528
   color: #FFF;
517 529
 }
518 530
 
@@ -521,10 +533,6 @@ p.min_padding {
521 533
   text-decoration: none;
522 534
 }
523 535
 
524
-.alertbar {
525
-  color: transparent;
526
-}
527
-
528 536
 .alertbar a[href="news.php"] {
529 537
   background: linear-gradient(#045362,#0d5968);
530 538
 }

+ 16
- 12
static/styles/genaviv/style.css View File

@@ -111,10 +111,13 @@ span.r99,.r09,.r10,.r20,.r30,.r40,.r50 {
111 111
   border-bottom:2px solid #45847e!important
112 112
 }
113 113
 #header>#menu ul>li.active>a {
114
-  color:#32867d
114
+  color:#32867d;
115 115
 }
116
-#header .alertbar a {
117
-  color:#32867d
116
+.alertbar a {
117
+  color:#32867d;
118
+}
119
+.alertbar.modbar a {
120
+  margin: 0px 5px;
118 121
 }
119 122
 .box.filter_torrents .head,.colhead td,.colhead_dark td,tr.colhead,tr.colhead_dark,#inbox .box .head,#inbox .box .head,#reply_box h3,#inbox form .send_form #quickpost h3,.sidebar .box .head,.main_column .box .head.colhead_dark,.box.news_post .head,tr.colhead,tr.colhead_dark,.head.colhead_dark,.main_column .box .head {
120 123
   background:#5aada5;
@@ -200,7 +203,16 @@ span.last_read {
200 203
   text-shadow:none!important;color:inherit!important;font-size:inherit!important;font-family:inherit!important;
201 204
 }
202 205
 .alertbar {
203
-  padding:7px
206
+  text-align:center;
207
+  padding:10px;
208
+  background:#D0D0D0;
209
+  text-align:center;
210
+}
211
+.alertbar.warning {
212
+  color:#cc7100;
213
+}
214
+.alertbar.error {
215
+  color:#ff0000;
204 216
 }
205 217
 .sidebar .stats.nobullet a {
206 218
   padding:3px 6px
@@ -828,9 +840,6 @@ div#alerts {
828 840
 .forum-post__heading,.forum_post tr.colhead_dark,.forum_post .colhead_dark td {
829 841
   max-height:32px;padding-top:0;padding-bottom:0;height:32px;line-height:32px;
830 842
 }
831
-.alertbar {
832
-  text-align:center
833
-}
834 843
 .colhead td,.colhead_dark td,#inbox .box .head,.box.filter_torrents .head,#reply_box h3,tr .colhead_dark td,.sidebar .box .head,.main_column .box .head.colhead_dark,.box.news_post .head,tr.colhead,tr.colhead_dark,.head.colhead_dark,.main_column .box .head {
835 844
 
836 845
 }
@@ -1427,11 +1436,6 @@ div.noty_bar {
1427 1436
 table#torrent_table strong {
1428 1437
   color:#313131
1429 1438
 }
1430
-.alertbar {
1431
-  padding:10px;
1432
-  background:#D0D0D0;
1433
-  text-align:center;
1434
-}
1435 1439
 .box.filter_torrents .head,.colhead td,.colhead_dark td,tr.colhead,tr.colhead_dark,#inbox .box .head,#inbox .box .head,#reply_box h3,#inbox form .send_form #quickpost h3,.sidebar .box .head,.main_column .box .head.colhead_dark,.box.news_post .head,tr.colhead,tr.colhead_dark,.head.colhead_dark,.main_column .box .head {
1436 1440
   color:#F7F7F7;
1437 1441
 }

+ 5
- 0
static/styles/global.css View File

@@ -934,6 +934,11 @@ input[type="search"] {
934 934
   max-width: 100%;
935 935
 }
936 936
 
937
+#publickey {
938
+  width: initial;
939
+  font-family: monospace;
940
+}
941
+
937 942
 .hidden {
938 943
   display: none;
939 944
 }

+ 10
- 1
static/styles/oppai/style.css View File

@@ -360,7 +360,6 @@ ul.thin li { margin:0px 0px; padding:0px; }
360 360
 }
361 361
 
362 362
 .alertbar {
363
-  /* border: 1px solid #999; */
364 363
   background-color: #fbc2e5;
365 364
   text-align: center;
366 365
   color: #444;
@@ -369,11 +368,21 @@ ul.thin li { margin:0px 0px; padding:0px; }
369 368
   width: 350px;
370 369
   margin: 0 auto 0px auto;
371 370
   padding: 10px;
371
+  margin-bottom: 8px;
372
+}
373
+.alertbar.warning {
374
+  background-color: #ffe68a;
375
+}
376
+.alertbar.error {
377
+  background-color: #ff8a8a;
372 378
 }
373 379
 .alertbar a {
374 380
   color: #555;
375 381
   text-decoration: underline;
376 382
 }
383
+.alertbar.modbar a {
384
+  margin: 0px 5px;
385
+}
377 386
 .alertbar a:hover {
378 387
   color: #777;
379 388
   text-decoration: none;

Loading…
Cancel
Save