Encryption::fopen()   F
last analyzed

Complexity

Conditions 30
Paths 6589

Size

Total Lines 119

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 30
nc 6589
nop 2
dl 0
loc 119
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @author Björn Schießle <[email protected]>
4
 * @author Joas Schilling <[email protected]>
5
 * @author Lukas Reschke <[email protected]>
6
 * @author Robin Appelman <[email protected]>
7
 * @author Thomas Müller <[email protected]>
8
 * @author Vincent Petry <[email protected]>
9
 *
10
 * @copyright Copyright (c) 2018, ownCloud GmbH
11
 * @license AGPL-3.0
12
 *
13
 * This code is free software: you can redistribute it and/or modify
14
 * it under the terms of the GNU Affero General Public License, version 3,
15
 * as published by the Free Software Foundation.
16
 *
17
 * This program is distributed in the hope that it will be useful,
18
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20
 * GNU Affero General Public License for more details.
21
 *
22
 * You should have received a copy of the GNU Affero General Public License, version 3,
23
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
24
 *
25
 */
26
27
namespace OC\Files\Storage\Wrapper;
28
29
use OC\Encryption\Exceptions\ModuleDoesNotExistsException;
30
use OC\Encryption\Update;
31
use OC\Encryption\Util;
32
use OC\Files\Cache\CacheEntry;
33
use OC\Files\Filesystem;
34
use OC\Files\Mount\Manager;
35
use OC\Files\Storage\LocalTempFileTrait;
36
use OC\Memcache\ArrayCache;
37
use OCP\Encryption\Exceptions\GenericEncryptionException;
38
use OCP\Encryption\IFile;
39
use OCP\Encryption\IManager;
40
use OCP\Encryption\Keys\IStorage;
41
use OCP\Files\Mount\IMountPoint;
42
use OCP\Files\Storage;
43
use OCP\ILogger;
44
use OCP\Files\Cache\ICacheEntry;
45
46
class Encryption extends Wrapper {
47
	use LocalTempFileTrait;
48
49
	/** @var string */
50
	private $mountPoint;
51
52
	/** @var \OC\Encryption\Util */
53
	private $util;
54
55
	/** @var \OCP\Encryption\IManager */
56
	private $encryptionManager;
57
58
	/** @var \OCP\ILogger */
59
	private $logger;
60
61
	/** @var string */
62
	private $uid;
63
64
	/** @var array */
65
	protected $unencryptedSize;
66
67
	/** @var \OCP\Encryption\IFile */
68
	private $fileHelper;
69
70
	/** @var IMountPoint */
71
	private $mount;
72
73
	/** @var IStorage */
74
	private $keyStorage;
75
76
	/** @var Update */
77
	private $update;
78
79
	/** @var Manager */
80
	private $mountManager;
81
82
	/** @var array remember for which path we execute the repair step to avoid recursions */
83
	private $fixUnencryptedSizeOf = [];
84
85
	/** @var  ArrayCache */
86
	private $arrayCache;
87
88
	/** @var array which has information of sourcePath during rename operation */
89
	private $sourcePath;
90
91
	private static $disableWriteEncryption = false;
92
93
	/**
94
	 * @param array $parameters
95
	 * @param IManager $encryptionManager
96
	 * @param Util $util
97
	 * @param ILogger $logger
98
	 * @param IFile $fileHelper
99
	 * @param string $uid
100
	 * @param IStorage $keyStorage
101
	 * @param Update $update
102
	 * @param Manager $mountManager
103
	 * @param ArrayCache $arrayCache
104
	 */
105
	public function __construct(
106
			$parameters,
107
			IManager $encryptionManager = null,
108
			Util $util = null,
109
			ILogger $logger = null,
110
			IFile $fileHelper = null,
111
			$uid = null,
112
			IStorage $keyStorage = null,
113
			Update $update = null,
114
			Manager $mountManager = null,
115
			ArrayCache $arrayCache = null
116
		) {
117
		$this->mountPoint = $parameters['mountPoint'];
118
		$this->mount = $parameters['mount'];
119
		$this->encryptionManager = $encryptionManager;
120
		$this->util = $util;
121
		$this->logger = $logger;
122
		$this->uid = $uid;
123
		$this->fileHelper = $fileHelper;
124
		$this->keyStorage = $keyStorage;
125
		$this->unencryptedSize = [];
126
		$this->update = $update;
127
		$this->mountManager = $mountManager;
128
		$this->arrayCache = $arrayCache;
129
		parent::__construct($parameters);
130
	}
131
132
	/**
133
	 * see http://php.net/manual/en/function.filesize.php
134
	 * The result for filesize when called on a folder is required to be 0
135
	 *
136
	 * @param string $path
137
	 * @return int
138
	 */
139
	public function filesize($path) {
140
		$fullPath = $this->getFullPath($path);
141
142
		/** @var CacheEntry $info */
143
		$info = $this->getCache()->get($path);
144
		if (isset($this->unencryptedSize[$fullPath])) {
145
			$size = $this->unencryptedSize[$fullPath];
146
			// update file cache
147
			if ($info instanceof ICacheEntry) {
148
				$info = $info->getData();
149
				$info['encrypted'] = $info['encryptedVersion'];
150
			} else {
151
				if (!\is_array($info)) {
152
					$info = [];
153
				}
154
				$info['encrypted'] = true;
155
			}
156
157
			$info['size'] = $size;
158
			$this->getCache()->put($path, $info);
159
160
			return $size;
161
		}
162
163
		if (isset($info['fileid']) && $info['encrypted']) {
164
			return $this->verifyUnencryptedSize($path, $info['size']);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->verifyUnencrypted...($path, $info['size']); of type integer|double adds the type double to the return on line 164 which is incompatible with the return type declared by the interface OCP\Files\Storage\IStorage::filesize of type integer|false.
Loading history...
165
		}
166
167
		return $this->storage->filesize($path);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression $this->storage->filesize($path); of type integer|false adds false to the return on line 167 which is incompatible with the return type documented by OC\Files\Storage\Wrapper\Encryption::filesize of type integer. It seems like you forgot to handle an error condition.
Loading history...
168
	}
169
170
	/**
171
	 * @param string $path
172
	 * @return array
173
	 */
174
	public function getMetaData($path) {
175
		$data = $this->storage->getMetaData($path);
176
		if ($data === null) {
177
			return null;
178
		}
179
		$fullPath = $this->getFullPath($path);
180
		$info = $this->getCache()->get($path);
181
182
		if (isset($this->unencryptedSize[$fullPath])) {
183
			$data['encrypted'] = true;
184
			$data['size'] = $this->unencryptedSize[$fullPath];
185
		} else {
186
			if (isset($info['fileid']) && $info['encrypted']) {
187
				$data['size'] = $this->verifyUnencryptedSize($path, $info['size']);
188
				$data['encrypted'] = true;
189
			}
190
		}
191
192
		if (isset($info['encryptedVersion']) && $info['encryptedVersion'] > 1) {
193
			$data['encryptedVersion'] = $info['encryptedVersion'];
194
		}
195
196
		return $data;
197
	}
198
199
	/**
200
	 * see http://php.net/manual/en/function.file_get_contents.php
201
	 *
202
	 * @param string $path
203
	 * @return string
204
	 */
205
	public function file_get_contents($path) {
206
		if ($this->encryptionManager->isEnabled() !== false) {
207
			$encryptionModule = $this->getEncryptionModule($path);
208
209
			if ($encryptionModule) {
210
				$handle = $this->fopen($path, "r");
211
				if (!$handle) {
212
					return false;
213
				}
214
				$data = \stream_get_contents($handle);
215
				\fclose($handle);
216
				return $data;
217
			}
218
		}
219
		return $this->storage->file_get_contents($path);
220
	}
221
222
	/**
223
	 * see http://php.net/manual/en/function.file_put_contents.php
224
	 *
225
	 * @param string $path
226
	 * @param string $data
227
	 * @return bool
228
	 */
229
	public function file_put_contents($path, $data) {
230
		// file put content will always be translated to a stream write
231
		$handle = $this->fopen($path, 'w');
232
		if (\is_resource($handle)) {
233
			$written = \fwrite($handle, $data);
234
			\fclose($handle);
235
			return $written;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $written; (integer) is incompatible with the return type declared by the interface OCP\Files\Storage\IStorage::file_put_contents of type boolean.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
236
		}
237
238
		return false;
239
	}
240
241
	/**
242
	 * see http://php.net/manual/en/function.unlink.php
243
	 *
244
	 * @param string $path
245
	 * @return bool
246
	 */
247
	public function unlink($path) {
248
		$fullPath = $this->getFullPath($path);
249
		if ($this->util->isExcluded($fullPath)) {
250
			return $this->storage->unlink($path);
251
		}
252
253
		$encryptionModule = ($this->encryptionManager->isEnabled()) ? $this->getEncryptionModule($path) : "";
254
		if ($encryptionModule) {
255
			$this->keyStorage->deleteAllFileKeys($this->getFullPath($path));
256
		}
257
258
		return $this->storage->unlink($path);
259
	}
260
261
	/**
262
	 * see http://php.net/manual/en/function.rename.php
263
	 *
264
	 * @param string $path1
265
	 * @param string $path2
266
	 * @return bool
267
	 */
268
	public function rename($path1, $path2) {
269
		$result = $this->storage->rename($path1, $path2);
270
271
		if ($result &&
272
			// versions always use the keys from the original file, so we can skip
273
			// this step for versions
274
			$this->isVersion($path2) === false &&
275
			$this->encryptionManager->isEnabled()) {
276
			$source = $this->getFullPath($path1);
277
			if (!$this->util->isExcluded($source)) {
278
				$target = $this->getFullPath($path2);
279
				if (isset($this->unencryptedSize[$source])) {
280
					$this->unencryptedSize[$target] = $this->unencryptedSize[$source];
281
				}
282
				$this->keyStorage->renameKeys($source, $target);
283
				$module = $this->getEncryptionModule($path2);
284
				if ($module) {
285
					$module->update($target, $this->uid, []);
286
				}
287
			}
288
		}
289
290
		return $result;
291
	}
292
293
	/**
294
	 * see http://php.net/manual/en/function.rmdir.php
295
	 *
296
	 * @param string $path
297
	 * @return bool
298
	 */
299
	public function rmdir($path) {
300
		$result = $this->storage->rmdir($path);
301
		$fullPath = $this->getFullPath($path);
302
		if ($result &&
303
			$this->util->isExcluded($fullPath) === false &&
304
			$this->encryptionManager->isEnabled()
305
		) {
306
			$this->keyStorage->deleteAllFileKeys($fullPath);
307
		}
308
309
		return $result;
310
	}
311
312
	/**
313
	 * check if a file can be read
314
	 *
315
	 * @param string $path
316
	 * @return bool
317
	 */
318
	public function isReadable($path) {
319
		$isReadable = true;
320
321
		$metaData = $this->getMetaData($path);
322
		if (
323
			!$this->is_dir($path) &&
324
			isset($metaData['encrypted']) &&
325
			$metaData['encrypted'] === true
326
		) {
327
			$fullPath = $this->getFullPath($path);
328
			$module = $this->getEncryptionModule($path);
329
			$isReadable = $module->isReadable($fullPath, $this->uid);
330
		}
331
332
		return $this->storage->isReadable($path) && $isReadable;
333
	}
334
335
	/**
336
	 * see http://php.net/manual/en/function.copy.php
337
	 *
338
	 * @param string $path1
339
	 * @param string $path2
340
	 * @return bool
341
	 */
342
	public function copy($path1, $path2) {
343
		$source = $this->getFullPath($path1);
344
345
		if ($this->util->isExcluded($source)) {
346
			return $this->storage->copy($path1, $path2);
347
		}
348
349
		// need to stream copy file by file in case we copy between a encrypted
350
		// and a unencrypted storage
351
		$this->unlink($path2);
352
		$result = $this->copyFromStorage($this, $path1, $path2);
353
354
		return $result;
355
	}
356
357
	/**
358
	 * see http://php.net/manual/en/function.fopen.php
359
	 *
360
	 * @param string $path
361
	 * @param string $mode
362
	 * @return resource|bool
363
	 * @throws GenericEncryptionException
364
	 * @throws ModuleDoesNotExistsException
365
	 */
366
	public function fopen($path, $mode) {
367
368
		// check if the file is stored in the array cache, this means that we
369
		// copy a file over to the versions folder, in this case we don't want to
370
		// decrypt it
371
		if ($this->arrayCache->hasKey('encryption_copy_version_' . $path)) {
372
			$this->arrayCache->remove('encryption_copy_version_' . $path);
373
			return $this->storage->fopen($path, $mode);
374
		}
375
376
		$encryptionEnabled = $this->encryptionManager->isEnabled();
377
		$shouldEncrypt = false;
378
		$encryptionModule = null;
379
		$header = $this->getHeader($path);
380
		$signed = (isset($header['signed']) && $header['signed'] === 'true') ? true : false;
381
		$fullPath = $this->getFullPath($path);
382
		$encryptionModuleId = $this->util->getEncryptionModuleId($header);
383
384
		if ($this->util->isExcluded($fullPath) === false) {
385
			$size = $unencryptedSize = 0;
386
			$realFile = $this->util->stripPartialFileExtension($path);
387
			$targetExists = $this->file_exists($realFile) || $this->file_exists($path);
388
			$targetIsEncrypted = false;
389
			if ($targetExists) {
390
				// in case the file exists we require the explicit module as
391
				// specified in the file header - otherwise we need to fail hard to
392
				// prevent data loss on client side
393
				if (!empty($encryptionModuleId)) {
394
					$targetIsEncrypted = true;
395
					$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
396
				}
397
398
				if ($this->file_exists($path)) {
399
					$size = $this->storage->filesize($path);
400
					$unencryptedSize = $this->filesize($path);
401
				} else {
402
					$size = $unencryptedSize = 0;
403
				}
404
			}
405
406
			try {
407
				if (
408
					$mode === 'w'
409
					|| $mode === 'w+'
410
					|| $mode === 'wb'
411
					|| $mode === 'wb+'
412
				) {
413
					// don't overwrite encrypted files if encryption is not enabled
414
					if ($targetIsEncrypted && $encryptionEnabled === false) {
415
						throw new GenericEncryptionException('Tried to access encrypted file but encryption is not enabled');
416
					}
417
					if ($encryptionEnabled) {
418
						// if $encryptionModuleId is empty, the default module will be used
419
						$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
420
						$shouldEncrypt = $encryptionModule->shouldEncrypt($fullPath);
421
						$signed = true;
422
					}
423
				} else {
424
					$info = $this->getCache()->get($path);
425
					// only get encryption module if we found one in the header
426
					// or if file should be encrypted according to the file cache
427
					if (!empty($encryptionModuleId)) {
428
						$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
429
						$shouldEncrypt = true;
430
					} elseif (empty($encryptionModuleId) && $info['encrypted'] === true) {
431
						// we come from a old installation. No header and/or no module defined
432
						// but the file is encrypted. In this case we need to use the
433
						// OC_DEFAULT_MODULE to read the file
434
						$encryptionModule = $this->encryptionManager->getEncryptionModule('OC_DEFAULT_MODULE');
435
						$shouldEncrypt = true;
436
						$targetIsEncrypted = true;
437
					}
438
				}
439
			} catch (ModuleDoesNotExistsException $e) {
440
				$this->logger->warning('Encryption module "' . $encryptionModuleId .
441
					'" not found, file will be stored unencrypted (' . $e->getMessage() . ')');
442
			}
443
444
			// encryption disabled on write of new file and write to existing unencrypted file -> don't encrypt
445
			if (!$encryptionEnabled || !$this->mount->getOption('encrypt', true)) {
446
				if (!$targetExists || !$targetIsEncrypted) {
447
					$shouldEncrypt = false;
448
				}
449
			}
450
451
			if ($shouldEncrypt === true && $encryptionModule !== null) {
452
				/**
453
				 * The check of $disableWriteEncryption, required to get the file in the decrypted state.
454
				 * It will help us get the normal file handler. And hence we can re-encrypt
455
				 * the file when necessary, later. The true/false of $getDecryptedFile decides whether
456
				 * to keep the file decrypted or not. The intention is to get the data decrypt
457
				 * for write mode.
458
				 */
459
				if (self::$disableWriteEncryption && ($mode !== 'r')) {
460
					return $this->getWrapperStorage()->fopen($path, $mode);
461
				}
462
463
				$headerSize = $this->getHeaderSize($path);
464
				$source = $this->storage->fopen($path, $mode);
465
				if (!\is_resource($source)) {
466
					return false;
467
				}
468
469
				if (isset($this->sourcePath[$path])) {
470
					$sourceFileOfRename = $this->sourcePath[$path];
471
				} else {
472
					$sourceFileOfRename = null;
473
				}
474
				$handle = \OC\Files\Stream\Encryption::wrap($source, $path, $fullPath, $header,
475
					$this->uid, $encryptionModule, $this->storage, $this, $this->util, $this->fileHelper, $mode,
0 ignored issues
show
Compatibility introduced by
$this->fileHelper of type object<OCP\Encryption\IFile> is not a sub-type of object<OC\Encryption\File>. It seems like you assume a concrete implementation of the interface OCP\Encryption\IFile to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
476
					$size, $unencryptedSize, $headerSize, $signed, $sourceFileOfRename);
0 ignored issues
show
Security Bug introduced by
It seems like $size defined by $this->storage->filesize($path) on line 399 can also be of type false; however, OC\Files\Stream\Encryption::wrap() does only seem to accept integer, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
477
				unset($this->sourcePath[$path]);
478
479
				return $handle;
480
			}
481
		}
482
483
		return $this->storage->fopen($path, $mode);
484
	}
485
486
	/**
487
	 * perform some plausibility checks if the the unencrypted size is correct.
488
	 * If not, we calculate the correct unencrypted size and return it
489
	 *
490
	 * @param string $path internal path relative to the storage root
491
	 * @param int $unencryptedSize size of the unencrypted file
492
	 *
493
	 * @return int unencrypted size
494
	 */
495
	protected function verifyUnencryptedSize($path, $unencryptedSize) {
496
		$size = $this->storage->filesize($path);
497
		$result = $unencryptedSize;
498
499
		if ($unencryptedSize < 0 ||
500
			($size > 0 && $unencryptedSize === $size)
501
		) {
502
			// check if we already calculate the unencrypted size for the
503
			// given path to avoid recursions
504
			if (isset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]) === false) {
505
				$this->fixUnencryptedSizeOf[$this->getFullPath($path)] = true;
506
				try {
507
					$result = $this->fixUnencryptedSize($path, $size, $unencryptedSize);
0 ignored issues
show
Security Bug introduced by
It seems like $size defined by $this->storage->filesize($path) on line 496 can also be of type false; however, OC\Files\Storage\Wrapper...n::fixUnencryptedSize() does only seem to accept integer, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
508
				} catch (\Exception $e) {
509
					$this->logger->error('Couldn\'t re-calculate unencrypted size for '. $path);
510
					$this->logger->logException($e);
511
				}
512
				unset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]);
