Passed
Push — master ( 77c016...18164a )
by Julius
13:06 queued 12s
created
lib/private/Authentication/Token/PublicKeyTokenProvider.php 1 patch
Indentation   +420 added lines, -420 removed lines patch added patch discarded remove patch
@@ -46,424 +46,424 @@
 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
-		$passwordHash = $this->hashPassword($password);
451
-		foreach ($tokens as $t) {
452
-			$publicKey = $t->getPublicKey();
453
-			if ($t->getPasswordHash() === null || $this->hasher->verify(sha1($password) . $password, $t->getPasswordHash())) {
454
-				$t->setPassword($this->encryptPassword($password, $publicKey));
455
-				$t->setPasswordHash($passwordHash);
456
-				$t->setPasswordInvalid(false);
457
-				$this->updateToken($t);
458
-			}
459
-		}
460
-	}
461
-
462
-	private function logOpensslError() {
463
-		$errors = [];
464
-		while ($error = openssl_error_string()) {
465
-			$errors[] = $error;
466
-		}
467
-		$this->logger->critical('Something is wrong with your openssl setup: ' . implode(', ', $errors));
468
-	}
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
+        $passwordHash = $this->hashPassword($password);
451
+        foreach ($tokens as $t) {
452
+            $publicKey = $t->getPublicKey();
453
+            if ($t->getPasswordHash() === null || $this->hasher->verify(sha1($password) . $password, $t->getPasswordHash())) {
454
+                $t->setPassword($this->encryptPassword($password, $publicKey));
455
+                $t->setPasswordHash($passwordHash);
456
+                $t->setPasswordInvalid(false);
457
+                $this->updateToken($t);
458
+            }
459
+        }
460
+    }
461
+
462
+    private function logOpensslError() {
463
+        $errors = [];
464
+        while ($error = openssl_error_string()) {
465
+            $errors[] = $error;
466
+        }
467
+        $this->logger->critical('Something is wrong with your openssl setup: ' . implode(', ', $errors));
468
+    }
469 469
 }
Please login to merge, or discard this patch.
lib/private/Authentication/Token/PublicKeyToken.php 1 patch
Indentation   +188 added lines, -188 removed lines patch added patch discarded remove patch
@@ -49,192 +49,192 @@
 block discarded – undo
49 49
  * @method setPasswordHash(string $hash)
50 50
  */
