Passed
Push — master ( 06ae9c...7c30d1 )
by Roeland
13:11 queued 11s
created

Encryption   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 549
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 188
c 0
b 0
f 0
dl 0
loc 549
rs 3.6
wmc 60

18 Methods

Rating   Name   Duplication   Size   Complexity  
C begin() 0 60 12
A __construct() 0 18 1
A getId() 0 2 1
B end() 0 39 8
A getDisplayName() 0 2 1
A isReadable() 0 19 3
A decrypt() 0 10 2
A getPathToRealFile() 0 10 2
A getOwner() 0 5 2
A encrypt() 0 52 4
A encryptAll() 0 2 1
A stripPartFileExtension() 0 7 2
B update() 0 40 7
A needDetailedAccessList() 0 2 1
A prepareDecryptAll() 0 2 1
A getUnencryptedBlockSize() 0 6 2
A isReadyForUser() 0 5 2
B shouldEncrypt() 0 23 8

How to fix   Complexity   

Complex Class

Complex classes like Encryption often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Encryption, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Bjoern Schiessle <[email protected]>
6
 * @author Björn Schießle <[email protected]>
7
 * @author Christoph Wurst <[email protected]>
8
 * @author Clark Tomlinson <[email protected]>
9
 * @author Jan-Christoph Borchardt <[email protected]>
10
 * @author Joas Schilling <[email protected]>
11
 * @author Julius Härtl <[email protected]>
12
 * @author Lukas Reschke <[email protected]>
13
 * @author Morris Jobke <[email protected]>
14
 * @author Roeland Jago Douma <[email protected]>
15
 * @author Thomas Müller <[email protected]>
16
 *
17
 * @license AGPL-3.0
18
 *
19
 * This code is free software: you can redistribute it and/or modify
20
 * it under the terms of the GNU Affero General Public License, version 3,
21
 * as published by the Free Software Foundation.
22
 *
23
 * This program is distributed in the hope that it will be useful,
24
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
25
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26
 * GNU Affero General Public License for more details.
27
 *
28
 * You should have received a copy of the GNU Affero General Public License, version 3,
29
 * along with this program. If not, see <http://www.gnu.org/licenses/>
30
 *
31
 */