513
			}
514
		}
515
516
		return $result;
517
	}
518
519
	/**
520
	 * calculate the unencrypted size
521
	 *
522
	 * @param string $path internal path relative to the storage root
523
	 * @param int $size size of the physical file
524
	 * @param int $unencryptedSize size of the unencrypted file
525
	 *
526
	 * @return int calculated unencrypted size
527
	 */
528
	protected function fixUnencryptedSize($path, $size, $unencryptedSize) {
529
		$headerSize = $this->getHeaderSize($path);
530
		$header = $this->getHeader($path);
531
		$encryptionModule = $this->getEncryptionModule($path);
532
533
		$stream = $this->storage->fopen($path, 'r');
534
535
		// if we couldn't open the file we return the old unencrypted size
536
		if (!\is_resource($stream)) {
537
			$this->logger->error('Could not open ' . $path . '. Recalculation of unencrypted size aborted.');
538
			return $unencryptedSize;
539
		}
540
541
		$newUnencryptedSize = 0;
542
		$size -= $headerSize;
543
		$blockSize = $this->util->getBlockSize();
544
545
		// if a header exists we skip it
546
		if ($headerSize > 0) {
547
			\fread($stream, $headerSize);
548
		}
549
550
		// fast path, else the calculation for $lastChunkNr is bogus
551
		if ($size === 0) {
552
			return 0;
553
		}
554
555
		$signed = (isset($header['signed']) && $header['signed'] === 'true') ? true : false;
556
		$unencryptedBlockSize = $encryptionModule->getUnencryptedBlockSize($signed);
557
558
		// calculate last chunk nr
559
		// next highest is end of chunks, one subtracted is last one
560
		// we have to read the last chunk, we can't just calculate it (because of padding etc)
561
562
		$lastChunkNr = \ceil($size/ $blockSize)-1;
563
		// calculate last chunk position
564
		$lastChunkPos = ($lastChunkNr * $blockSize);
565
		// try to fseek to the last chunk, if it fails we have to read the whole file
566
		if (@\fseek($stream, $lastChunkPos, SEEK_CUR) === 0) {
567
			$newUnencryptedSize += $lastChunkNr * $unencryptedBlockSize;
568
		}
569
570
		$lastChunkContentEncrypted='';
571
		$count = $blockSize;
572
573
		while ($count > 0) {
574
			$data=\fread($stream, $blockSize);
575
			$count=\strlen($data);
576
			$lastChunkContentEncrypted .= $data;
577
			if (\strlen($lastChunkContentEncrypted) > $blockSize) {
578
				$newUnencryptedSize += $unencryptedBlockSize;
579
				$lastChunkContentEncrypted=\substr($lastChunkContentEncrypted, $blockSize);
580
			}
581
		}
582
583
		\fclose($stream);
584
585
		// we have to decrypt the last chunk to get it actual size
586
		$encryptionModule->begin($this->getFullPath($path), $this->uid, 'r', $header, [], null);
587
		$decryptedLastChunk = $encryptionModule->decrypt($lastChunkContentEncrypted, $lastChunkNr . 'end');
588
		$decryptedLastChunk .= $encryptionModule->end($this->getFullPath($path), $lastChunkNr . 'end');
589
590
		// calc the real file size with the size of the last chunk
591
		$newUnencryptedSize += \strlen($decryptedLastChunk);
592
593
		$this->updateUnencryptedSize($this->getFullPath($path), $newUnencryptedSize);
594
595
		// write to cache if applicable
596
		$cache = $this->storage->getCache();
597
		if ($cache) {
598
			$entry = $cache->get($path);
599
			$cache->update($entry['fileid'], ['size' => $newUnencryptedSize]);
600
		}
601
602
		return $newUnencryptedSize;
603
	}
