Completed
Push — master ( 4826fd...c0e9b3 )
by Björn
17:02
created

Encryption::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 18

Duplication

Lines 19
Ratio 100 %

Importance

Changes 0
Metric Value
cc 1
eloc 18
nc 1
nop 8
dl 19
loc 19
rs 9.4285
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Björn Schießle <[email protected]>
6
 * @author Clark Tomlinson <[email protected]>
7
 * @author Jan-Christoph Borchardt <[email protected]>
8
 * @author Joas Schilling <[email protected]>
9
 * @author Lukas Reschke <[email protected]>
10
 * @author Thomas Müller <[email protected]>
11
 *
12
 * @license AGPL-3.0
13
 *
14
 * This code is free software: you can redistribute it and/or modify
15
 * it under the terms of the GNU Affero General Public License, version 3,
16
 * as published by the Free Software Foundation.
17
 *
18
 * This program is distributed in the hope that it will be useful,
19
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21
 * GNU Affero General Public License for more details.
22
 *
23
 * You should have received a copy of the GNU Affero General Public License, version 3,
24
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
25
 *
26
 */
27
28
namespace OCA\Encryption\Crypto;
29
30
31
use OC\Encryption\Exceptions\DecryptionFailedException;
32
use OC\Files\Cache\Scanner;
33
use OC\Files\View;
34
use OCA\Encryption\Exceptions\PublicKeyMissingException;
35
use OCA\Encryption\Session;
36
use OCA\Encryption\Util;
37
use OCP\Encryption\IEncryptionModule;
38
use OCA\Encryption\KeyManager;
39
use OCP\IL10N;
40
use OCP\ILogger;
41
use Symfony\Component\Console\Input\InputInterface;
42
use Symfony\Component\Console\Output\OutputInterface;
43
44
class Encryption implements IEncryptionModule {
45
46
	const ID = 'OC_DEFAULT_MODULE';
47
	const DISPLAY_NAME = 'Default encryption module';
48
49
	/**
50
	 * @var Crypt
51
	 */
52
	private $crypt;
53
54
	/** @var string */
55
	private $cipher;
56
57
	/** @var string */
58
	private $path;
59
60
	/** @var string */
61
	private $user;
62
63
	/** @var  array */
64
	private $owner;
65
66
	/** @var string */
67
	private $fileKey;
68
69
	/** @var string */
70
	private $writeCache;
71
72
	/** @var KeyManager */
73
	private $keyManager;
74
75
	/** @var array */
76
	private $accessList;
77
78
	/** @var boolean */
79
	private $isWriteOperation;
80
81
	/** @var Util */
82
	private $util;
83
84
	/** @var  Session */
85
	private $session;
86
87
	/** @var  ILogger */
88
	private $logger;
89
90
	/** @var IL10N */
91
	private $l;
92
93
	/** @var EncryptAll */
94
	private $encryptAll;
95
96
	/** @var  bool */
97
	private $useMasterPassword;
98
99
	/** @var DecryptAll  */
100
	private $decryptAll;
101
102
	/** @var int unencrypted block size if block contains signature */
103
	private $unencryptedBlockSizeSigned = 6072;
104
105
	/** @var int unencrypted block size */
106
	private $unencryptedBlockSize = 6126;
107
108
	/** @var int Current version of the file */
109
	private $version = 0;
110
111
	/** @var array remember encryption signature version */
112
	private static $rememberVersion = [];
113
114
115
	/**
116
	 *
117
	 * @param Crypt $crypt
118
	 * @param KeyManager $keyManager
119
	 * @param Util $util
120
	 * @param Session $session
121
	 * @param EncryptAll $encryptAll
122
	 * @param DecryptAll $decryptAll
123
	 * @param ILogger $logger
124
	 * @param IL10N $il10n
125
	 */
126 View Code Duplication
	public function __construct(Crypt $crypt,
127
								KeyManager $keyManager,
128
								Util $util,
129
								Session $session,
130
								EncryptAll $encryptAll,
131
								DecryptAll $decryptAll,
132
								ILogger $logger,
133
								IL10N $il10n) {
134
		$this->crypt = $crypt;
135
		$this->keyManager = $keyManager;
136
		$this->util = $util;
137
		$this->session = $session;
138
		$this->encryptAll = $encryptAll;
139
		$this->decryptAll = $decryptAll;
140
		$this->logger = $logger;
141
		$this->l = $il10n;
142
		$this->owner = [];
143
		$this->useMasterPassword = $util->isMasterKeyEnabled();
144
	}
145
146
	/**
147
	 * @return string defining the technical unique id
148
	 */
149
	public function getId() {
150
		return self::ID;
151
	}
152
153
	/**
154
	 * In comparison to getKey() this function returns a human readable (maybe translated) name
155
	 *
156
	 * @return string
157
	 */
158
	public function getDisplayName() {
159
		return self::DISPLAY_NAME;
160
	}
161
162
	/**
163
	 * start receiving chunks from a file. This is the place where you can
164
	 * perform some initial step before starting encrypting/decrypting the
165
	 * chunks
166
	 *
167
	 * @param string $path to the file
168
	 * @param string $user who read/write the file
169
	 * @param string $mode php stream open mode
170
	 * @param array $header contains the header data read from the file
171
	 * @param array $accessList who has access to the file contains the key 'users' and 'public'
172
	 *
173
	 * @return array $header contain data as key-value pairs which should be
174
	 *                       written to the header, in case of a write operation
175
	 *                       or if no additional data is needed return a empty array
176
	 */
177
	public function begin($path, $user, $mode, array $header, array $accessList) {
178
		$this->path = $this->getPathToRealFile($path);
179
		$this->accessList = $accessList;
180
		$this->user = $user;
181
		$this->isWriteOperation = false;
182
		$this->writeCache = '';
183
184
		if($this->session->isReady() === false) {
185
			// if the master key is enabled we can initialize encryption
186
			// with a empty password and user name
187
			if ($this->util->isMasterKeyEnabled()) {
188
				$this->keyManager->init('', '');
189
			}
190
		}
191
192
		if ($this->session->decryptAllModeActivated()) {
193
			$encryptedFileKey = $this->keyManager->getEncryptedFileKey($this->path);
194
			$shareKey = $this->keyManager->getShareKey($this->path, $this->session->getDecryptAllUid());
195
			$this->fileKey = $this->crypt->multiKeyDecrypt($encryptedFileKey,
196
				$shareKey,
197
				$this->session->getDecryptAllKey());
198
		} else {
199
			$this->fileKey = $this->keyManager->getFileKey($this->path, $this->user);
200
		}
201
202
		// always use the version from the original file, also part files
203
		// need to have a correct version number if they get moved over to the
204
		// final location
205
		$this->version = (int)$this->keyManager->getVersion($this->stripPartFileExtension($path), new View());
206
207
		if (
208
			$mode === 'w'
209
			|| $mode === 'w+'
210
			|| $mode === 'wb'
211
			|| $mode === 'wb+'
212
		) {
213
			$this->isWriteOperation = true;
214
			if (empty($this->fileKey)) {
215
				$this->fileKey = $this->crypt->generateFileKey();
216
			}
217
		} else {
218
			// if we read a part file we need to increase the version by 1
219
			// because the version number was also increased by writing
220
			// the part file
221
			if(Scanner::isPartialFile($path)) {
222
				$this->version = $this->version + 1;
223
			}
224
		}
225
226
		if ($this->isWriteOperation) {
227
			$this->cipher = $this->crypt->getCipher();
228
		} elseif (isset($header['cipher'])) {
229
			$this->cipher = $header['cipher'];
230
		} else {
231
			// if we read a file without a header we fall-back to the legacy cipher
232
			// which was used in <=oC6
233
			$this->cipher = $this->crypt->getLegacyCipher();
234
		}
235
236
		return array('cipher' => $this->cipher, 'signed' => 'true');
237
	}
238
239
	/**
240
	 * last chunk received. This is the place where you can perform some final
241
	 * operation and return some remaining data if something is left in your
242
	 * buffer.
243
	 *
244
	 * @param string $path to the file
245
	 * @param int $position
246
	 * @return string remained data which should be written to the file in case
247
	 *                of a write operation
248
	 * @throws PublicKeyMissingException
249
	 * @throws \Exception
250
	 * @throws \OCA\Encryption\Exceptions\MultiKeyEncryptException
251
	 */
252
	public function end($path, $position = 0) {
253
		$result = '';
254
		if ($this->isWriteOperation) {
255
			$this->keyManager->setVersion($path, $this->version + 1, new View());
256
			// in case of a part file we remember the new signature versions
257
			// the version will be set later on update.
258
			// This way we make sure that other apps listening to the pre-hooks
259
			// still get the old version which should be the correct value for them
260
			if (Scanner::isPartialFile($path)) {
261
				self::$rememberVersion[$this->stripPartFileExtension($path)] = $this->version + 1;
262
			}
263
			if (!empty($this->writeCache)) {
264
				$result = $this->crypt->symmetricEncryptFileContent($this->writeCache, $this->fileKey, $this->version + 1, $position);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression $this->crypt->symmetricE...ersion + 1, $position); of type false|string adds false to the return on line 291 which is incompatible with the return type declared by the interface OCP\Encryption\IEncryptionModule::end of type string. It seems like you forgot to handle an error condition.
Loading history...
265
				$this->writeCache = '';
266
			}
267
			$publicKeys = array();
268
			if ($this->useMasterPassword === true) {
269
				$publicKeys[$this->keyManager->getMasterKeyId()] = $this->keyManager->getPublicMasterKey();
270
			} else {
271
				foreach ($this->accessList['users'] as $uid) {
272
					try {
273
						$publicKeys[$uid] = $this->keyManager->getPublicKey($uid);
274
					} catch (PublicKeyMissingException $e) {
275
						$this->logger->warning(
276
							'no public key found for user "{uid}", user will not be able to read the file',
277
							['app' => 'encryption', 'uid' => $uid]
278
						);
279
						// if the public key of the owner is missing we should fail
280
						if ($uid === $this->user) {
281
							throw $e;
282
						}
283
					}
284
				}
285
			}
286
287
			$publicKeys = $this->keyManager->addSystemKeys($this->accessList, $publicKeys, $this->getOwner($path));
288
			$encryptedKeyfiles = $this->crypt->multiKeyEncrypt($this->fileKey, $publicKeys);
289
			$this->keyManager->setAllFileKeys($this->path, $encryptedKeyfiles);
290
		}
291
		return $result;
292
	}