51 51
 class PublicKeyToken extends Entity implements INamedToken, IWipeableToken {
52
-	public const VERSION = 2;
53
-
54
-	/** @var string user UID */
55
-	protected $uid;
56
-
57
-	/** @var string login name used for generating the token */
58
-	protected $loginName;
59
-
60
-	/** @var string encrypted user password */
61
-	protected $password;
62
-
63
-	/** @var string hashed user password */
64
-	protected $passwordHash;
65
-
66
-	/** @var string token name (e.g. browser/OS) */
67
-	protected $name;
68
-
69
-	/** @var string */
70
-	protected $token;
71
-
72
-	/** @var int */
73
-	protected $type;
74
-
75
-	/** @var int */
76
-	protected $remember;
77
-
78
-	/** @var int */
79
-	protected $lastActivity;
80
-
81
-	/** @var int */
82
-	protected $lastCheck;
83
-
84
-	/** @var string */
85
-	protected $scope;
86
-
87
-	/** @var int */
88
-	protected $expires;
89
-
90
-	/** @var string */
91
-	protected $privateKey;
92
-
93
-	/** @var string */
94
-	protected $publicKey;
95
-
96
-	/** @var int */
97
-	protected $version;
98
-
99
-	/** @var bool */
100
-	protected $passwordInvalid;
101
-
102
-	public function __construct() {
103
-		$this->addType('uid', 'string');
104
-		$this->addType('loginName', 'string');
105
-		$this->addType('password', 'string');
106
-		$this->addType('passwordHash', 'string');
107
-		$this->addType('name', 'string');
108
-		$this->addType('token', 'string');
109
-		$this->addType('type', 'int');
110
-		$this->addType('remember', 'int');
111
-		$this->addType('lastActivity', 'int');
112
-		$this->addType('lastCheck', 'int');
113
-		$this->addType('scope', 'string');
114
-		$this->addType('expires', 'int');
115
-		$this->addType('publicKey', 'string');
116
-		$this->addType('privateKey', 'string');
117
-		$this->addType('version', 'int');
118
-		$this->addType('passwordInvalid', 'bool');
119
-	}
120
-
121
-	public function getId(): int {
122
-		return $this->id;
123
-	}
124
-
125
-	public function getUID(): string {
126
-		return $this->uid;
127
-	}
128
-
129
-	/**
130
-	 * Get the login name used when generating the token
131
-	 *
132
-	 * @return string
133
-	 */
134
-	public function getLoginName(): string {
135
-		return parent::getLoginName();
136
-	}
137
-
138
-	/**
139
-	 * Get the (encrypted) login password
140
-	 *
141
-	 * @return string|null
142
-	 */
143
-	public function getPassword() {
144
-		return parent::getPassword();
145
-	}
146
-
147
-	public function jsonSerialize(): array {
148
-		return [
149
-			'id' => $this->id,
150
-			'name' => $this->name,
151
-			'lastActivity' => $this->lastActivity,
152
-			'type' => $this->type,
153
-			'scope' => $this->getScopeAsArray()
154
-		];
155
-	}
156
-
157
-	/**
158
-	 * Get the timestamp of the last password check
159
-	 *
160
-	 * @return int
161
-	 */
162
-	public function getLastCheck(): int {
163
-		return parent::getLastCheck();
164
-	}
165
-
166
-	/**
167
-	 * Get the timestamp of the last password check
168
-	 *
169
-	 * @param int $time
170
-	 */
171
-	public function setLastCheck(int $time) {
172
-		parent::setLastCheck($time);
173
-	}
174
-
175
-	public function getScope(): string {
176
-		$scope = parent::getScope();
177
-		if ($scope === null) {
178
-			return '';
179
-		}
180
-
181
-		return $scope;
182
-	}
183
-
184
-	public function getScopeAsArray(): array {
185
-		$scope = json_decode($this->getScope(), true);
186
-		if (!$scope) {
187
-			return [
188
-				'filesystem' => true
189
-			];
190
-		}
191
-		return $scope;
192
-	}
193
-
194
-	public function setScope($scope) {
195
-		if (is_array($scope)) {
196
-			parent::setScope(json_encode($scope));
197
-		} else {
198
-			parent::setScope((string)$scope);
199
-		}
200
-	}
201
-
202
-	public function getName(): string {
203
-		return parent::getName();
204
-	}
205
-
206
-	public function setName(string $name): void {
207
-		parent::setName($name);
208
-	}
209
-
210
-	public function getRemember(): int {
211
-		return parent::getRemember();
212
-	}
213
-
214
-	public function setToken(string $token) {
215
-		parent::setToken($token);
216
-	}
217
-
218
-	public function setPassword(string $password = null) {
219
-		parent::setPassword($password);
220
-	}
221
-
222
-	public function setExpires($expires) {
223
-		parent::setExpires($expires);
224
-	}
225
-
226
-	/**
227
-	 * @return int|null
228
-	 */
229
-	public function getExpires() {
230
-		return parent::getExpires();
231
-	}
232
-
233
-	public function setPasswordInvalid(bool $invalid) {
234
-		parent::setPasswordInvalid($invalid);
235
-	}
236
-
237
-	public function wipe(): void {
238
-		parent::setType(IToken::WIPE_TOKEN);
239
-	}
52
+    public const VERSION = 2;
53
+
54
+    /** @var string user UID */
55
+    protected $uid;
56
+
57
+    /** @var string login name used for generating the token */
58
+    protected $loginName;
59
+
60
+    /** @var string encrypted user password */
61
+    protected $password;
62
+
63
+    /** @var string hashed user password */
64
+    protected $passwordHash;
65
+
66
+    /** @var string token name (e.g. browser/OS) */
67
+    protected $name;
68
+
69
+    /** @var string */
70
+    protected $token;
71
+
72
+    /** @var int */
73
+    protected $type;
74
+
75
+    /** @var int */
76
+    protected $remember;
77
+
78
+    /** @var int */
79
+    protected $lastActivity;
80
+
81
+    /** @var int */
82
+    protected $lastCheck;
83
+
84
+    /** @var string */
85
+    protected $scope;
86
+
87
+    /** @var int */
88
+    protected $expires;
89
+
90
+    /** @var string */
91
+    protected $privateKey;
92
+
93
+    /** @var string */
94
+    protected $publicKey;
95
+
96
+    /** @var int */
97
+    protected $version;
98
+
99
+    /** @var bool */
100
+    protected $passwordInvalid;
101
+
102
+    public function __construct() {
103
+        $this->addType('uid', 'string');
104
+        $this->addType('loginName', 'string');
105
+        $this->addType('password', 'string');
106
+        $this->addType('passwordHash', 'string');
107
+        $this->addType('name', 'string');
108
+        $this->addType('token', 'string');
109
+        $this->addType('type', 'int');
110
+        $this->addType('remember', 'int');
111
+        $this->addType('lastActivity', 'int');
112
+        $this->addType('lastCheck', 'int');
113
+        $this->addType('scope', 'string');
114
+        $this->addType('expires', 'int');
115
+        $this->addType('publicKey', 'string');
116
+        $this->addType('privateKey', 'string');
117
+        $this->addType('version', 'int');
118
+        $this->addType('passwordInvalid', 'bool');
119
+    }
120
+
121
+    public function getId(): int {
122
+        return $this->id;
123
+    }
124
+
125
+    public function getUID(): string {
126
+        return $this->uid;
127
+    }
128
+
129
+    /**
130
+     * Get the login name used when generating the token
131
+     *
132
+     * @return string
133
+     */
134
+    public function getLoginName(): string {
135
+        return parent::getLoginName();
136
+    }
137
+
138
+    /**
139
+     * Get the (encrypted) login password
140
+     *
141
+     * @return string|null
142
+     */
143
+    public function getPassword() {
144
+        return parent::getPassword();
145
+    }
146
+
147
+    public function jsonSerialize(): array {
148
+        return [
149
+            'id' => $this->id,
150
+            'name' => $this->name,
151
+            'lastActivity' => $this->lastActivity,
152
+            'type' => $this->type,
153
+            'scope' => $this->getScopeAsArray()
154
+        ];
155
+    }
156
+
157
+    /**
158
+     * Get the timestamp of the last password check
159
+     *
160
+     * @return int
161
+     */
162
+    public function getLastCheck(): int {
163
+        return parent::getLastCheck();
164
+    }
165
+
166
+    /**
167
+     * Get the timestamp of the last password check
168
+     *
169
+     * @param int $time
170
+     */
171
+    public function setLastCheck(int $time) {
172
+        parent::setLastCheck($time);
173
+    }
174
+
175
+    public function getScope(): string {
176
+        $scope = parent::getScope();
177
+        if ($scope === null) {
178
+            return '';
179
+        }
180
+
181
+        return $scope;
182
+    }
183
+
184
+    public function getScopeAsArray(): array {
185
+        $scope = json_decode($this->getScope(), true);
186
+        if (!$scope) {
187
+            return [
188
+                'filesystem' => true
189
+            ];
190
+        }
191
+        return $scope;
192
+    }
193
+
194
+    public function setScope($scope) {
195
+        if (is_array($scope)) {
196
+            parent::setScope(json_encode($scope));
197
+        } else {
198
+            parent::setScope((string)$scope);
199
+        }
200
+    }
201
+
202
+    public function getName(): string {
203
+        return parent::getName();
204
+    }
205
+
206
+    public function setName(string $name): void {
207
+        parent::setName($name);
208
+    }
209
+
210
+    public function getRemember(): int {
211
+        return parent::getRemember();
212
+    }
213
+
214
+    public function setToken(string $token) {
215
+        parent::setToken($token);
216
+    }
217
+
218
+    public function setPassword(string $password = null) {
219
+        parent::setPassword($password);
220
+    }
221
+
222
+    public function setExpires($expires) {
223
+        parent::setExpires($expires);
224
+    }
225
+
226
+    /**
227
+     * @return int|null
228
+     */
229
+    public function getExpires() {
230
+        return parent::getExpires();
231
+    }
232
+
233
+    public function setPasswordInvalid(bool $invalid) {
234
+        parent::setPasswordInvalid($invalid);
235
+    }
236
+
237
+    public function wipe(): void {
238
+        parent::setType(IToken::WIPE_TOKEN);
239
+    }
240 240
 }