604
605
	/**
606
	 * @param Storage $sourceStorage
607
	 * @param string $sourceInternalPath
608
	 * @param string $targetInternalPath
609
	 * @param bool $preserveMtime
610
	 * @return bool
611
	 * @throws \Exception
612
	 */
613
	public function moveFromStorage(Storage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = true) {
614
		if ($sourceStorage === $this) {
615
			return $this->rename($sourceInternalPath, $targetInternalPath);
616
		}
617
618
		// TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
619
		// - call $this->storage->moveFromStorage() instead of $this->copyBetweenStorage
620
		// - copy the file cache update from  $this->copyBetweenStorage to this method
621
		// - copy the copyKeys() call from  $this->copyBetweenStorage to this method
622
		// - remove $this->copyBetweenStorage
623
624
		if (!$sourceStorage->isDeletable($sourceInternalPath)) {
625
			return false;
626
		}
627
628
		if (!$this->encryptionManager->isEnabled()) {
629
			return $this->storage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime);
0 ignored issues
show
Unused Code introduced by
The call to Storage::moveFromStorage() has too many arguments starting with $preserveMtime.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
630
		}
631
		$result = $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, true);
632
		if ($result) {
633
			if ($sourceStorage->is_dir($sourceInternalPath)) {
634
				$result &= $sourceStorage->rmdir($sourceInternalPath);
635
			} else {
636
				$result &= $sourceStorage->unlink($sourceInternalPath);
637
			}
638
		}
