Passed
Push — master ( 65fd24...fc096a )
by Joas
17:08 queued 14s
created
lib/private/Authentication/Token/PublicKeyTokenProvider.php 2 patches
Indentation   +444 added lines, -444 removed lines patch added patch discarded remove patch
@@ -46,448 +46,448 @@
 block discarded – undo
46 46
 use Psr\Log\LoggerInterface;
47 47
 
48 48
 class PublicKeyTokenProvider implements IProvider {
49
-	use TTransactional;
50
-
51
-	/** @var PublicKeyTokenMapper */
52
-	private $mapper;
53
-
54
-	/** @var ICrypto */
55
-	private $crypto;
56
-
57
-	/** @var IConfig */
58
-	private $config;
59
-
60
-	private IDBConnection $db;
61
-
62
-	/** @var LoggerInterface */
63
-	private $logger;
64
-
65
-	/** @var ITimeFactory */
66
-	private $time;
67
-
68
-	/** @var CappedMemoryCache */
69
-	private $cache;
70
-
71
-	private IHasher $hasher;
72
-
73
-	public function __construct(PublicKeyTokenMapper $mapper,
74
-								ICrypto $crypto,
75
-								IConfig $config,
76
-								IDBConnection $db,
77
-								LoggerInterface $logger,
78
-								ITimeFactory $time,
79
-								IHasher $hasher) {
80
-		$this->mapper = $mapper;
81
-		$this->crypto = $crypto;
82
-		$this->config = $config;
83
-		$this->db = $db;
84
-		$this->logger = $logger;
85
-		$this->time = $time;
86
-
87
-		$this->cache = new CappedMemoryCache();
88
-		$this->hasher = $hasher;
89
-	}
90
-
91
-	/**
92
-	 * {@inheritDoc}
93
-	 */
94
-	public function generateToken(string $token,
95
-								  string $uid,
96
-								  string $loginName,
97
-								  ?string $password,
98
-								  string $name,
99
-								  int $type = IToken::TEMPORARY_TOKEN,
100
-								  int $remember = IToken::DO_NOT_REMEMBER): IToken {
101
-		if (mb_strlen($name) > 128) {
102
-			$name = mb_substr($name, 0, 120) . '…';
103
-		}
104
-
105
-		$dbToken = $this->newToken($token, $uid, $loginName, $password, $name, $type, $remember);
106
-		$this->mapper->insert($dbToken);
107
-
108
-		// Add the token to the cache
109
-		$this->cache[$dbToken->getToken()] = $dbToken;
110
-
111
-		return $dbToken;
112
-	}
113
-
114
-	public function getToken(string $tokenId): IToken {
115
-		$tokenHash = $this->hashToken($tokenId);
116
-
117
-		if (isset($this->cache[$tokenHash])) {
118
-			if ($this->cache[$tokenHash] instanceof DoesNotExistException) {
119
-				$ex = $this->cache[$tokenHash];
120
-				throw new InvalidTokenException("Token does not exist: " . $ex->getMessage(), 0, $ex);
121
-			}
122
-			$token = $this->cache[$tokenHash];
123
-		} else {
124
-			try {
125
-				$token = $this->mapper->getToken($this->hashToken($tokenId));
126
-				$this->cache[$token->getToken()] = $token;
127
-			} catch (DoesNotExistException $ex) {
128
-				try {
129
-					$token = $this->mapper->getToken($this->hashTokenWithEmptySecret($tokenId));
130
-					$this->cache[$token->getToken()] = $token;
131
-					$this->rotate($token, $tokenId, $tokenId);
132
-				} catch (DoesNotExistException $ex2) {
133
-					$this->cache[$tokenHash] = $ex2;
134
-					throw new InvalidTokenException("Token does not exist: " . $ex->getMessage(), 0, $ex);
135
-				}
136
-			}
137
-		}
138
-
139
-		if ((int)$token->getExpires() !== 0 && $token->getExpires() < $this->time->getTime()) {
140
-			throw new ExpiredTokenException($token);
141
-		}
142
-
143
-		if ($token->getType() === IToken::WIPE_TOKEN) {
144
-			throw new WipeTokenException($token);
145
-		}
146
-
147
-		if ($token->getPasswordInvalid() === true) {
148
-			//The password is invalid we should throw an TokenPasswordExpiredException
149
-			throw new TokenPasswordExpiredException($token);
150
-		}
151
-
152
-		return $token;
153
-	}
154
-
155
-	public function getTokenById(int $tokenId): IToken {
156
-		try {
157
-			$token = $this->mapper->getTokenById($tokenId);
158
-		} catch (DoesNotExistException $ex) {
159
-			throw new InvalidTokenException("Token with ID $tokenId does not exist: " . $ex->getMessage(), 0, $ex);
160
-		}
161
-
162
-		if ((int)$token->getExpires() !== 0 && $token->getExpires() < $this->time->getTime()) {
163
-			throw new ExpiredTokenException($token);
164
-		}
165
-
166
-		if ($token->getType() === IToken::WIPE_TOKEN) {
167
-			throw new WipeTokenException($token);
168
-		}
169
-
170
-		if ($token->getPasswordInvalid() === true) {
171
-			//The password is invalid we should throw an TokenPasswordExpiredException
172
-			throw new TokenPasswordExpiredException($token);
173
-		}
174
-
175
-		return $token;
176
-	}
177
-
178
-	public function renewSessionToken(string $oldSessionId, string $sessionId): IToken {
179
-		$this->cache->clear();
180
-
181
-		return $this->atomic(function () use ($oldSessionId, $sessionId) {
182
-			$token = $this->getToken($oldSessionId);
183
-
184
-			if (!($token instanceof PublicKeyToken)) {
185
-				throw new InvalidTokenException("Invalid token type");
186
-			}
187
-
188
-			$password = null;
189
-			if (!is_null($token->getPassword())) {
190
-				$privateKey = $this->decrypt($token->getPrivateKey(), $oldSessionId);
191
-				$password = $this->decryptPassword($token->getPassword(), $privateKey);
192
-			}
193
-			$newToken = $this->generateToken(
194
-				$sessionId,
195
-				$token->getUID(),
196
-				$token->getLoginName(),
197
-				$password,
198
-				$token->getName(),
199
-				IToken::TEMPORARY_TOKEN,
200
-				$token->getRemember()
201
-			);
202
-
203
-			$this->mapper->delete($token);
204
-
205
-			return $newToken;
206
-		}, $this->db);
207
-	}
208
-
209
-	public function invalidateToken(string $token) {
210
-		$this->cache->clear();
211
-
212
-		$this->mapper->invalidate($this->hashToken($token));
213
-		$this->mapper->invalidate($this->hashTokenWithEmptySecret($token));
214
-	}
215
-
216
-	public function invalidateTokenById(string $uid, int $id) {
217
-		$this->cache->clear();
218
-
219
-		$this->mapper->deleteById($uid, $id);
220
-	}
221
-
222
-	public function invalidateOldTokens() {
223
-		$this->cache->clear();
224
-
225
-		$olderThan = $this->time->getTime() - (int) $this->config->getSystemValue('session_lifetime', 60 * 60 * 24);
226
-		$this->logger->debug('Invalidating session tokens older than ' . date('c', $olderThan), ['app' => 'cron']);
227
-		$this->mapper->invalidateOld($olderThan, IToken::DO_NOT_REMEMBER);
228
-		$rememberThreshold = $this->time->getTime() - (int) $this->config->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15);
229
-		$this->logger->debug('Invalidating remembered session tokens older than ' . date('c', $rememberThreshold), ['app' => 'cron']);
230
-		$this->mapper->invalidateOld($rememberThreshold, IToken::REMEMBER);
231
-	}
232
-
233
-	public function updateToken(IToken $token) {
234
-		$this->cache->clear();
235
-
236
-		if (!($token instanceof PublicKeyToken)) {
237
-			throw new InvalidTokenException("Invalid token type");
238
-		}
239
-		$this->mapper->update($token);
240
-	}
241
-
242
-	public function updateTokenActivity(IToken $token) {
243
-		$this->cache->clear();
244
-
245
-		if (!($token instanceof PublicKeyToken)) {
246
-			throw new InvalidTokenException("Invalid token type");
247
-		}
248
-
249
-		$activityInterval = $this->config->getSystemValueInt('token_auth_activity_update', 60);
250
-		$activityInterval = min(max($activityInterval, 0), 300);
251
-
252
-		/** @var PublicKeyToken $token */
253
-		$now = $this->time->getTime();
254
-		if ($token->getLastActivity() < ($now - $activityInterval)) {
255
-			$token->setLastActivity($now);
256
-			$this->mapper->updateActivity($token, $now);
257
-		}
258
-	}
259
-
260
-	public function getTokenByUser(string $uid): array {
261
-		return $this->mapper->getTokenByUser($uid);
262
-	}
263
-
264
-	public function getPassword(IToken $savedToken, string $tokenId): string {
265
-		if (!($savedToken instanceof PublicKeyToken)) {
266
-			throw new InvalidTokenException("Invalid token type");
267
-		}
268
-
269
-		if ($savedToken->getPassword() === null) {
270
-			throw new PasswordlessTokenException();
271
-		}
272
-
273
-		// Decrypt private key with tokenId
274
-		$privateKey = $this->decrypt($savedToken->getPrivateKey(), $tokenId);
275
-
276
-		// Decrypt password with private key
277
-		return $this->decryptPassword($savedToken->getPassword(), $privateKey);
278
-	}
279
-
280
-	public function setPassword(IToken $token, string $tokenId, string $password) {
281
-		$this->cache->clear();
282
-
283
-		if (!($token instanceof PublicKeyToken)) {
284
-			throw new InvalidTokenException("Invalid token type");
285
-		}
286
-
287
-		// When changing passwords all temp tokens are deleted
288
-		$this->mapper->deleteTempToken($token);
289
-
290
-		// Update the password for all tokens
291
-		$tokens = $this->mapper->getTokenByUser($token->getUID());
292
-		foreach ($tokens as $t) {
293
-			$publicKey = $t->getPublicKey();
294
-			$t->setPassword($this->encryptPassword($password, $publicKey));
295
-			$t->setPasswordHash($this->hashPassword($password));
296
-			$this->updateToken($t);
297
-		}
298
-	}
299
-
300
-	private function hashPassword(string $password): string {
301
-		return $this->hasher->hash(sha1($password) . $password);
302
-	}
303
-
304
-	public function rotate(IToken $token, string $oldTokenId, string $newTokenId): IToken {
305
-		$this->cache->clear();
306
-
307
-		if (!($token instanceof PublicKeyToken)) {
308
-			throw new InvalidTokenException("Invalid token type");
309
-		}
310
-
311
-		// Decrypt private key with oldTokenId
312
-		$privateKey = $this->decrypt($token->getPrivateKey(), $oldTokenId);
313
-		// Encrypt with the new token
314
-		$token->setPrivateKey($this->encrypt($privateKey, $newTokenId));
315
-
316
-		$token->setToken($this->hashToken($newTokenId));
317
-		$this->updateToken($token);
318
-
319
-		return $token;
320
-	}
321
-
322
-	private function encrypt(string $plaintext, string $token): string {
323
-		$secret = $this->config->getSystemValue('secret');
324
-		return $this->crypto->encrypt($plaintext, $token . $secret);
325
-	}
326
-
327
-	/**
328
-	 * @throws InvalidTokenException
329
-	 */
330
-	private function decrypt(string $cipherText, string $token): string {
331
-		$secret = $this->config->getSystemValue('secret');
332
-		try {
333
-			return $this->crypto->decrypt($cipherText, $token . $secret);
334
-		} catch (\Exception $ex) {
335
-			// Retry with empty secret as a fallback for instances where the secret might not have been set by accident
336
-			try {
337
-				return $this->crypto->decrypt($cipherText, $token);
338
-			} catch (\Exception $ex2) {
339
-				// Delete the invalid token
340
-				$this->invalidateToken($token);
341
-				throw new InvalidTokenException("Could not decrypt token password: " . $ex->getMessage(), 0, $ex2);
342
-			}
343
-		}
344
-	}
345
-
346
-	private function encryptPassword(string $password, string $publicKey): string {
347
-		openssl_public_encrypt($password, $encryptedPassword, $publicKey, OPENSSL_PKCS1_OAEP_PADDING);
348
-		$encryptedPassword = base64_encode($encryptedPassword);
349
-
350
-		return $encryptedPassword;
351
-	}
352
-
353
-	private function decryptPassword(string $encryptedPassword, string $privateKey): string {
354
-		$encryptedPassword = base64_decode($encryptedPassword);
355
-		openssl_private_decrypt($encryptedPassword, $password, $privateKey, OPENSSL_PKCS1_OAEP_PADDING);
356
-
357
-		return $password;
358
-	}
359
-
360
-	private function hashToken(string $token): string {
361
-		$secret = $this->config->getSystemValue('secret');
362
-		return hash('sha512', $token . $secret);
363
-	}
364
-
365
-	/**
366
-	 * @deprecated Fallback for instances where the secret might not have been set by accident
367
-	 */
368
-	private function hashTokenWithEmptySecret(string $token): string {
369
-		return hash('sha512', $token);
370
-	}
371
-
372
-	/**
373
-	 * @throws \RuntimeException when OpenSSL reports a problem
374
-	 */
375
-	private function newToken(string $token,
376
-							  string $uid,
377
-							  string $loginName,
378
-							  $password,
379
-							  string $name,
380
-							  int $type,
381
-							  int $remember): PublicKeyToken {
382
-		$dbToken = new PublicKeyToken();
383
-		$dbToken->setUid($uid);
384
-		$dbToken->setLoginName($loginName);
385
-
386
-		$config = array_merge([
387
-			'digest_alg' => 'sha512',
388
-			'private_key_bits' => $password !== null && strlen($password) > 250 ? 4096 : 2048,
389
-		], $this->config->getSystemValue('openssl', []));
390
-
391
-		// Generate new key
392
-		$res = openssl_pkey_new($config);
393
-		if ($res === false) {
394
-			$this->logOpensslError();
395
-			throw new \RuntimeException('OpenSSL reported a problem');
396
-		}
397
-
398
-		if (openssl_pkey_export($res, $privateKey, null, $config) === false) {
399
-			$this->logOpensslError();
400
-			throw new \RuntimeException('OpenSSL reported a problem');
401
-		}
402
-
403
-		// Extract the public key from $res to $pubKey
404
-		$publicKey = openssl_pkey_get_details($res);
405
-		$publicKey = $publicKey['key'];
406
-
407
-		$dbToken->setPublicKey($publicKey);
408
-		$dbToken->setPrivateKey($this->encrypt($privateKey, $token));
409
-
410
-		if (!is_null($password) && $this->config->getSystemValueBool('auth.storeCryptedPassword', true)) {
411
-			if (strlen($password) > IUserManager::MAX_PASSWORD_LENGTH) {
412
-				throw new \RuntimeException('Trying to save a password with more than 469 characters is not supported. If you want to use big passwords, disable the auth.storeCryptedPassword option in config.php');
413
-			}
414
-			$dbToken->setPassword($this->encryptPassword($password, $publicKey));
415
-			$dbToken->setPasswordHash($this->hashPassword($password));
416
-		}
417
-
418
-		$dbToken->setName($name);
419
-		$dbToken->setToken($this->hashToken($token));
420
-		$dbToken->setType($type);
421
-		$dbToken->setRemember($remember);
422
-		$dbToken->setLastActivity($this->time->getTime());
423
-		$dbToken->setLastCheck($this->time->getTime());
424
-		$dbToken->setVersion(PublicKeyToken::VERSION);
425
-
426
-		return $dbToken;
427
-	}
428
-
429
-	public function markPasswordInvalid(IToken $token, string $tokenId) {
430
-		$this->cache->clear();
431
-
432
-		if (!($token instanceof PublicKeyToken)) {
433
-			throw new InvalidTokenException("Invalid token type");
434
-		}
435
-
436
-		$token->setPasswordInvalid(true);
437
-		$this->mapper->update($token);
438
-	}
439
-
440
-	public function updatePasswords(string $uid, string $password) {
441
-		$this->cache->clear();
442
-
443
-		// prevent setting an empty pw as result of pw-less-login
444
-		if ($password === '' || !$this->config->getSystemValueBool('auth.storeCryptedPassword', true)) {
445
-			return;
446
-		}
447
-
448
-		// Update the password for all tokens
449
-		$tokens = $this->mapper->getTokenByUser($uid);
450
-		$newPasswordHash = null;
451
-
452
-		/**
453
-		 * - true: The password hash could not be verified anymore
454
-		 *     and the token needs to be updated with the newly encrypted password
455
-		 * - false: The hash could still be verified
456
-		 * - missing: The hash needs to be verified
457
-		 */
458
-		$hashNeedsUpdate = [];
459
-
460
-		foreach ($tokens as $t) {
461
-			if (!isset($hashNeedsUpdate[$t->getPasswordHash()])) {
462
-				if ($t->getPasswordHash() === null) {
463
-					$hashNeedsUpdate[$t->getPasswordHash() ?: ''] = true;
464
-				} elseif (!$this->hasher->verify(sha1($password) . $password, $t->getPasswordHash())) {
465
-					$hashNeedsUpdate[$t->getPasswordHash() ?: ''] = true;
466
-				} else {
467
-					$hashNeedsUpdate[$t->getPasswordHash() ?: ''] = false;
468
-				}
469
-			}
470
-			$needsUpdating = $hashNeedsUpdate[$t->getPasswordHash() ?: ''] ?? true;
471
-
472
-			if ($needsUpdating) {
473
-				if ($newPasswordHash === null) {
474
-					$newPasswordHash = $this->hashPassword($password);
475
-				}
476
-
477
-				$publicKey = $t->getPublicKey();
478
-				$t->setPassword($this->encryptPassword($password, $publicKey));
479
-				$t->setPasswordHash($newPasswordHash);
480
-				$t->setPasswordInvalid(false);
481
-				$this->updateToken($t);
482
-			}
483
-		}
484
-	}
485
-
486
-	private function logOpensslError() {
487
-		$errors = [];
488
-		while ($error = openssl_error_string()) {
489
-			$errors[] = $error;
490
-		}
491
-		$this->logger->critical('Something is wrong with your openssl setup: ' . implode(', ', $errors));
492
-	}
49
+    use TTransactional;
50
+
51
+    /** @var PublicKeyTokenMapper */
52
+    private $mapper;
53
+
54
+    /** @var ICrypto */
55
+    private $crypto;
56
+
57
+    /** @var IConfig */
58
+    private $config;
59
+
60
+    private IDBConnection $db;
61
+
62
+    /** @var LoggerInterface */
63
+    private $logger;
64
+
65
+    /** @var ITimeFactory */
66
+    private $time;
67
+
68
+    /** @var CappedMemoryCache */
69
+    private $cache;
70
+
71
+    private IHasher $hasher;
72
+
73
+    public function __construct(PublicKeyTokenMapper $mapper,
74
+                                ICrypto $crypto,
75
+                                IConfig $config,
76
+                                IDBConnection $db,
77
+                                LoggerInterface $logger,
78
+                                ITimeFactory $time,
79
+                                IHasher $hasher) {
80
+        $this->mapper = $mapper;
81
+        $this->crypto = $crypto;
82
+        $this->config = $config;
83
+        $this->db = $db;
84
+        $this->logger = $logger;
85
+        $this->time = $time;
86
+
87
+        $this->cache = new CappedMemoryCache();
88
+        $this->hasher = $hasher;
89
+    }
90
+
91
+    /**
92
+     * {@inheritDoc}
93
+     */
94
+    public function generateToken(string $token,
95
+                                    string $uid,
96
+                                    string $loginName,
97
+                                  ?string $password,
98
+                                    string $name,
99
+                                    int $type = IToken::TEMPORARY_TOKEN,
100
+                                    int $remember = IToken::DO_NOT_REMEMBER): IToken {
101
+        if (mb_strlen($name) > 128) {
102
+            $name = mb_substr($name, 0, 120) . '…';
103
+        }
104
+
105
+        $dbToken = $this->newToken($token, $uid, $loginName, $password, $name, $type, $remember);
106
+        $this->mapper->insert($dbToken);
107
+
108
+        // Add the token to the cache
109
+        $this->cache[$dbToken->getToken()] = $dbToken;
110
+
111
+        return $dbToken;
112
+    }
113
+
114
+    public function getToken(string $tokenId): IToken {
115
+        $tokenHash = $this->hashToken($tokenId);
116
+
117
+        if (isset($this->cache[$tokenHash])) {
118
+            if ($this->cache[$tokenHash] instanceof DoesNotExistException) {
119
+                $ex = $this->cache[$tokenHash];
120
+                throw new InvalidTokenException("Token does not exist: " . $ex->getMessage(), 0, $ex);
121
+            }
122
+            $token = $this->cache[$tokenHash];
123
+        } else {
124
+            try {
125
+                $token = $this->mapper->getToken($this->hashToken($tokenId));
126
+                $this->cache[$token->getToken()] = $token;
127
+            } catch (DoesNotExistException $ex) {
128
+                try {
129
+                    $token = $this->mapper->getToken($this->hashTokenWithEmptySecret($tokenId));
130
+                    $this->cache[$token->getToken()] = $token;
131
+                    $this->rotate($token, $tokenId, $tokenId);
132
+                } catch (DoesNotExistException $ex2) {
133
+                    $this->cache[$tokenHash] = $ex2;
134
+                    throw new InvalidTokenException("Token does not exist: " . $ex->getMessage(), 0, $ex);
135
+                }
136
+            }
137
+        }
138
+
139
+        if ((int)$token->getExpires() !== 0 && $token->getExpires() < $this->time->getTime()) {
140
+            throw new ExpiredTokenException($token);
141
+        }
142
+
143
+        if ($token->getType() === IToken::WIPE_TOKEN) {
144
+            throw new WipeTokenException($token);
145
+        }
146
+
147
+        if ($token->getPasswordInvalid() === true) {
148
+            //The password is invalid we should throw an TokenPasswordExpiredException
149
+            throw new TokenPasswordExpiredException($token);
150
+        }
151
+
152
+        return $token;
153
+    }
154
+
155
+    public function getTokenById(int $tokenId): IToken {
156
+        try {
157
+            $token = $this->mapper->getTokenById($tokenId);
158
+        } catch (DoesNotExistException $ex) {
159
+            throw new InvalidTokenException("Token with ID $tokenId does not exist: " . $ex->getMessage(), 0, $ex);
160
+        }
161
+
162
+        if ((int)$token->getExpires() !== 0 && $token->getExpires() < $this->time->getTime()) {
163
+            throw new ExpiredTokenException($token);
164
+        }
165
+
166
+        if ($token->getType() === IToken::WIPE_TOKEN) {
167
+            throw new WipeTokenException($token);
168
+        }
169
+
170
+        if ($token->getPasswordInvalid() === true) {
171
+            //The password is invalid we should throw an TokenPasswordExpiredException
172
+            throw new TokenPasswordExpiredException($token);
173
+        }
174
+
175
+        return $token;
176
+    }
177
+
178
+    public function renewSessionToken(string $oldSessionId, string $sessionId): IToken {
179
+        $this->cache->clear();
180
+
181
+        return $this->atomic(function () use ($oldSessionId, $sessionId) {
182
+            $token = $this->getToken($oldSessionId);
183
+
184
+            if (!($token instanceof PublicKeyToken)) {
185
+                throw new InvalidTokenException("Invalid token type");
186
+            }
187
+
188
+            $password = null;
189
+            if (!is_null($token->getPassword())) {
190
+                $privateKey = $this->decrypt($token->getPrivateKey(), $oldSessionId);
191
+                $password = $this->decryptPassword($token->getPassword(), $privateKey);
192
+            }
193
+            $newToken = $this->generateToken(
194
+                $sessionId,
195
+                $token->getUID(),
196
+                $token->getLoginName(),
197
+                $password,
198
+                $token->getName(),
199
+                IToken::TEMPORARY_TOKEN,
200
+                $token->getRemember()
201
+            );
202
+
203
+            $this->mapper->delete($token);
204
+
205
+            return $newToken;
206
+        }, $this->db);
207
+    }
208
+
209
+    public function invalidateToken(string $token) {
210
+        $this->cache->clear();
211
+
212
+        $this->mapper->invalidate($this->hashToken($token));
213
+        $this->mapper->invalidate($this->hashTokenWithEmptySecret($token));
214
+    }
215
+
216
+    public function invalidateTokenById(string $uid, int $id) {
217
+        $this->cache->clear();
218
+
219
+        $this->mapper->deleteById($uid, $id);
220
+    }
221
+
222
+    public function invalidateOldTokens() {
223
+        $this->cache->clear();
224
+
225
+        $olderThan = $this->time->getTime() - (int) $this->config->getSystemValue('session_lifetime', 60 * 60 * 24);
226
+        $this->logger->debug('Invalidating session tokens older than ' . date('c', $olderThan), ['app' => 'cron']);
227
+        $this->mapper->invalidateOld($olderThan, IToken::DO_NOT_REMEMBER);
228
+        $rememberThreshold = $this->time->getTime() - (int) $this->config->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15);
229
+        $this->logger->debug('Invalidating remembered session tokens older than ' . date('c', $rememberThreshold), ['app' => 'cron']);
230
+        $this->mapper->invalidateOld($rememberThreshold, IToken::REMEMBER);
231
+    }
232
+
233
+    public function updateToken(IToken $token) {
234
+        $this->cache->clear();
235
+
236
+        if (!($token instanceof PublicKeyToken)) {
237
+            throw new InvalidTokenException("Invalid token type");
238
+        }
239
+        $this->mapper->update($token);
240
+    }
241
+
242
+    public function updateTokenActivity(IToken $token) {
243
+        $this->cache->clear();
244
+
245
+        if (!($token instanceof PublicKeyToken)) {
246
+            throw new InvalidTokenException("Invalid token type");
247
+        }
248
+
249
+        $activityInterval = $this->config->getSystemValueInt('token_auth_activity_update', 60);
250
+        $activityInterval = min(max($activityInterval, 0), 300);
251
+
252
+        /** @var PublicKeyToken $token */
253
+        $now = $this->time->getTime();
254
+        if ($token->getLastActivity() < ($now - $activityInterval)) {
255
+            $token->setLastActivity($now);
256
+            $this->mapper->updateActivity($token, $now);
257
+        }
258
+    }
259
+
260
+    public function getTokenByUser(string $uid): array {
261
+        return $this->mapper->getTokenByUser($uid);
262
+    }
263
+
264
+    public function getPassword(IToken $savedToken, string $tokenId): string {
265
+        if (!($savedToken instanceof PublicKeyToken)) {
266
+            throw new InvalidTokenException("Invalid token type");
267
+        }
268
+
269
+        if ($savedToken->getPassword() === null) {
270
+            throw new PasswordlessTokenException();
271
+        }
272
+
273
+        // Decrypt private key with tokenId
274
+        $privateKey = $this->decrypt($savedToken->getPrivateKey(), $tokenId);
275
+
276
+        // Decrypt password with private key
277
+        return $this->decryptPassword($savedToken->getPassword(), $privateKey);
278
+    }
279
+
280
+    public function setPassword(IToken $token, string $tokenId, string $password) {
281
+        $this->cache->clear();
282
+
283
+        if (!($token instanceof PublicKeyToken)) {
284
+            throw new InvalidTokenException("Invalid token type");
285
+        }
286
+
287
+        // When changing passwords all temp tokens are deleted
288
+        $this->mapper->deleteTempToken($token);
289
+
290
+        // Update the password for all tokens
291
+        $tokens = $this->mapper->getTokenByUser($token->getUID());
292
+        foreach ($tokens as $t) {
293
+            $publicKey = $t->getPublicKey();
294
+            $t->setPassword($this->encryptPassword($password, $publicKey));
295
+            $t->setPasswordHash($this->hashPassword($password));
296
+            $this->updateToken($t);
297
+        }
298
+    }
299
+
300
+    private function hashPassword(string $password): string {
301
+        return $this->hasher->hash(sha1($password) . $password);
302
+    }
303
+
304
+    public function rotate(IToken $token, string $oldTokenId, string $newTokenId): IToken {
305
+        $this->cache->clear();
306
+
307
+        if (!($token instanceof PublicKeyToken)) {
308
+            throw new InvalidTokenException("Invalid token type");
309
+        }
310
+
311
+        // Decrypt private key with oldTokenId
312
+        $privateKey = $this->decrypt($token->getPrivateKey(), $oldTokenId);
313
+        // Encrypt with the new token
314
+        $token->setPrivateKey($this->encrypt($privateKey, $newTokenId));
315
+
316
+        $token->setToken($this->hashToken($newTokenId));
317
+        $this->updateToken($token);
318
+
319
+        return $token;
320
+    }
321
+
322
+    private function encrypt(string $plaintext, string $token): string {
323
+        $secret = $this->config->getSystemValue('secret');
324
+        return $this->crypto->encrypt($plaintext, $token . $secret);
325
+    }
326
+
327
+    /**
328
+     * @throws InvalidTokenException
329
+     */
330
+    private function decrypt(string $cipherText, string $token): string {
331
+        $secret = $this->config->getSystemValue('secret');
332
+        try {
333
+            return $this->crypto->decrypt($cipherText, $token . $secret);
334
+        } catch (\Exception $ex) {
335
+            // Retry with empty secret as a fallback for instances where the secret might not have been set by accident
336
+            try {
337
+                return $this->crypto->decrypt($cipherText, $token);
338
+            } catch (\Exception $ex2) {
339
+                // Delete the invalid token
340
+                $this->invalidateToken($token);
341
+                throw new InvalidTokenException("Could not decrypt token password: " . $ex->getMessage(), 0, $ex2);
342
+            }
343
+        }
344
+    }
345
+
346
+    private function encryptPassword(string $password, string $publicKey): string {
347
+        openssl_public_encrypt($password, $encryptedPassword, $publicKey, OPENSSL_PKCS1_OAEP_PADDING);
348
+        $encryptedPassword = base64_encode($encryptedPassword);
349
+
350
+        return $encryptedPassword;
351
+    }
352
+
353
+    private function decryptPassword(string $encryptedPassword, string $privateKey): string {
354
+        $encryptedPassword = base64_decode($encryptedPassword);
355
+        openssl_private_decrypt($encryptedPassword, $password, $privateKey, OPENSSL_PKCS1_OAEP_PADDING);
356
+
357
+        return $password;
358
+    }
359
+
360
+    private function hashToken(string $token): string {
361
+        $secret = $this->config->getSystemValue('secret');
362
+        return hash('sha512', $token . $secret);
363
+    }
364
+
365
+    /**
366
+     * @deprecated Fallback for instances where the secret might not have been set by accident
367
+     */
368
+    private function hashTokenWithEmptySecret(string $token): string {
369
+        return hash('sha512', $token);
370
+    }
371
+
372
+    /**
373
+     * @throws \RuntimeException when OpenSSL reports a problem
374
+     */
375
+    private function newToken(string $token,
376
+                                string $uid,
377
+                                string $loginName,
378
+                                $password,
379
+                                string $name,
380
+                                int $type,
381
+                                int $remember): PublicKeyToken {
382
+        $dbToken = new PublicKeyToken();
383
+        $dbToken->setUid($uid);
384
+        $dbToken->setLoginName($loginName);
385
+
386
+        $config = array_merge([
387
+            'digest_alg' => 'sha512',
388
+            'private_key_bits' => $password !== null && strlen($password) > 250 ? 4096 : 2048,
389
+        ], $this->config->getSystemValue('openssl', []));
390
+
391
+        // Generate new key
392
+        $res = openssl_pkey_new($config);
393
+        if ($res === false) {
394
+            $this->logOpensslError();
395
+            throw new \RuntimeException('OpenSSL reported a problem');
396
+        }
397
+
398
+        if (openssl_pkey_export($res, $privateKey, null, $config) === false) {
399
+            $this->logOpensslError();
400
+            throw new \RuntimeException('OpenSSL reported a problem');
401
+        }
402
+
403
+        // Extract the public key from $res to $pubKey
404
+        $publicKey = openssl_pkey_get_details($res);
405
+        $publicKey = $publicKey['key'];
406
+
407
+        $dbToken->setPublicKey($publicKey);
408
+        $dbToken->setPrivateKey($this->encrypt($privateKey, $token));
409
+
410
+        if (!is_null($password) && $this->config->getSystemValueBool('auth.storeCryptedPassword', true)) {
411
+            if (strlen($password) > IUserManager::MAX_PASSWORD_LENGTH) {
412
+                throw new \RuntimeException('Trying to save a password with more than 469 characters is not supported. If you want to use big passwords, disable the auth.storeCryptedPassword option in config.php');
413
+            }
414
+            $dbToken->setPassword($this->encryptPassword($password, $publicKey));
415
+            $dbToken->setPasswordHash($this->hashPassword($password));
416
+        }
417
+
418
+        $dbToken->setName($name);
419
+        $dbToken->setToken($this->hashToken($token));
420
+        $dbToken->setType($type);
421
+        $dbToken->setRemember($remember);
422
+        $dbToken->setLastActivity($this->time->getTime());
423
+        $dbToken->setLastCheck($this->time->getTime());
424
+        $dbToken->setVersion(PublicKeyToken::VERSION);
425
+
426
+        return $dbToken;
427
+    }
428
+
429
+    public function markPasswordInvalid(IToken $token, string $tokenId) {
430
+        $this->cache->clear();
431
+
432
+        if (!($token instanceof PublicKeyToken)) {
433
+            throw new InvalidTokenException("Invalid token type");
434
+        }
435
+
436
+        $token->setPasswordInvalid(true);
437
+        $this->mapper->update($token);
438
+    }
439
+
440
+    public function updatePasswords(string $uid, string $password) {
441
+        $this->cache->clear();
442
+
443
+        // prevent setting an empty pw as result of pw-less-login
444
+        if ($password === '' || !$this->config->getSystemValueBool('auth.storeCryptedPassword', true)) {
445
+            return;
446
+        }
447
+
448
+        // Update the password for all tokens
449
+        $tokens = $this->mapper->getTokenByUser($uid);
450
+        $newPasswordHash = null;
451
+
452
+        /**
453
+         * - true: The password hash could not be verified anymore
454
+         *     and the token needs to be updated with the newly encrypted password
455
+         * - false: The hash could still be verified
456
+         * - missing: The hash needs to be verified
457
+         */
458
+        $hashNeedsUpdate = [];
459
+
460
+        foreach ($tokens as $t) {
461
+            if (!isset($hashNeedsUpdate[$t->getPasswordHash()])) {
462
+                if ($t->getPasswordHash() === null) {
463
+                    $hashNeedsUpdate[$t->getPasswordHash() ?: ''] = true;
464
+                } elseif (!$this->hasher->verify(sha1($password) . $password, $t->getPasswordHash())) {
465
+                    $hashNeedsUpdate[$t->getPasswordHash() ?: ''] = true;
466
+                } else {
467
+                    $hashNeedsUpdate[$t->getPasswordHash() ?: ''] = false;
468
+                }
469
+            }
470
+            $needsUpdating = $hashNeedsUpdate[$t->getPasswordHash() ?: ''] ?? true;
471
+
472
+            if ($needsUpdating) {
473
+                if ($newPasswordHash === null) {
474
+                    $newPasswordHash = $this->hashPassword($password);
475
+                }
476
+
477
+                $publicKey = $t->getPublicKey();
478
+                $t->setPassword($this->encryptPassword($password, $publicKey));
479
+                $t->setPasswordHash($newPasswordHash);
480
+                $t->setPasswordInvalid(false);
481
+                $this->updateToken($t);
482
+            }
483
+        }
484
+    }
485
+
486
+    private function logOpensslError() {
487
+        $errors = [];
488
+        while ($error = openssl_error_string()) {
489
+            $errors[] = $error;
490
+        }
491
+        $this->logger->critical('Something is wrong with your openssl setup: ' . implode(', ', $errors));
492
+    }
493 493
 }
