Passed
Push — master ( 14dc97...6f0079 )
by Roeland
10:31
created

PublicKeyTokenProvider::logOpensslError()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
/**
4
 * @copyright Copyright 2018, Roeland Jago Douma <[email protected]>
5
 *
6
 * @author Roeland Jago Douma <[email protected]>
7
 *
8
 * @license AGPL-3.0
9
 *
10
 * This code is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU Affero General Public License, version 3,
12
 * as published by the Free Software Foundation.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU Affero General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU Affero General Public License, version 3,
20
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
21
 *
22
 */
23
24
namespace OC\Authentication\Token;
25
26
use OC\Authentication\Exceptions\ExpiredTokenException;
27
use OC\Authentication\Exceptions\InvalidTokenException;
28
use OC\Authentication\Exceptions\PasswordlessTokenException;
29
use OCP\AppFramework\Db\DoesNotExistException;
30
use OCP\AppFramework\Utility\ITimeFactory;
31
use OCP\IConfig;
32
use OCP\ILogger;
33
use OCP\Security\ICrypto;
34
35
class PublicKeyTokenProvider implements IProvider {
36
	/** @var PublicKeyTokenMapper */
37
	private $mapper;
38
39
	/** @var ICrypto */
40
	private $crypto;
41
42
	/** @var IConfig */
43
	private $config;
44
45
	/** @var ILogger $logger */
46
	private $logger;
47
48
	/** @var ITimeFactory $time */
49
	private $time;
50
51
	public function __construct(PublicKeyTokenMapper $mapper,
52
								ICrypto $crypto,
53
								IConfig $config,
54
								ILogger $logger,
55
								ITimeFactory $time) {
56
		$this->mapper = $mapper;
57
		$this->crypto = $crypto;
58
		$this->config = $config;
59
		$this->logger = $logger;
60
		$this->time = $time;
61
	}
62
63
	public function generateToken(string $token,
64
								  string $uid,
65
								  string $loginName,
66
								  $password,
67
								  string $name,
68
								  int $type = IToken::TEMPORARY_TOKEN,
69
								  int $remember = IToken::DO_NOT_REMEMBER): IToken {
70
		$dbToken = $this->newToken($token, $uid, $loginName, $password, $name, $type, $remember);
71
72
		$this->mapper->insert($dbToken);
73
74
		return $dbToken;
75
	}
76
77
	public function getToken(string $tokenId): IToken {
78
		try {
79
			$token = $this->mapper->getToken($this->hashToken($tokenId));
80
		} catch (DoesNotExistException $ex) {
81
			throw new InvalidTokenException();
82
		}
83
84
		if ((int)$token->getExpires() !== 0 && $token->getExpires() < $this->time->getTime()) {
85
			throw new ExpiredTokenException($token);
86
		}
87
88
		return $token;
89
	}
90
91
	public function getTokenById(int $tokenId): IToken {
92
		try {
93
			$token = $this->mapper->getTokenById($tokenId);
94
		} catch (DoesNotExistException $ex) {
95
			throw new InvalidTokenException();
96
		}
97
98
		if ((int)$token->getExpires() !== 0 && $token->getExpires() < $this->time->getTime()) {
99
			throw new ExpiredTokenException($token);
100
		}
101
102
		return $token;
103
	}
104
105
	public function renewSessionToken(string $oldSessionId, string $sessionId) {
106
		$token = $this->getToken($oldSessionId);
107
108
		if (!($token instanceof PublicKeyToken)) {
0 ignored issues
show
introduced by
$token is always a sub-type of OC\Authentication\Token\PublicKeyToken.
Loading history...
109
			throw new InvalidTokenException();
110
		}
111
112
		$password = null;
113
		if (!is_null($token->getPassword())) {
114
			$privateKey = $this->decrypt($token->getPrivateKey(), $oldSessionId);
115
			$password = $this->decryptPassword($token->getPassword(), $privateKey);
116
		}
117
118
		$this->generateToken(
119
			$sessionId,
120
			$token->getUID(),
121
			$token->getLoginName(),
122
			$password,
123
			$token->getName(),
124
			IToken::TEMPORARY_TOKEN,
125
			$token->getRemember()
126
		);
127
128
		$this->mapper->delete($token);
129
	}
130
131
	public function invalidateToken(string $token) {
132
		$this->mapper->invalidate($this->hashToken($token));
133
	}
134
135
	public function invalidateTokenById(string $uid, int $id) {
136
		$this->mapper->deleteById($uid, $id);
137
	}
138
139
	public function invalidateOldTokens() {
140
		$olderThan = $this->time->getTime() - (int) $this->config->getSystemValue('session_lifetime', 60 * 60 * 24);
141
		$this->logger->debug('Invalidating session tokens older than ' . date('c', $olderThan), ['app' => 'cron']);
142
		$this->mapper->invalidateOld($olderThan, IToken::DO_NOT_REMEMBER);
143
		$rememberThreshold = $this->time->getTime() - (int) $this->config->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15);
144
		$this->logger->debug('Invalidating remembered session tokens older than ' . date('c', $rememberThreshold), ['app' => 'cron']);
145
		$this->mapper->invalidateOld($rememberThreshold, IToken::REMEMBER);
146
	}