293
294
295
296
	/**
297
	 * encrypt data
298
	 *
299
	 * @param string $data you want to encrypt
300
	 * @param int $position
301
	 * @return string encrypted data
302
	 */
303
	public function encrypt($data, $position = 0) {
304
		// If extra data is left over from the last round, make sure it
305
		// is integrated into the next block
306
		if ($this->writeCache) {
307
308
			// Concat writeCache to start of $data
309
			$data = $this->writeCache . $data;
310
311
			// Clear the write cache, ready for reuse - it has been
312
			// flushed and its old contents processed
313
			$this->writeCache = '';
314
315
		}
316
317
		$encrypted = '';
318
		// While there still remains some data to be processed & written
319
		while (strlen($data) > 0) {
320
321
			// Remaining length for this iteration, not of the
322
			// entire file (may be greater than 8192 bytes)
323
			$remainingLength = strlen($data);
324
325
			// If data remaining to be written is less than the
326
			// size of 1 6126 byte block
327
			if ($remainingLength < $this->unencryptedBlockSizeSigned) {
328
329
				// Set writeCache to contents of $data
330
				// The writeCache will be carried over to the
331
				// next write round, and added to the start of
332
				// $data to ensure that written blocks are
333
				// always the correct length. If there is still
334
				// data in writeCache after the writing round
335
				// has finished, then the data will be written
336
				// to disk by $this->flush().
337
				$this->writeCache = $data;
338
339
				// Clear $data ready for next round
340
				$data = '';
341
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
		}
357
358
		return $encrypted;
359
	}
360
361
	/**
362
	 * decrypt data
363
	 *
364
	 * @param string $data you want to decrypt
365
	 * @param int $position
366
	 * @return string decrypted data
367
	 * @throws DecryptionFailedException
368
	 */
369
	public function decrypt($data, $position = 0) {
370
		if (empty($this->fileKey)) {
371
			$msg = 'Can not decrypt this file, probably this is a shared file. Please ask the file owner to reshare the file with you.';
372
			$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.');
373
			$this->logger->error($msg);
374
375
			throw new DecryptionFailedException($msg, $hint);
376
		}
377
378
		return $this->crypt->symmetricDecryptFileContent($data, $this->fileKey, $this->cipher, $this->version, $position);
379
	}
380
381
	/**
382
	 * update encrypted file, e.g. give additional users access to the file
383
	 *
384
	 * @param string $path path to the file which should be updated
385
	 * @param string $uid of the user who performs the operation
386
	 * @param array $accessList who has access to the file contains the key 'users' and 'public'
387
	 * @return boolean
388
	 */
389
	public function update($path, $uid, array $accessList) {
390
391
		if (empty($accessList)) {
392
			if (isset(self::$rememberVersion[$path])) {
393
				$this->keyManager->setVersion($path, self::$rememberVersion[$path], new View());
394
				unset(self::$rememberVersion[$path]);
395
			}
396
			return;
397
		}
398
399
		$fileKey = $this->keyManager->getFileKey($path, $uid);
400
401
		if (!empty($fileKey)) {
402
403
			$publicKeys = array();
404
			if ($this->useMasterPassword === true) {
405
				$publicKeys[$this->keyManager->getMasterKeyId()] = $this->keyManager->getPublicMasterKey();
406
			} else {
407
				foreach ($accessList['users'] as $user) {
408
					try {
409
						$publicKeys[$user] = $this->keyManager->getPublicKey($user);
410
					} catch (PublicKeyMissingException $e) {
411
						$this->logger->warning('Could not encrypt file for ' . $user . ': ' . $e->getMessage());
412
					}
413
				}
414
			}
415
416
			$publicKeys = $this->keyManager->addSystemKeys($accessList, $publicKeys, $this->getOwner($path));
417
418
			$encryptedFileKey = $this->crypt->multiKeyEncrypt($fileKey, $publicKeys);
419
420
			$this->keyManager->deleteAllFileKeys($path);
421
422
			$this->keyManager->setAllFileKeys($path, $encryptedFileKey);
423
424
		} else {
425
			$this->logger->debug('no file key found, we assume that the file "{file}" is not encrypted',
426
				array('file' => $path, 'app' => 'encryption'));
427
428
			return false;
429
		}
430
431
		return true;
432
	}
433
434
	/**
435
	 * should the file be encrypted or not
436
	 *
437
	 * @param string $path
438
	 * @return boolean
439
	 */
440
	public function shouldEncrypt($path) {
441
		if ($this->util->shouldEncryptHomeStorage() === false) {
442
			$storage = $this->util->getStorage($path);
443
			if ($storage->instanceOfStorage('\OCP\Files\IHomeStorage')) {
444
				return false;
445
			}
446
		}
447
		$parts = explode('/', $path);
448
		if (count($parts) < 4) {
449
			return false;
450
		}
451
452
		if ($parts[2] == 'files') {
453
			return true;
454
		}
455
		if ($parts[2] == 'files_versions') {
456
			return true;
457
		}
458
		if ($parts[2] == 'files_trashbin') {
459
			return true;
460
		}
461
462
		return false;
463
	}
464
465
	/**
466
	 * get size of the unencrypted payload per block.
467
	 * Nextcloud read/write files with a block size of 8192 byte
468
	 *
469
	 * @param bool $signed
470
	 * @return int
471
	 */
472
	public function getUnencryptedBlockSize($signed = false) {
473
		if ($signed === false) {
474
			return $this->unencryptedBlockSize;
475
		}
476
477
		return $this->unencryptedBlockSizeSigned;
478
	}
479
480
	/**
481
	 * check if the encryption module is able to read the file,
482
	 * e.g. if all encryption keys exists
483
	 *
484
	 * @param string $path
485
	 * @param string $uid user for whom we want to check if he can read the file
486
	 * @return bool
487
	 * @throws DecryptionFailedException
488
	 */
489
	public function isReadable($path, $uid) {
490
		$fileKey = $this->keyManager->getFileKey($path, $uid);
491
		if (empty($fileKey)) {
492
			$owner = $this->util->getOwner($path);
493
			if ($owner !== $uid) {
494
				// if it is a shared file we throw a exception with a useful
495
				// error message because in this case it means that the file was
496
				// shared with the user at a point where the user didn't had a
497
				// valid private/public key
498
				$msg = 'Encryption module "' . $this->getDisplayName() .
499
					'" is not able to read ' . $path;
500
				$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.');
501
				$this->logger->warning($msg);
502
				throw new DecryptionFailedException($msg, $hint);
503
			}
504
			return false;
505
		}
506
507
		return true;
508
	}
509
510
	/**
511
	 * Initial encryption of all files
512
	 *
513
	 * @param InputInterface $input
514
	 * @param OutputInterface $output write some status information to the terminal during encryption
515
	 */
516
	public function encryptAll(InputInterface $input, OutputInterface $output) {
517
		$this->encryptAll->encryptAll($input, $output);
518
	}
519
520
	/**
521
	 * prepare module to perform decrypt all operation
522
	 *
523
	 * @param InputInterface $input
524
	 * @param OutputInterface $output
525
	 * @param string $user
526
	 * @return bool
527
	 */
528
	public function prepareDecryptAll(InputInterface $input, OutputInterface $output, $user = '') {
529
		return $this->decryptAll->prepare($input, $output, $user);
530
	}
531
532
533
	/**
534
	 * @param string $path
535
	 * @return string
536
	 */
537
	protected function getPathToRealFile($path) {
538
		$realPath = $path;
539
		$parts = explode('/', $path);
540
		if ($parts[2] === 'files_versions') {
541
			$realPath = '/' . $parts[1] . '/files/' . implode('/', array_slice($parts, 3));
542
			$length = strrpos($realPath, '.');
543
			$realPath = substr($realPath, 0, $length);
544
		}
545
546
		return $realPath;
547
	}
548
549
	/**
550
	 * remove .part file extension and the ocTransferId from the file to get the
551
	 * original file name
552
	 *
553
	 * @param string $path
554
	 * @return string
555
	 */
556
	protected function stripPartFileExtension($path) {
557
		if (pathinfo($path, PATHINFO_EXTENSION) === 'part') {
558
			$pos = strrpos($path, '.', -6);
559
			$path = substr($path, 0, $pos);
560
		}
561
562
		return $path;
563
	}
564
565
	/**
566
	 * get owner of a file
567
	 *
568
	 * @param string $path
569
	 * @return string
570
	 */
571
	protected function getOwner($path) {
572
		if (!isset($this->owner[$path])) {
573
			$this->owner[$path] = $this->util->getOwner($path);
574
		}
575
		return $this->owner[$path];
576
	}
577
578
	/**
579
	 * Check if the module is ready to be used by that specific user.
580
	 * In case a module is not ready - because e.g. key pairs have not been generated
581
	 * upon login this method can return false before any operation starts and might
582
	 * cause issues during operations.
583
	 *
584
	 * @param string $user
585
	 * @return boolean
586
	 * @since 9.1.0
587
	 */
588
	public function isReadyForUser($user) {
589
		return $this->keyManager->userHasKeys($user);
590
	}
591
592
	/**
593
	 * We only need a detailed access list if the master key is not enabled
594
	 *
595
	 * @return bool
596
	 */
597
	public function needDetailedAccessList() {
598
		return !$this->util->isMasterKeyEnabled();
599
	}
600
}
601