639
		return $result;
640
	}
641
642
	/**
643
	 * Set the flag to true, so that the file would be
644
	 * in the decrypted state.
645
	 *
646
	 * @param $isDisabled bool
647
	 */
648
	public static function setDisableWriteEncryption($isDisabled) {
649
		self::$disableWriteEncryption = $isDisabled;
650
	}
651
652
	/**
653
	 * @param Storage $sourceStorage
654
	 * @param string $sourceInternalPath
655
	 * @param string $targetInternalPath
656
	 * @param bool $preserveMtime
657
	 * @param bool $isRename
658
	 * @return bool
659
	 * @throws \Exception
660
	 */
661
	public function copyFromStorage(Storage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false, $isRename = false) {
662
663
		// TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
664
		// - call $this->storage->copyFromStorage() instead of $this->copyBetweenStorage
665
		// - copy the file cache update from  $this->copyBetweenStorage to this method
666
		// - copy the copyKeys() call from  $this->copyBetweenStorage to this method
667
		// - remove $this->copyBetweenStorage
668
669
		return $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename);
670
	}
671
672
	/**
673
	 * Update the encrypted cache version in the database
674
	 *
675
	 * @param Storage $sourceStorage
676
	 * @param string $sourceInternalPath
677
	 * @param string $targetInternalPath
678
	 * @param bool $isRename
679
	 */