147
148
	public function updateToken(IToken $token) {
149
		if (!($token instanceof PublicKeyToken)) {
150
			throw new InvalidTokenException();
151
		}
152
		$this->mapper->update($token);
153
	}
154
155
	public function updateTokenActivity(IToken $token) {
156
		if (!($token instanceof PublicKeyToken)) {
157
			throw new InvalidTokenException();
158
		}
159
		/** @var DefaultToken $token */
160
		$now = $this->time->getTime();
161
		if ($token->getLastActivity() < ($now - 60)) {
162
			// Update token only once per minute
163
			$token->setLastActivity($now);
164
			$this->mapper->update($token);
165
		}
166
	}
167
168
	public function getTokenByUser(string $uid): array {
169
		return $this->mapper->getTokenByUser($uid);
170
	}
171
172
	public function getPassword(IToken $token, string $tokenId): string {
173
		if (!($token instanceof PublicKeyToken)) {
174
			throw new InvalidTokenException();
175
		}
176
177
		if ($token->getPassword() === null) {
178
			throw new PasswordlessTokenException();
179
		}
180
181
		// Decrypt private key with tokenId
182
		$privateKey = $this->decrypt($token->getPrivateKey(), $tokenId);
183
184
		// Decrypt password with private key
185
		return $this->decryptPassword($token->getPassword(), $privateKey);
186
	}
187
188
	public function setPassword(IToken $token, string $tokenId, string $password) {
189
		if (!($token instanceof PublicKeyToken)) {
190
			throw new InvalidTokenException();
191
		}
192
193
		// When changing passwords all temp tokens are deleted
194
		$this->mapper->deleteTempToken($token);
195
196
		// Update the password for all tokens
197
		$tokens = $this->mapper->getTokenByUser($token->getUID());
198
		foreach ($tokens as $t) {
199
			$publicKey = $t->getPublicKey();
200
			$t->setPassword($this->encryptPassword($password, $publicKey));
201
			$this->updateToken($t);
202
		}
203
	}
204
205
	public function rotate(IToken $token, string $oldTokenId, string $newTokenId): IToken {
206
		if (!($token instanceof PublicKeyToken)) {
207
			throw new InvalidTokenException();
208
		}
209
210
		// Decrypt private key with oldTokenId
211
		$privateKey = $this->decrypt($token->getPrivateKey(), $oldTokenId);
212
		// Encrypt with the new token
213
		$token->setPrivateKey($this->encrypt($privateKey, $newTokenId));
214
215
		$token->setToken($this->hashToken($newTokenId));
216
		$this->updateToken($token);
217
218
		return $token;
219
	}
220
221
	private function encrypt(string $plaintext, string $token): string {
222
		$secret = $this->config->getSystemValue('secret');
223
		return $this->crypto->encrypt($plaintext, $token . $secret);
224
	}
225
226
	/**
227
	 * @throws InvalidTokenException
228
	 */
229
	private function decrypt(string $cipherText, string $token): string {
230
		$secret = $this->config->getSystemValue('secret');
231
		try {
232
			return $this->crypto->decrypt($cipherText, $token . $secret);
233
		} catch (\Exception $ex) {
234
			// Delete the invalid token
235
			$this->invalidateToken($token);
236
			throw new InvalidTokenException();
237
		}
238
	}