Please login to merge, or discard this patch.
core/Migrations/Version25000Date20220905140840.php 1 patch
Indentation   +19 added lines, -19 removed lines patch added patch discarded remove patch
@@ -34,24 +34,24 @@
 block discarded – undo
34 34
 
35 35
 class Version25000Date20220905140840 extends SimpleMigrationStep {
36 36
 
37
-	/**
38
-	 * @param IOutput $output
39
-	 * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
40
-	 * @param array $options
41
-	 * @return null|ISchemaWrapper
42
-	 */
43
-	public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
44
-		/** @var ISchemaWrapper $schema */
45
-		$schema = $schemaClosure();
37
+    /**
38
+     * @param IOutput $output
39
+     * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
40
+     * @param array $options
41
+     * @return null|ISchemaWrapper
42
+     */
43
+    public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
44
+        /** @var ISchemaWrapper $schema */
45
+        $schema = $schemaClosure();
46 46
 
47
-		$authTokenTable = $schema->getTable('authtoken');
48
-		if (!$authTokenTable->hasColumn('password_hash')) {
49
-			$authTokenTable->addColumn('password_hash', Types::STRING, [
50
-				'notnull' => false,
51
-				'length' => 255,
52
-			]);
53
-			return $schema;
54
-		}
55
-		return null;
56
-	}
47
+        $authTokenTable = $schema->getTable('authtoken');
48
+        if (!$authTokenTable->hasColumn('password_hash')) {
49
+            $authTokenTable->addColumn('password_hash', Types::STRING, [
50
+                'notnull' => false,
51
+                'length' => 255,
52
+            ]);
53
+            return $schema;
54
+        }
55
+        return null;
56
+    }
57 57
 }
Please login to merge, or discard this patch.