680
	private function updateEncryptedVersion(Storage\IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename) {
681
		$isEncrypted = $this->encryptionManager->isEnabled() && $this->mount->getOption('encrypt', true) ? 1 : 0;
682
		$cacheInformation = [
683
			'encrypted' => (bool)$isEncrypted,
684
		];
685
		if ($isEncrypted === 1) {
686
			$encryptedVersion = $sourceStorage->getCache()->get($sourceInternalPath)['encryptedVersion'];
687
688
			// In case of a move operation from an unencrypted to an encrypted
689
			// storage the old encrypted version would stay with "0" while the
690
			// correct value would be "1". Thus we manually set the value to "1"
691
			// for those cases.
692
			// See also https://github.com/owncloud/core/issues/23078
693
			if ($encryptedVersion === 0) {
694
				$encryptedVersion = 1;
695
			}
696
697
			$cacheInformation['encryptedVersion'] = $encryptedVersion;
698
		}
699
700
		// in case of a rename we need to manipulate the source cache because
701
		// this information will be kept for the new target
702
		if ($isRename) {
703
			/*
704
			 * Rename is a process of creating a new file. Here we try to use the
705
			 * incremented version of source file, for the destination file.
706
			 */
707
			$encryptedVersion = $sourceStorage->getCache()->get($sourceInternalPath)['encryptedVersion'];
708
			if ($this->encryptionManager->isEnabled()) {
709
				$cacheInformation['encryptedVersion'] = $encryptedVersion + 1;
710
			}
711
			$sourceStorage->getCache()->put($sourceInternalPath, $cacheInformation);
712
		} else {
713
			$this->getCache()->put($targetInternalPath, $cacheInformation);
714
		}
715
	}