Please login to merge, or discard this patch.
Spacing   +16 added lines, -16 removed lines patch added patch discarded remove patch
@@ -99,7 +99,7 @@  discard block
 block discarded – undo
99 99
 								  int $type = IToken::TEMPORARY_TOKEN,
100 100
 								  int $remember = IToken::DO_NOT_REMEMBER): IToken {
101 101
 		if (mb_strlen($name) > 128) {
102
-			$name = mb_substr($name, 0, 120) . '…';
102
+			$name = mb_substr($name, 0, 120).'…';
103 103
 		}
104 104
 
105 105
 		$dbToken = $this->newToken($token, $uid, $loginName, $password, $name, $type, $remember);
@@ -117,7 +117,7 @@  discard block
 block discarded – undo
117 117
 		if (isset($this->cache[$tokenHash])) {
118 118
 			if ($this->cache[$tokenHash] instanceof DoesNotExistException) {
119 119
 				$ex = $this->cache[$tokenHash];
120
-				throw new InvalidTokenException("Token does not exist: " . $ex->getMessage(), 0, $ex);
120
+				throw new InvalidTokenException("Token does not exist: ".$ex->getMessage(), 0, $ex);
121 121
 			}
122 122
 			$token = $this->cache[$tokenHash];
123 123
 		} else {
@@ -131,12 +131,12 @@  discard block
 block discarded – undo
131 131
 					$this->rotate($token, $tokenId, $tokenId);
132 132
 				} catch (DoesNotExistException $ex2) {
133 133
 					$this->cache[$tokenHash] = $ex2;
134
-					throw new InvalidTokenException("Token does not exist: " . $ex->getMessage(), 0, $ex);
134
+					throw new InvalidTokenException("Token does not exist: ".$ex->getMessage(), 0, $ex);
135 135
 				}
136 136
 			}
