Completed
Push — master ( b8492c...7ef722 )
by Blizzz
158:19 queued 134:06
created

PublicKeyTokenProvider::getToken()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 13
Code Lines 8

Duplication

Lines 13
Ratio 100 %

Importance

Changes 0
Metric Value
cc 4
eloc 8
nc 3
nop 1
dl 13
loc 13
rs 9.2
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\InvalidTokenException;
27
use OC\Authentication\Exceptions\PasswordlessTokenException;
28
use OCP\AppFramework\Db\DoesNotExistException;
29
use OCP\AppFramework\Utility\ITimeFactory;
30
use OCP\IConfig;
31
use OCP\ILogger;
32
use OCP\Security\ICrypto;
33
34
class PublicKeyTokenProvider implements IProvider {
35
	/** @var PublicKeyTokenMapper */
36
	private $mapper;
37
38
	/** @var ICrypto */
39
	private $crypto;
40
41
	/** @var IConfig */
42
	private $config;
43
44
	/** @var ILogger $logger */
45
	private $logger;
46
47
	/** @var ITimeFactory $time */
48
	private $time;
49
50 View Code Duplication
	public function __construct(PublicKeyTokenMapper $mapper,
51
								ICrypto $crypto,
52
								IConfig $config,
53
								ILogger $logger,
54
								ITimeFactory $time) {
55
		$this->mapper = $mapper;
56
		$this->crypto = $crypto;
57
		$this->config = $config;
58
		$this->logger = $logger;
59
		$this->time = $time;
60
	}
61
62
	public function generateToken(string $token,
63
								  string $uid,
64
								  string $loginName,
65
								  $password,
66
								  string $name,
67
								  int $type = IToken::TEMPORARY_TOKEN,
68
								  int $remember = IToken::DO_NOT_REMEMBER): IToken {
69
		$dbToken = $this->newToken($token, $uid, $loginName, $password, $name, $type, $remember);
70
71
		$this->mapper->insert($dbToken);
72
73
		return $dbToken;
74
	}
75
76 View Code Duplication
	public function getToken(string $tokenId): IToken {
77
		try {
78
			$token = $this->mapper->getToken($this->hashToken($tokenId));
79
		} catch (DoesNotExistException $ex) {
80
			throw new InvalidTokenException();
81
		}
82
83
		if ($token->getExpires() !== null && $token->getExpires() < $this->time->getTime()) {
84
			throw new ExpiredTokenException($token);
85
		}
86
87
		return $token;
88
	}
89
90 View Code Duplication
	public function getTokenById(int $tokenId): IToken {
91
		try {
92
			$token = $this->mapper->getTokenById($tokenId);
93
		} catch (DoesNotExistException $ex) {
94
			throw new InvalidTokenException();
95
		}
96
97
		if ($token->getExpires() !== null && $token->getExpires() < $this->time->getTime()) {
98
			throw new ExpiredTokenException($token);
99
		}
100
101
		return $token;
102
	}
103
104
	public function renewSessionToken(string $oldSessionId, string $sessionId) {
105
		$token = $this->getToken($oldSessionId);
106
107
		if (!($token instanceof PublicKeyToken)) {
108
			throw new InvalidTokenException();
109
		}
110
111
		$password = null;
112
		if (!is_null($token->getPassword())) {
113
			$privateKey = $this->decrypt($token->getPrivateKey(), $oldSessionId);
114
			$password = $this->decryptPassword($token->getPassword(), $privateKey);
115
		}
116
117
		$this->generateToken(
118
			$sessionId,
119
			$token->getUID(),
120
			$token->getLoginName(),
121
			$password,
122
			$token->getName(),
123
			IToken::TEMPORARY_TOKEN,
124
			$token->getRemember()
125
		);
126
127
		$this->mapper->delete($token);
128
	}
129
130
	public function invalidateToken(string $token) {
131
		$this->mapper->invalidate($this->hashToken($token));
132
	}
133
134
	public function invalidateTokenById(string $uid, int $id) {
135
		$this->mapper->deleteById($uid, $id);
136
	}
137
138 View Code Duplication
	public function invalidateOldTokens() {
139
		$olderThan = $this->time->getTime() - (int) $this->config->getSystemValue('session_lifetime', 60 * 60 * 24);
140
		$this->logger->debug('Invalidating session tokens older than ' . date('c', $olderThan), ['app' => 'cron']);
141
		$this->mapper->invalidateOld($olderThan, IToken::DO_NOT_REMEMBER);
142
		$rememberThreshold = $this->time->getTime() - (int) $this->config->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15);
143
		$this->logger->debug('Invalidating remembered session tokens older than ' . date('c', $rememberThreshold), ['app' => 'cron']);
144
		$this->mapper->invalidateOld($rememberThreshold, IToken::REMEMBER);
145
	}
146
147
	public function updateToken(IToken $token) {
148
		if (!($token instanceof PublicKeyToken)) {
149
			throw new InvalidTokenException();
150
		}
151
		$this->mapper->update($token);
152
	}
153
154 View Code Duplication
	public function updateTokenActivity(IToken $token) {
155
		if (!($token instanceof PublicKeyToken)) {
156
			throw new InvalidTokenException();
157
		}
158
		/** @var DefaultToken $token */
159
		$now = $this->time->getTime();
160
		if ($token->getLastActivity() < ($now - 60)) {
161
			// Update token only once per minute
162
			$token->setLastActivity($now);
163
			$this->mapper->update($token);
164
		}
165
	}
166
167
	public function getTokenByUser(string $uid): array {
168
		return $this->mapper->getTokenByUser($uid);
169
	}