716
717
	/**
718
	 * copy file between two storages
719
	 *
720
	 * @param Storage $sourceStorage
721
	 * @param string $sourceInternalPath
722
	 * @param string $targetInternalPath
723
	 * @param bool $preserveMtime
724
	 * @param bool $isRename
725
	 * @return bool
726
	 * @throws \Exception
727
	 */
728
	private function copyBetweenStorage(Storage\IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename) {
729
		// for versions we have nothing to do, because versions should always use the
730
		// key from the original file. Just create a 1:1 copy and done
731
		if ($this->isVersion($targetInternalPath) ||
732
			$this->isVersion($sourceInternalPath)) {
733
			// remember that we try to create a version so that we can detect it during
734
			// fopen($sourceInternalPath) and by-pass the encryption in order to
735
			// create a 1:1 copy of the file
736
			$this->arrayCache->set('encryption_copy_version_' . $sourceInternalPath, true);
737
			$result = $this->storage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
0 ignored issues
show
Compatibility introduced by
$sourceStorage of type object<OCP\Files\Storage\IStorage> is not a sub-type of object<OCP\Files\Storage>. It seems like you assume a child interface of the interface OCP\Files\Storage\IStorage to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
738
			$this->arrayCache->remove('encryption_copy_version_' . $sourceInternalPath);
739
			if ($result) {
740
				$info = $this->getCache('', $sourceStorage)->get($sourceInternalPath);
741
				// make sure that we update the unencrypted size for the version
742
				if (isset($info['encrypted']) && $info['encrypted'] === true) {
743
					$this->updateUnencryptedSize(
744
						$this->getFullPath($targetInternalPath),
745
						$info['size']
746
					);
747
				}
748
				$this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename);
749
			}
750
			return $result;
751
		}
752
753
		// first copy the keys that we reuse the existing file key on the target location
754
		// and don't create a new one which would break versions for example.
755
		$mount = $this->mountManager->findByStorageId($sourceStorage->getId());
756
		if (\is_array($mount) && \count($mount) === 1) {
757
			$mountPoint = $mount[0]->getMountPoint();
758
			$source = $mountPoint . '/' . $sourceInternalPath;
759
			$target = $this->getFullPath($targetInternalPath);
760
			$this->copyKeys($source, $target);
761
		} else {
762
			$this->logger->error('Could not find mount point, can\'t keep encryption keys');
763
		}
764
765
		if ($sourceStorage->is_dir($sourceInternalPath)) {
766
			$dh = $sourceStorage->opendir($sourceInternalPath);
767
			$result = $this->mkdir($targetInternalPath);
768 View Code Duplication
			if (\is_resource($dh)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
769
				while ($result and ($file = \readdir($dh)) !== false) {
770
					if (!Filesystem::isIgnoredDir($file)) {
771
						$result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file, false, $isRename);
772
					}
773
				}
774
			}