239
240
	private function encryptPassword(string $password, string $publicKey): string {
241
		openssl_public_encrypt($password, $encryptedPassword, $publicKey, OPENSSL_PKCS1_OAEP_PADDING);
242
		$encryptedPassword = base64_encode($encryptedPassword);
243
244
		return $encryptedPassword;
245
	}
246
247
	private function decryptPassword(string $encryptedPassword, string $privateKey): string {
248
		$encryptedPassword = base64_decode($encryptedPassword);
249
		openssl_private_decrypt($encryptedPassword, $password, $privateKey, OPENSSL_PKCS1_OAEP_PADDING);
250
251
		return $password;
252
	}
253
254
	private function hashToken(string $token): string {
255
		$secret = $this->config->getSystemValue('secret');
256
		return hash('sha512', $token . $secret);
257
	}
258
259
	/**
260
	 * Convert a DefaultToken to a publicKeyToken
261
	 * This will also be updated directly in the Database
262
	 */
263
	public function convertToken(DefaultToken $defaultToken, string $token, $password): PublicKeyToken {
264
		$pkToken = $this->newToken(
265
			$token,
266
			$defaultToken->getUID(),
267
			$defaultToken->getLoginName(),
268
			$password,
269
			$defaultToken->getName(),
270
			$defaultToken->getType(),
271
			$defaultToken->getRemember()
272
		);
273
274
		$pkToken->setExpires($defaultToken->getExpires());
275
		$pkToken->setId($defaultToken->getId());
276
277
		return $this->mapper->update($pkToken);
278
	}
279
280
	private function newToken(string $token,
281
							  string $uid,
282
							  string $loginName,
283
							  $password,
284
							  string $name,
285
							  int $type,
286
							  int $remember): PublicKeyToken {
287
		$dbToken = new PublicKeyToken();
288
		$dbToken->setUid($uid);
289
		$dbToken->setLoginName($loginName);
290
291
		$config = array_merge([
292
			'digest_alg' => 'sha512',
293
			'private_key_bits' => 2048,
294
		], $this->config->getSystemValue('openssl', []));
295
296
		// Generate new key
297
		$res = openssl_pkey_new($config);
298
		if ($res === false) {
299
			$this->logOpensslError();
300
		}
301
302
		openssl_pkey_export($res, $privateKey);
303
304
		// Extract the public key from $res to $pubKey
305
		$publicKey = openssl_pkey_get_details($res);
306
		$publicKey = $publicKey['key'];
307
308
		$dbToken->setPublicKey($publicKey);
309
		$dbToken->setPrivateKey($this->encrypt($privateKey, $token));
310
311
		if (!is_null($password)) {
312
			$dbToken->setPassword($this->encryptPassword($password, $publicKey));
313
		}
314
315
		$dbToken->setName($name);
316
		$dbToken->setToken($this->hashToken($token));
317
		$dbToken->setType($type);
318
		$dbToken->setRemember($remember);
319
		$dbToken->setLastActivity($this->time->getTime());
320
		$dbToken->setLastCheck($this->time->getTime());
321
		$dbToken->setVersion(PublicKeyToken::VERSION);
322
323
		return $dbToken;
324
	}
325
326
	public function markPasswordInvalid(IToken $token, string $tokenId) {
327
		if (!($token instanceof PublicKeyToken)) {
328
			throw new InvalidTokenException();
329
		}
330
331
		$token->setPasswordInvalid(true);
332
		$this->mapper->update($token);
333
	}
334
335
	public function updatePasswords(string $uid, string $password) {
336
		if (!$this->mapper->hasExpiredTokens($uid)) {
337
			// Nothing to do here
338
			return;
339
		}
340
341
		// Update the password for all tokens
342
		$tokens = $this->mapper->getTokenByUser($uid);
343
		foreach ($tokens as $t) {
344
			$publicKey = $t->getPublicKey();
345
			$t->setPassword($this->encryptPassword($password, $publicKey));
346
			$this->updateToken($t);
347
		}
348
	}
349
350
	private function logOpensslError() {
351
		$errors = [];
352
		while ($error = openssl_error_string()) {
353
			$errors[] = $error;
354
		}
355
		$this->logger->critical('Something is wrong with your openssl setup: ' . implode(', ', $errors));
356
	}
357
}
358