32
33
namespace OCA\Encryption\Crypto;
34
35
use OC\Encryption\Exceptions\DecryptionFailedException;
36
use OC\Files\Cache\Scanner;
37
use OC\Files\View;
38
use OCA\Encryption\Exceptions\PublicKeyMissingException;
39
use OCA\Encryption\KeyManager;
40
use OCA\Encryption\Session;
41
use OCA\Encryption\Util;
42
use OCP\Encryption\IEncryptionModule;
43
use OCP\IL10N;
44
use OCP\ILogger;
45
use Symfony\Component\Console\Input\InputInterface;
46
use Symfony\Component\Console\Output\OutputInterface;
47
48
class Encryption implements IEncryptionModule {
49
	public const ID = 'OC_DEFAULT_MODULE';
50
	public const DISPLAY_NAME = 'Default encryption module';
51
52
	/**
53
	 * @var Crypt
54
	 */
55
	private $crypt;
56
57
	/** @var string */
58
	private $cipher;
59
60
	/** @var string */
61
	private $path;
62
63
	/** @var string */
64
	private $user;
65
66
	/** @var  array */
67
	private $owner;
68
69
	/** @var string */
70
	private $fileKey;
71
72
	/** @var string */
73
	private $writeCache;
74
75
	/** @var KeyManager */
76
	private $keyManager;
77
78
	/** @var array */
79
	private $accessList;
80
81
	/** @var boolean */
82
	private $isWriteOperation;
83
84
	/** @var Util */
85
	private $util;
86
87
	/** @var  Session */
88
	private $session;
89
90
	/** @var  ILogger */
91
	private $logger;
92
93
	/** @var IL10N */
94
	private $l;
95
96
	/** @var EncryptAll */
97
	private $encryptAll;
98
99
	/** @var  bool */
100
	private $useMasterPassword;
101
102
	/** @var DecryptAll  */
103
	private $decryptAll;
104
105
	/** @var int unencrypted block size if block contains signature */
106
	private $unencryptedBlockSizeSigned = 6072;
107
108
	/** @var int unencrypted block size */
109
	private $unencryptedBlockSize = 6126;
110
111
	/** @var int Current version of the file */
112
	private $version = 0;
113
114
	/** @var array remember encryption signature version */
115
	private static $rememberVersion = [];
116
117
118
	/**
119
	 *
120
	 * @param Crypt $crypt
121
	 * @param KeyManager $keyManager
122
	 * @param Util $util
123
	 * @param Session $session
124
	 * @param EncryptAll $encryptAll
125
	 * @param DecryptAll $decryptAll
126
	 * @param ILogger $logger
127
	 * @param IL10N $il10n
128
	 */
129
	public function __construct(Crypt $crypt,
130
								KeyManager $keyManager,
131
								Util $util,
132
								Session $session,
133
								EncryptAll $encryptAll,
134
								DecryptAll $decryptAll,
135
								ILogger $logger,
136
								IL10N $il10n) {
137
		$this->crypt = $crypt;
138
		$this->keyManager = $keyManager;
139
		$this->util = $util;
140
		$this->session = $session;
141
		$this->encryptAll = $encryptAll;
142
		$this->decryptAll = $decryptAll;
143
		$this->logger = $logger;
144
		$this->l = $il10n;
145
		$this->owner = [];
146
		$this->useMasterPassword = $util->isMasterKeyEnabled();
147
	}
148
149
	/**
150
	 * @return string defining the technical unique id
151
	 */
152
	public function getId() {
153
		return self::ID;
154
	}
155
156
	/**
157
	 * In comparison to getKey() this function returns a human readable (maybe translated) name
158
	 *
159
	 * @return string
160
	 */
161
	public function getDisplayName() {
162
		return self::DISPLAY_NAME;
163
	}
164
165
	/**
166
	 * start receiving chunks from a file. This is the place where you can
167
	 * perform some initial step before starting encrypting/decrypting the
168
	 * chunks
169
	 *
170
	 * @param string $path to the file
171
	 * @param string $user who read/write the file
172
	 * @param string $mode php stream open mode
173
	 * @param array $header contains the header data read from the file
174
	 * @param array $accessList who has access to the file contains the key 'users' and 'public'
175
	 *
176
	 * @return array $header contain data as key-value pairs which should be
177
	 *                       written to the header, in case of a write operation
178
	 *                       or if no additional data is needed return a empty array
179
	 */
180
	public function begin($path, $user, $mode, array $header, array $accessList) {
181
		$this->path = $this->getPathToRealFile($path);
182
		$this->accessList = $accessList;
183
		$this->user = $user;
184
		$this->isWriteOperation = false;
185
		$this->writeCache = '';
186
187
		if ($this->session->isReady() === false) {
188
			// if the master key is enabled we can initialize encryption
189
			// with a empty password and user name
190
			if ($this->util->isMasterKeyEnabled()) {
191
				$this->keyManager->init('', '');
192
			}
193
		}
194
195
		if ($this->session->decryptAllModeActivated()) {
196
			$encryptedFileKey = $this->keyManager->getEncryptedFileKey($this->path);
197
			$shareKey = $this->keyManager->getShareKey($this->path, $this->session->getDecryptAllUid());
198
			$this->fileKey = $this->crypt->multiKeyDecrypt($encryptedFileKey,
199
				$shareKey,
200
				$this->session->getDecryptAllKey());
201
		} else {
202
			$this->fileKey = $this->keyManager->getFileKey($this->path, $this->user);
203
		}
204
205
		// always use the version from the original file, also part files
206
		// need to have a correct version number if they get moved over to the
207
		// final location
208
		$this->version = (int)$this->keyManager->getVersion($this->stripPartFileExtension($path), new View());
209
210
		if (
211
			$mode === 'w'
212
			|| $mode === 'w+'
213
			|| $mode === 'wb'
214
			|| $mode === 'wb+'
215
		) {
216
			$this->isWriteOperation = true;
217
			if (empty($this->fileKey)) {
218
				$this->fileKey = $this->crypt->generateFileKey();
219
			}
220
		} else {
221
			// if we read a part file we need to increase the version by 1
222
			// because the version number was also increased by writing
223
			// the part file
224
			if (Scanner::isPartialFile($path)) {
225
				$this->version = $this->version + 1;
226
			}
227
		}
228
229
		if ($this->isWriteOperation) {
230
			$this->cipher = $this->crypt->getCipher();
231
		} elseif (isset($header['cipher'])) {
232
			$this->cipher = $header['cipher'];
233
		} else {
234
			// if we read a file without a header we fall-back to the legacy cipher
235
			// which was used in <=oC6
236
			$this->cipher = $this->crypt->getLegacyCipher();
237
		}
238
239
		return ['cipher' => $this->cipher, 'signed' => 'true'];
240
	}
241
242
	/**
243
	 * last chunk received. This is the place where you can perform some final
244
	 * operation and return some remaining data if something is left in your
245
	 * buffer.
246
	 *
247
	 * @param string $path to the file
248
	 * @param int $position
249
	 * @return string remained data which should be written to the file in case
250
	 *                of a write operation
251
	 * @throws PublicKeyMissingException
252
	 * @throws \Exception
253
	 * @throws \OCA\Encryption\Exceptions\MultiKeyEncryptException
254
	 */
255
	public function end($path, $position = 0) {
256
		$result = '';
257
		if ($this->isWriteOperation) {
258
			// in case of a part file we remember the new signature versions
259
			// the version will be set later on update.
260
			// This way we make sure that other apps listening to the pre-hooks
261
			// still get the old version which should be the correct value for them
262
			if (Scanner::isPartialFile($path)) {
263
				self::$rememberVersion[$this->stripPartFileExtension($path)] = $this->version + 1;
264
			}
265
			if (!empty($this->writeCache)) {
266
				$result = $this->crypt->symmetricEncryptFileContent($this->writeCache, $this->fileKey, $this->version + 1, $position);
267
				$this->writeCache = '';
268
			}
269
			$publicKeys = [];
270
			if ($this->useMasterPassword === true) {
271
				$publicKeys[$this->keyManager->getMasterKeyId()] = $this->keyManager->getPublicMasterKey();
272
			} else {
273
				foreach ($this->accessList['users'] as $uid) {
274
					try {
275
						$publicKeys[$uid] = $this->keyManager->getPublicKey($uid);
276
					} catch (PublicKeyMissingException $e) {
277
						$this->logger->warning(
278
							'no public key found for user "{uid}", user will not be able to read the file',
279
							['app' => 'encryption', 'uid' => $uid]
280
						);
281
						// if the public key of the owner is missing we should fail
282
						if ($uid === $this->user) {
283
							throw $e;
284
						}
285
					}
286
				}
287
			}
288
289
			$publicKeys = $this->keyManager->addSystemKeys($this->accessList, $publicKeys, $this->getOwner($path));
290
			$encryptedKeyfiles = $this->crypt->multiKeyEncrypt($this->fileKey, $publicKeys);
291
			$this->keyManager->setAllFileKeys($this->path, $encryptedKeyfiles);
292
		}
293
		return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result could also return false which is incompatible with the documented return type string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
294
	}
295
296
297
298
	/**
299
	 * encrypt data
300
	 *
301
	 * @param string $data you want to encrypt
302
	 * @param int $position
303
	 * @return string encrypted data
304
	 */
305
	public function encrypt($data, $position = 0) {
306
		// If extra data is left over from the last round, make sure it
307
		// is integrated into the next block
308
		if ($this->writeCache) {
309
310
			// Concat writeCache to start of $data
311
			$data = $this->writeCache . $data;
312
313
			// Clear the write cache, ready for reuse - it has been
314
			// flushed and its old contents processed
315
			$this->writeCache = '';
316
		}
317
318
		$encrypted = '';
319
		// While there still remains some data to be processed & written
320
		while (strlen($data) > 0) {
321
322
			// Remaining length for this iteration, not of the
323
			// entire file (may be greater than 8192 bytes)
324
			$remainingLength = strlen($data);
325
326
			// If data remaining to be written is less than the
327
			// size of 1 6126 byte block
328
			if ($remainingLength < $this->unencryptedBlockSizeSigned) {
329
330
				// Set writeCache to contents of $data
331
				// The writeCache will be carried over to the
332
				// next write round, and added to the start of
333
				// $data to ensure that written blocks are
334
				// always the correct length. If there is still
335
				// data in writeCache after the writing round
336
				// has finished, then the data will be written
337
				// to disk by $this->flush().
338
				$this->writeCache = $data;
339
340
				// Clear $data ready for next round
341
				$data = '';
342
			} else {
343
344
				// Read the chunk from the start of $data
345
				$chunk = substr($data, 0, $this->unencryptedBlockSizeSigned);
346
347
				$encrypted .= $this->crypt->symmetricEncryptFileContent($chunk, $this->fileKey, $this->version + 1, $position);
348
349
				// Remove the chunk we just processed from
350
				// $data, leaving only unprocessed data in $data
351
				// var, for handling on the next round
352
				$data = substr($data, $this->unencryptedBlockSizeSigned);
353
			}
354
		}
355
356
		return $encrypted;
357
	}
358
359
	/**
360
	 * decrypt data
361
	 *
362
	 * @param string $data you want to decrypt
363
	 * @param int|string $position
364
	 * @return string decrypted data
365
	 * @throws DecryptionFailedException
366
	 */
367
	public function decrypt($data, $position = 0) {
368
		if (empty($this->fileKey)) {
369
			$msg = 'Can not decrypt this file, probably this is a shared file. Please ask the file owner to reshare the file with you.';
370
			$hint = $this->l->t('Can not decrypt this file, probably this is a shared file. Please ask the file owner to reshare the file with you.');
371
			$this->logger->error($msg);
372
373
			throw new DecryptionFailedException($msg, $hint);
374
		}
375
376
		return $this->crypt->symmetricDecryptFileContent($data, $this->fileKey, $this->cipher, $this->version, $position);
377
	}
378
379
	/**
380
	 * update encrypted file, e.g. give additional users access to the file
381
	 *
382
	 * @param string $path path to the file which should be updated
383
	 * @param string $uid of the user who performs the operation
384
	 * @param array $accessList who has access to the file contains the key 'users' and 'public'
385
	 * @return boolean
386
	 */
387
	public function update($path, $uid, array $accessList) {
388
		if (empty($accessList)) {
389
			if (isset(self::$rememberVersion[$path])) {
390
				$this->keyManager->setVersion($path, self::$rememberVersion[$path], new View());
391
				unset(self::$rememberVersion[$path]);
392
			}
393
			return;
394
		}
395
396
		$fileKey = $this->keyManager->getFileKey($path, $uid);
397
398
		if (!empty($fileKey)) {
399
			$publicKeys = [];
400
			if ($this->useMasterPassword === true) {
401
				$publicKeys[$this->keyManager->getMasterKeyId()] = $this->keyManager->getPublicMasterKey();
402
			} else {
403
				foreach ($accessList['users'] as $user) {
404
					try {
405
						$publicKeys[$user] = $this->keyManager->getPublicKey($user);
406
					} catch (PublicKeyMissingException $e) {
407
						$this->logger->warning('Could not encrypt file for ' . $user . ': ' . $e->getMessage());
408
					}
409
				}
410
			}
411
412
			$publicKeys = $this->keyManager->addSystemKeys($accessList, $publicKeys, $this->getOwner($path));
413
414
			$encryptedFileKey = $this->crypt->multiKeyEncrypt($fileKey, $publicKeys);
415
416
			$this->keyManager->deleteAllFileKeys($path);
417
418
			$this->keyManager->setAllFileKeys($path, $encryptedFileKey);
419
		} else {
420
			$this->logger->debug('no file key found, we assume that the file "{file}" is not encrypted',
421
				['file' => $path, 'app' => 'encryption']);
422
423
			return false;
424
		}
425
426
		return true;
427
	}
428
429
	/**
430
	 * should the file be encrypted or not
431
	 *
432
	 * @param string $path
433
	 * @return boolean
434
	 */
435
	public function shouldEncrypt($path) {
436
		if ($this->util->shouldEncryptHomeStorage() === false) {
437
			$storage = $this->util->getStorage($path);
438
			if ($storage && $storage->instanceOfStorage('\OCP\Files\IHomeStorage')) {
439
				return false;
440
			}
441
		}
442
		$parts = explode('/', $path);
443
		if (count($parts) < 4) {
444
			return false;
445
		}
446
447
		if ($parts[2] === 'files') {
448
			return true;
449
		}
450
		if ($parts[2] === 'files_versions') {
451
			return true;
452
		}
453
		if ($parts[2] === 'files_trashbin') {
454
			return true;
455
		}
456
457
		return false;
458
	}
459
460
	/**
461
	 * get size of the unencrypted payload per block.
462
	 * Nextcloud read/write files with a block size of 8192 byte
463
	 *
464
	 * @param bool $signed
465
	 * @return int
466
	 */
467
	public function getUnencryptedBlockSize($signed = false) {
468
		if ($signed === false) {
469
			return $this->unencryptedBlockSize;
470
		}
471
472
		return $this->unencryptedBlockSizeSigned;
473
	}
474
475
	/**
476
	 * check if the encryption module is able to read the file,
477
	 * e.g. if all encryption keys exists
478
	 *
479
	 * @param string $path
480
	 * @param string $uid user for whom we want to check if he can read the file
481
	 * @return bool
482
	 * @throws DecryptionFailedException
483
	 */
484
	public function isReadable($path, $uid) {
485
		$fileKey = $this->keyManager->getFileKey($path, $uid);
486
		if (empty($fileKey)) {
487
			$owner = $this->util->getOwner($path);
488
			if ($owner !== $uid) {
489
				// if it is a shared file we throw a exception with a useful
490
				// error message because in this case it means that the file was
491
				// shared with the user at a point where the user didn't had a
492
				// valid private/public key
493
				$msg = 'Encryption module "' . $this->getDisplayName() .
494
					'" is not able to read ' . $path;
495
				$hint = $this->l->t('Can not read this file, probably this is a shared file. Please ask the file owner to reshare the file with you.');
496
				$this->logger->warning($msg);
497
				throw new DecryptionFailedException($msg, $hint);
498
			}
499
			return false;
500
		}
501
502
		return true;
503
	}
504
505
	/**
506
	 * Initial encryption of all files
507
	 *
508
	 * @param InputInterface $input
509
	 * @param OutputInterface $output write some status information to the terminal during encryption
510
	 */
511
	public function encryptAll(InputInterface $input, OutputInterface $output) {
512
		$this->encryptAll->encryptAll($input, $output);
513
	}
514
515
	/**
516
	 * prepare module to perform decrypt all operation
517
	 *
518
	 * @param InputInterface $input
519
	 * @param OutputInterface $output
520
	 * @param string $user
521
	 * @return bool
522
	 */
523
	public function prepareDecryptAll(InputInterface $input, OutputInterface $output, $user = '') {
524
		return $this->decryptAll->prepare($input, $output, $user);
525
	}
526
527
528
	/**
529
	 * @param string $path
530
	 * @return string
531
	 */
532
	protected function getPathToRealFile($path) {
533
		$realPath = $path;
534
		$parts = explode('/', $path);
535
		if ($parts[2] === 'files_versions') {
536
			$realPath = '/' . $parts[1] . '/files/' . implode('/', array_slice($parts, 3));
537
			$length = strrpos($realPath, '.');
538
			$realPath = substr($realPath, 0, $length);
539
		}
540
541
		return $realPath;
542
	}
543
544
	/**
545
	 * remove .part file extension and the ocTransferId from the file to get the
546
	 * original file name
547
	 *
548
	 * @param string $path
549
	 * @return string
550
	 */
551
	protected function stripPartFileExtension($path) {
552
		if (pathinfo($path, PATHINFO_EXTENSION) === 'part') {
553
			$pos = strrpos($path, '.', -6);
554
			$path = substr($path, 0, $pos);
555
		}
556
557
		return $path;
558
	}
559
560
	/**
561
	 * get owner of a file
562
	 *
563
	 * @param string $path
564
	 * @return string
565
	 */
566
	protected function getOwner($path) {
567
		if (!isset($this->owner[$path])) {
568
			$this->owner[$path] = $this->util->getOwner($path);
569
		}
570
		return $this->owner[$path];
571
	}
572
573
	/**
574
	 * Check if the module is ready to be used by that specific user.
575
	 * In case a module is not ready - because e.g. key pairs have not been generated
576
	 * upon login this method can return false before any operation starts and might
577
	 * cause issues during operations.
578
	 *
579
	 * @param string $user
580
	 * @return boolean
581
	 * @since 9.1.0
582
	 */
583
	public function isReadyForUser($user) {
584
		if ($this->util->isMasterKeyEnabled()) {
585
			return true;
586
		}
587
		return $this->keyManager->userHasKeys($user);
588
	}
589
590
	/**
591
	 * We only need a detailed access list if the master key is not enabled
592
	 *
593
	 * @return bool
594
	 */
595
	public function needDetailedAccessList() {
596
		return !$this->util->isMasterKeyEnabled();
597
	}
598
}
599