775
		} else {
776
			try {
777
				$source = $sourceStorage->fopen($sourceInternalPath, 'r');
778
				if ($isRename && (\count($mount) === 1)) {
779
					$sourceStorageMountPoint = $mount[0]->getMountPoint();
780
					$this->sourcePath[$targetInternalPath] = $sourceStorageMountPoint . '/' . $sourceInternalPath;
781
				} else {
782
					unset($this->sourcePath[$targetInternalPath]);
783
				}
784
				$target = $this->fopen($targetInternalPath, 'w');
785
				list(, $result) = \OC_Helper::streamCopy($source, $target);
0 ignored issues
show
Security Bug introduced by
It seems like $source can also be of type false; however, OC_Helper::streamCopy() does only seem to accept resource, did you maybe forget to handle an error condition?
Loading history...
Security Bug introduced by
It seems like $target can also be of type false; however, OC_Helper::streamCopy() does only seem to accept resource, did you maybe forget to handle an error condition?
Loading history...
786
				\fclose($source);
787
				\fclose($target);
788
			} catch (\Exception $e) {
789
				Encryption::setDisableWriteEncryption(false);
790
				\fclose($source);
791
				\fclose($target);
0 ignored issues
show
Bug introduced by
The variable $target does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
792
				throw $e;
793
			}
794
			if ($result) {
795
				if ($preserveMtime) {
796
					$this->touch($targetInternalPath, $sourceStorage->filemtime($sourceInternalPath));
0 ignored issues
show
Security Bug introduced by
It seems like $sourceStorage->filemtime($sourceInternalPath) targeting OCP\Files\Storage\IStorage::filemtime() can also be of type false; however, OC\Files\Storage\Wrapper\Wrapper::touch() does only seem to accept integer|null, did you maybe forget to handle an error condition?
Loading history...
797
				}
798
				$this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename);
799
			} else {
800
				// delete partially written target file
801
				$this->unlink($targetInternalPath);
802
				// delete cache entry that was created by fopen
803
				$this->getCache()->remove($targetInternalPath);
804
			}
805
		}
806
		return (bool)$result;
807
	}
808
809
	/**
810
	 * get the path to a local version of the file.
811
	 * The local version of the file can be temporary and doesn't have to be persistent across requests
812
	 *
813
	 * @param string $path
814
	 * @return string
815
	 */
816
	public function getLocalFile($path) {
817
		if ($this->encryptionManager->isEnabled()) {
818
			$cachedFile = $this->getCachedFile($path);
819
			if (\is_string($cachedFile)) {
820
				return $cachedFile;
821
			}
822
		}
823
		return $this->storage->getLocalFile($path);
824
	}
825
826
	/**
827
	 * Returns the wrapped storage's value for isLocal()
828
	 *
829
	 * @return bool wrapped storage's isLocal() value
830
	 */
831
	public function isLocal() {
832
		if ($this->encryptionManager->isEnabled()) {
833
			return false;
834
		}
835
		return $this->storage->isLocal();
836
	}
837
838
	/**
839
	 * see http://php.net/manual/en/function.stat.php
840
	 * only the following keys are required in the result: size and mtime
841
	 *
842
	 * @param string $path
843
	 * @return array
844
	 */
845
	public function stat($path) {
846
		$stat = $this->storage->stat($path);
847
		$fileSize = $this->filesize($path);
848
		$stat['size'] = $fileSize;
849
		$stat[7] = $fileSize;
850
		return $stat;
851
	}
852
853
	/**
854
	 * see http://php.net/manual/en/function.hash.php
855
	 *
856
	 * @param string $type
857
	 * @param string $path
858
	 * @param bool $raw
859
	 * @return string
860
	 */
861 View Code Duplication
	public function hash($type, $path, $raw = false) {
862
		$fh = $this->fopen($path, 'rb');
863
		$ctx = \hash_init($type);
864
		\hash_update_stream($ctx, $fh);
865
		\fclose($fh);
866
		return \hash_final($ctx, $raw);
867
	}
868
869
	/**
870
	 * return full path, including mount point
871
	 *
872
	 * @param string $path relative to mount point
873
	 * @return string full path including mount point
874
	 */
875
	protected function getFullPath($path) {
876
		return Filesystem::normalizePath($this->mountPoint . '/' . $path);
877
	}
878
879
	/**
880
	 * read first block of encrypted file, typically this will contain the
881
	 * encryption header
882
	 *
883
	 * @param string|resource $path
884
	 * @return string
885
	 */
886
	protected function readFirstBlock($path) {
887
		if (\is_resource($path)) {
888
			$firstBlock = \fread($path, $this->util->getHeaderSize());
889
			\rewind($path);
890
			return $firstBlock;
891
		}
892
		$firstBlock = '';
893
		if ($this->storage->file_exists($path)) {
0 ignored issues
show
Bug introduced by
It seems like $path defined by parameter $path on line 886 can also be of type resource; however, OCP\Files\Storage\IStorage::file_exists() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
894
			$handle = $this->storage->fopen($path, 'r');
0 ignored issues
show
Bug introduced by
It seems like $path defined by parameter $path on line 886 can also be of type resource; however, OCP\Files\Storage\IStorage::fopen() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
895
			$firstBlock = \fread($handle, $this->util->getHeaderSize());
896
			\fclose($handle);
897
		}
898
		return $firstBlock;
899
	}
900
901
	/**
902
	 * return header size of given file
903
	 *
904
	 * @param string|resource $path
905
	 * @return int
906
	 */
907
	protected function getHeaderSize($path) {
908
		$headerSize = 0;
909 View Code Duplication
		if (!\is_resource($path)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
910
			$realFile = $this->util->stripPartialFileExtension($path);
0 ignored issues
show
Bug introduced by
It seems like $path defined by parameter $path on line 907 can also be of type resource; however, OC\Encryption\Util::stripPartialFileExtension() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
911
			if ($this->storage->file_exists($realFile)) {
912
				$path = $realFile;
913
			}
914
		}
915
		$firstBlock = $this->readFirstBlock($path);
916
917
		if (\substr($firstBlock, 0, \strlen(Util::HEADER_START)) === Util::HEADER_START) {
918
			$headerSize = $this->util->getHeaderSize();
919
		}
920
921
		return $headerSize;
922
	}