137 137
 		}
138 138
 
139
-		if ((int)$token->getExpires() !== 0 && $token->getExpires() < $this->time->getTime()) {
139
+		if ((int) $token->getExpires() !== 0 && $token->getExpires() < $this->time->getTime()) {
140 140
 			throw new ExpiredTokenException($token);
141 141
 		}
142 142
 
@@ -156,10 +156,10 @@  discard block
 block discarded – undo
156 156
 		try {
157 157
 			$token = $this->mapper->getTokenById($tokenId);
158 158
 		} catch (DoesNotExistException $ex) {
159
-			throw new InvalidTokenException("Token with ID $tokenId does not exist: " . $ex->getMessage(), 0, $ex);
159
+			throw new InvalidTokenException("Token with ID $tokenId does not exist: ".$ex->getMessage(), 0, $ex);
160 160
 		}
161 161
 
162
-		if ((int)$token->getExpires() !== 0 && $token->getExpires() < $this->time->getTime()) {
162
+		if ((int) $token->getExpires() !== 0 && $token->getExpires() < $this->time->getTime()) {
163 163
 			throw new ExpiredTokenException($token);
164 164
 		}
165 165
 
@@ -178,7 +178,7 @@  discard block
 block discarded – undo
178 178
 	public function renewSessionToken(string $oldSessionId, string $sessionId): IToken {
179 179
 		$this->cache->clear();
180 180
 
181
-		return $this->atomic(function () use ($oldSessionId, $sessionId) {
181
+		return $this->atomic(function() use ($oldSessionId, $sessionId) {
182 182
 			$token = $this->getToken($oldSessionId);
183 183
 
184 184
 			if (!($token instanceof PublicKeyToken)) {
@@ -223,10 +223,10 @@  discard block
 block discarded – undo
223 223
 		$this->cache->clear();
224 224
 
225 225
 		$olderThan = $this->time->getTime() - (int) $this->config->getSystemValue('session_lifetime', 60 * 60 * 24);
226
-		$this->logger->debug('Invalidating session tokens older than ' . date('c', $olderThan), ['app' => 'cron']);
226
+		$this->logger->debug('Invalidating session tokens older than '.date('c', $olderThan), ['app' => 'cron']);
227 227
 		$this->mapper->invalidateOld($olderThan, IToken::DO_NOT_REMEMBER);
228 228
 		$rememberThreshold = $this->time->getTime() - (int) $this->config->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15);
229
-		$this->logger->debug('Invalidating remembered session tokens older than ' . date('c', $rememberThreshold), ['app' => 'cron']);
229
+		$this->logger->debug('Invalidating remembered session tokens older than '.date('c', $rememberThreshold), ['app' => 'cron']);
230 230
 		$this->mapper->invalidateOld($rememberThreshold, IToken::REMEMBER);
231 231
 	}
232 232
 
@@ -298,7 +298,7 @@  discard block
 block discarded – undo
298 298
 	}
299 299
 
300 300
 	private function hashPassword(string $password): string {
301
-		return $this->hasher->hash(sha1($password) . $password);
301
+		return $this->hasher->hash(sha1($password).$password);
302 302
 	}
303 303
 
304 304
 	public function rotate(IToken $token, string $oldTokenId, string $newTokenId): IToken {
@@ -321,7 +321,7 @@  discard block
 block discarded – undo
321 321
 
322 322
 	private function encrypt(string $plaintext, string $token): string {
323 323
 		$secret = $this->config->getSystemValue('secret');
324
-		return $this->crypto->encrypt($plaintext, $token . $secret);
324
+		return $this->crypto->encrypt($plaintext, $token.$secret);
325 325
 	}
326 326
 
327 327
 	/**
@@ -330,7 +330,7 @@  discard block
 block discarded – undo
330 330
 	private function decrypt(string $cipherText, string $token): string {
331 331
 		$secret = $this->config->getSystemValue('secret');
332 332
 		try {
333
-			return $this->crypto->decrypt($cipherText, $token . $secret);
333
+			return $this->crypto->decrypt($cipherText, $token.$secret);
334 334
 		} catch (\Exception $ex) {
335 335
 			// Retry with empty secret as a fallback for instances where the secret might not have been set by accident
336 336
 			try {
@@ -338,7 +338,7 @@  discard block
 block discarded – undo
338 338
 			} catch (\Exception $ex2) {
339 339
 				// Delete the invalid token
340 340
 				$this->invalidateToken($token);
341
-				throw new InvalidTokenException("Could not decrypt token password: " . $ex->getMessage(), 0, $ex2);
341
+				throw new InvalidTokenException("Could not decrypt token password: ".$ex->getMessage(), 0, $ex2);
342 342
 			}
343 343
 		}
344 344
 	}
@@ -359,7 +359,7 @@  discard block
 block discarded – undo
359 359
 
360 360
 	private function hashToken(string $token): string {
361 361
 		$secret = $this->config->getSystemValue('secret');
362
-		return hash('sha512', $token . $secret);
362
+		return hash('sha512', $token.$secret);
363 363
 	}
364 364
 
365 365
 	/**
@@ -461,7 +461,7 @@  discard block
 block discarded – undo
461 461
 			if (!isset($hashNeedsUpdate[$t->getPasswordHash()])) {
462 462
 				if ($t->getPasswordHash() === null) {
463 463
 					$hashNeedsUpdate[$t->getPasswordHash() ?: ''] = true;
464
-				} elseif (!$this->hasher->verify(sha1($password) . $password, $t->getPasswordHash())) {
464
+				} elseif (!$this->hasher->verify(sha1($password).$password, $t->getPasswordHash())) {
465 465
 					$hashNeedsUpdate[$t->getPasswordHash() ?: ''] = true;
466 466
 				} else {
467 467
 					$hashNeedsUpdate[$t->getPasswordHash() ?: ''] = false;
@@ -488,6 +488,6 @@  discard block
 block discarded – undo
488 488
 		while ($error = openssl_error_string()) {
489 489
 			$errors[] = $error;
490 490
 		}
491
-		$this->logger->critical('Something is wrong with your openssl setup: ' . implode(', ', $errors));
491
+		$this->logger->critical('Something is wrong with your openssl setup: '.implode(', ', $errors));
492 492
 	}
493 493
 }
Please login to merge, or discard this patch.