170
171
	public function getPassword(IToken $token, string $tokenId): string {
172
		if (!($token instanceof PublicKeyToken)) {
173
			throw new InvalidTokenException();
174
		}
175
176
		if ($token->getPassword() === null) {
177
			throw new PasswordlessTokenException();
178
		}
179
180
		// Decrypt private key with tokenId
181
		$privateKey = $this->decrypt($token->getPrivateKey(), $tokenId);
182
183
		// Decrypt password with private key
184
		return $this->decryptPassword($token->getPassword(), $privateKey);
185
	}
186
187
	public function setPassword(IToken $token, string $tokenId, string $password) {
188
		if (!($token instanceof PublicKeyToken)) {
189
			throw new InvalidTokenException();
190
		}
191
192
		// When changing passwords all temp tokens are deleted
193
		$this->mapper->deleteTempToken($token);
194
195
		// Update the password for all tokens
196
		$tokens = $this->mapper->getTokenByUser($token->getUID());
197
		foreach ($tokens as $t) {
198
			$publicKey = $t->getPublicKey();
199
			$t->setPassword($this->encryptPassword($password, $publicKey));
200
			$this->updateToken($t);
201
		}
202
	}
203
204
	public function rotate(IToken $token, string $oldTokenId, string $newTokenId): IToken {
205
		if (!($token instanceof PublicKeyToken)) {
206
			throw new InvalidTokenException();
207
		}
208
209
		// Decrypt private key with oldTokenId
210
		$privateKey = $this->decrypt($token->getPrivateKey(), $oldTokenId);
211
		// Encrypt with the new token
212
		$token->setPrivateKey($this->encrypt($privateKey, $newTokenId));
213
214
		$token->setToken($this->hashToken($newTokenId));
215
		$this->updateToken($token);
216
217
		return $token;
218
	}
219
220
	private function encrypt(string $plaintext, string $token): string {
221
		$secret = $this->config->getSystemValue('secret');
222
		return $this->crypto->encrypt($plaintext, $token . $secret);
223
	}
224
225
	/**
226
	 * @throws InvalidTokenException
227
	 */
228 View Code Duplication
	private function decrypt(string $cipherText, string $token): string {
229
		$secret = $this->config->getSystemValue('secret');
230
		try {
231
			return $this->crypto->decrypt($cipherText, $token . $secret);
232
		} catch (\Exception $ex) {
233
			// Delete the invalid token
234
			$this->invalidateToken($token);
235
			throw new InvalidTokenException();
236
		}
237
	}
238
239
	private function encryptPassword(string $password, string $publicKey): string {
240
		openssl_public_encrypt($password, $encryptedPassword, $publicKey, OPENSSL_PKCS1_OAEP_PADDING);
241
		$encryptedPassword = base64_encode($encryptedPassword);
242
243
		return $encryptedPassword;
244
	}
245
246
	private function decryptPassword(string $encryptedPassword, string $privateKey): string {
247
		$encryptedPassword = base64_decode($encryptedPassword);
248
		openssl_private_decrypt($encryptedPassword, $password, $privateKey, OPENSSL_PKCS1_OAEP_PADDING);
249
250
		return $password;
251
	}
252
253
	private function hashToken(string $token): string {
254
		$secret = $this->config->getSystemValue('secret');
255
		return hash('sha512', $token . $secret);
256
	}
257
258
	/**
259
	 * Convert a DefaultToken to a publicKeyToken
260
	 * This will also be updated directly in the Database
261
	 */
262
	public function convertToken(DefaultToken $defaultToken, string $token, $password): PublicKeyToken {
263
		$pkToken = $this->newToken(
264
			$token,
265
			$defaultToken->getUID(),
266
			$defaultToken->getLoginName(),
267
			$password,
268
			$defaultToken->getName(),
269
			$defaultToken->getType(),
270
			$defaultToken->getRemember()
271
		);
272
273
		$pkToken->setExpires($defaultToken->getExpires());
274
		$pkToken->setId($defaultToken->getId());
275
276
		return $this->mapper->update($pkToken);
277
	}
278
279
	private function newToken(string $token,
280
							  string $uid,
281
							  string $loginName,
282
							  $password,
283
							  string $name,
284
							  int $type,
285
							  int $remember): PublicKeyToken {
286
		$dbToken = new PublicKeyToken();
287
		$dbToken->setUid($uid);
288
		$dbToken->setLoginName($loginName);
289
290
		$config = [
291
			'digest_alg' => 'sha512',
292
			'private_key_bits' => 2048,
293
		];
294
295
		// Generate new key
296
		$res = openssl_pkey_new($config);
297
		openssl_pkey_export($res, $privateKey);
298
299
		// Extract the public key from $res to $pubKey
300
		$publicKey = openssl_pkey_get_details($res);
301
		$publicKey = $publicKey['key'];
302
303
		$dbToken->setPublicKey($publicKey);
304
		$dbToken->setPrivateKey($this->encrypt($privateKey, $token));
305
306
		if (!is_null($password)) {
307
			$dbToken->setPassword($this->encryptPassword($password, $publicKey));
308
		}
309
310
		$dbToken->setName($name);
311
		$dbToken->setToken($this->hashToken($token));
312
		$dbToken->setType($type);
313
		$dbToken->setRemember($remember);
314
		$dbToken->setLastActivity($this->time->getTime());
315
		$dbToken->setLastCheck($this->time->getTime());
316
		$dbToken->setVersion(PublicKeyToken::VERSION);
317
318
		return $dbToken;
319
	}
320
}
321