923
924
	/**
925
	 * parse raw header to array
926
	 *
927
	 * @param string $rawHeader
928
	 * @return array
929
	 */
930
	protected function parseRawHeader($rawHeader) {
931
		$result = [];
932
		if (\substr($rawHeader, 0, \strlen(Util::HEADER_START)) === Util::HEADER_START) {
933
			$header = $rawHeader;
934
			$endAt = \strpos($header, Util::HEADER_END);
935
			if ($endAt !== false) {
936
				$header = \substr($header, 0, $endAt + \strlen(Util::HEADER_END));
937
938
				// +1 to not start with an ':' which would result in empty element at the beginning
939
				$exploded = \explode(':', \substr($header, \strlen(Util::HEADER_START)+1));
940
941
				$element = \array_shift($exploded);
942
				while ($element !== Util::HEADER_END) {
943
					$result[$element] = \array_shift($exploded);
944
					$element = \array_shift($exploded);
945
				}
946
			}
947
		}
948
949
		return $result;
950
	}
951
952
	/**
953
	 * read header from file
954
	 *
955
	 * @param string|resource $path
956
	 * @return array
957
	 */
958
	protected function getHeader($path) {
959
		if (\is_resource($path)) {
960
			$exists = false;
961 View Code Duplication
		} else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
962
			$realFile = $this->util->stripPartialFileExtension($path);
0 ignored issues
show
Bug introduced by
It seems like $path defined by parameter $path on line 958 can also be of type resource; however, OC\Encryption\Util::stripPartialFileExtension() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
963
			$exists = $this->storage->file_exists($realFile);
964
			if ($exists) {
965
				$path = $realFile;
966
			}
967
		}
968
969
		$firstBlock = $this->readFirstBlock($path);
970
		$result = $this->parseRawHeader($firstBlock);
971
972
		// if the header doesn't contain a encryption module we check if it is a
973
		// legacy file. If true, we add the default encryption module
974
		if (!isset($result[Util::HEADER_ENCRYPTION_MODULE_KEY])) {
975
			if (!empty($result)) {
976
				$result[Util::HEADER_ENCRYPTION_MODULE_KEY] = 'OC_DEFAULT_MODULE';
977
			} elseif ($exists) {
978
				// if the header was empty we have to check first if it is a encrypted file at all
979
				// We would do query to filecache only if we know that entry in filecache exists
980
				$info = $this->getCache()->get($path);
981
				if (isset($info['encrypted']) && $info['encrypted'] === true) {
982
					$result[Util::HEADER_ENCRYPTION_MODULE_KEY] = 'OC_DEFAULT_MODULE';
983
				}
984
			}
985
		}
986
987
		return $result;
988
	}
989
990
	/**
991
	 * read encryption module needed to read/write the file located at $path
992
	 *
993
	 * @param string $path
994
	 * @return null|\OCP\Encryption\IEncryptionModule
995
	 * @throws ModuleDoesNotExistsException
996
	 * @throws \Exception
997
	 */
998
	protected function getEncryptionModule($path) {
999
		$encryptionModule = null;
1000
		$header = $this->getHeader($path);
1001
		$encryptionModuleId = $this->util->getEncryptionModuleId($header);
1002
		if (!empty($encryptionModuleId)) {
1003
			try {
1004
				$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
1005
			} catch (ModuleDoesNotExistsException $e) {
1006
				$this->logger->critical('Encryption module defined in "' . $path . '" not loaded!');
1007
				throw $e;
1008
			}
1009
		}
1010
		return $encryptionModule;
1011
	}
1012
1013
	/**
1014
	 * @param string $path
1015
	 * @param int $unencryptedSize
1016
	 */
1017
	public function updateUnencryptedSize($path, $unencryptedSize) {
1018
		$this->unencryptedSize[$path] = $unencryptedSize;
1019
	}
1020
1021
	/**
1022
	 * copy keys to new location
1023
	 *
1024
	 * @param string $source path relative to data/
1025
	 * @param string $target path relative to data/
1026
	 * @return bool
1027
	 */
1028
	protected function copyKeys($source, $target) {
1029
		if (!$this->util->isExcluded($source)) {
1030
			return $this->keyStorage->copyKeys($source, $target);
1031
		}
1032
1033
		return false;
1034
	}
1035
1036
	/**
1037
	 *
1038
	 * delete file keys of the file
1039
	 *
1040
	 * @param $path path of the file key to delete
1041
	 * @return bool
1042
	 */
1043
	protected function deleteAllFileKeys($path) {
1044
		$fullPath = $this->getFullPath($path);
1045
		return $this->keyStorage->deleteAllFileKeys($fullPath);
1046
	}
1047
1048
	/**
1049
	 * check if path points to a files version
1050
	 *
1051
	 * @param $path
1052
	 * @return bool
1053
	 */
1054
	protected function isVersion($path) {
1055
		$normalized = Filesystem::normalizePath($path);
1056
		return \substr($normalized, 0, \strlen('/files_versions/')) === '/files_versions/';
1057
	}
1058
}
1059