Completed
Pull Request — master (#5174)
by Björn
17:50
created

Encryption::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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