Passed
Push — master ( 495329...2abeff )
by Daniel
17:26 queued 12s
created

Encryption::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 26
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 14
nc 1
nop 10
dl 0
loc 26
rs 9.7998
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 Arthur Schiwon <[email protected]>
6
 * @author Bjoern Schiessle <[email protected]>
7
 * @author Björn Schießle <[email protected]>
8
 * @author Christoph Wurst <[email protected]>
9
 * @author J0WI <[email protected]>
10
 * @author jknockaert <[email protected]>
11
 * @author Joas Schilling <[email protected]>
12
 * @author Lukas Reschke <[email protected]>
13
 * @author Morris Jobke <[email protected]>
14
 * @author Piotr M <[email protected]>
15
 * @author Robin Appelman <[email protected]>
16
 * @author Roeland Jago Douma <[email protected]>
17
 * @author Thomas Müller <[email protected]>
18
 * @author Tigran Mkrtchyan <[email protected]>
19
 * @author Vincent Petry <[email protected]>
20
 *
21
 * @license AGPL-3.0
22
 *
23
 * This code is free software: you can redistribute it and/or modify
24
 * it under the terms of the GNU Affero General Public License, version 3,
25
 * as published by the Free Software Foundation.
26
 *
27
 * This program is distributed in the hope that it will be useful,
28
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
29
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30
 * GNU Affero General Public License for more details.
31
 *
32
 * You should have received a copy of the GNU Affero General Public License, version 3,
33
 * along with this program. If not, see <http://www.gnu.org/licenses/>
34
 *
35
 */
36
37
namespace OC\Files\Storage\Wrapper;
38
39
use OC\Encryption\Exceptions\ModuleDoesNotExistsException;
40
use OC\Encryption\Update;
41
use OC\Encryption\Util;
42
use OC\Files\Cache\CacheEntry;
43
use OC\Files\Filesystem;
44
use OC\Files\Mount\Manager;
45
use OC\Files\ObjectStore\ObjectStoreStorage;
46
use OC\Files\Storage\LocalTempFileTrait;
47
use OC\Memcache\ArrayCache;
48
use OCP\Cache\CappedMemoryCache;
49
use OCP\Encryption\Exceptions\GenericEncryptionException;
50
use OCP\Encryption\IFile;
51
use OCP\Encryption\IManager;
52
use OCP\Encryption\Keys\IStorage;
53
use OCP\Files\Cache\ICacheEntry;
54
use OCP\Files\Mount\IMountPoint;
55
use OCP\Files\Storage;
56
use Psr\Log\LoggerInterface;
57
58
class Encryption extends Wrapper {
59
	use LocalTempFileTrait;
60
61
	/** @var string */
62
	private $mountPoint;
63
64
	/** @var \OC\Encryption\Util */
65
	private $util;
66
67
	/** @var \OCP\Encryption\IManager */
68
	private $encryptionManager;
69
70
	private LoggerInterface $logger;
71
72
	/** @var string */
73
	private $uid;
74
75
	/** @var array */
76
	protected $unencryptedSize;
77
78
	/** @var \OCP\Encryption\IFile */
79
	private $fileHelper;
80
81
	/** @var IMountPoint */
82
	private $mount;
83
84
	/** @var IStorage */
85
	private $keyStorage;
86
87
	/** @var Update */
88
	private $update;
89
90
	/** @var Manager */
91
	private $mountManager;
92
93
	/** @var array remember for which path we execute the repair step to avoid recursions */
94
	private $fixUnencryptedSizeOf = [];
95
96
	/** @var  ArrayCache */
97
	private $arrayCache;
98
99
	/** @var CappedMemoryCache<bool> */
100
	private CappedMemoryCache $encryptedPaths;
101
102
	/**
103
	 * @param array $parameters
104
	 */
105
	public function __construct(
106
		$parameters,
107
		IManager $encryptionManager = null,
108
		Util $util = null,
109
		LoggerInterface $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
		$this->encryptedPaths = new CappedMemoryCache();
130
		parent::__construct($parameters);
131
	}
132
133
	/**
134
	 * see https://www.php.net/manual/en/function.filesize.php
135
	 * The result for filesize when called on a folder is required to be 0
136
	 */
137
	public function filesize($path): false|int|float {
138
		$fullPath = $this->getFullPath($path);
139
140
		$info = $this->getCache()->get($path);
141
		if ($info === false) {
142
			return false;
143
		}
144
		if (isset($this->unencryptedSize[$fullPath])) {
145
			$size = $this->unencryptedSize[$fullPath];
146
			// update file cache
147
			if ($info instanceof ICacheEntry) {
0 ignored issues
show
introduced by
$info is always a sub-type of OCP\Files\Cache\ICacheEntry.
Loading history...
148
				$info['encrypted'] = $info['encryptedVersion'];
149
			} else {
150
				if (!is_array($info)) {
151
					$info = [];
152
				}
153
				$info['encrypted'] = true;
154
				$info = new CacheEntry($info);
155
			}
156
157
			if ($size !== $info->getUnencryptedSize()) {
158
				$this->getCache()->update($info->getId(), [
159
					'unencrypted_size' => $size
160
				]);
161
			}
162
163
			return $size;
164
		}
165
166
		if (isset($info['fileid']) && $info['encrypted']) {
167
			return $this->verifyUnencryptedSize($path, $info->getUnencryptedSize());
168
		}
169
170
		return $this->storage->filesize($path);
171
	}
172
173
	/**
174
	 * @param string $path
175
	 * @param array $data
176
	 * @return array
177
	 */
178
	private function modifyMetaData(string $path, array $data): array {
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
			$data['unencrypted_size'] = $data['size'];
186
		} else {
187
			if (isset($info['fileid']) && $info['encrypted']) {
188
				$data['size'] = $this->verifyUnencryptedSize($path, $info->getUnencryptedSize());
189
				$data['encrypted'] = true;
190
				$data['unencrypted_size'] = $data['size'];
191
			}
192
		}
193
194
		if (isset($info['encryptedVersion']) && $info['encryptedVersion'] > 1) {
195
			$data['encryptedVersion'] = $info['encryptedVersion'];
196
		}
197
198
		return $data;
199
	}
200
201
	public function getMetaData($path) {
202
		$data = $this->storage->getMetaData($path);
203
		if (is_null($data)) {
204
			return null;
205
		}
206
		return $this->modifyMetaData($path, $data);
207
	}
208
209
	public function getDirectoryContent($directory): \Traversable {
210
		$parent = rtrim($directory, '/');
211
		foreach ($this->getWrapperStorage()->getDirectoryContent($directory) as $data) {
212
			yield $this->modifyMetaData($parent . '/' . $data['name'], $data);
213
		}
214
	}
215
216
	/**
217
	 * see https://www.php.net/manual/en/function.file_get_contents.php
218
	 *
219
	 * @param string $path
220
	 * @return string
221
	 */
222
	public function file_get_contents($path) {
223
		$encryptionModule = $this->getEncryptionModule($path);
224
225
		if ($encryptionModule) {
226
			$handle = $this->fopen($path, "r");
227
			if (!$handle) {
228
				return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
229
			}
230
			$data = stream_get_contents($handle);
231
			fclose($handle);
232
			return $data;
233
		}
234
		return $this->storage->file_get_contents($path);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->storage->file_get_contents($path) also could return the type boolean which is incompatible with the documented return type string.
Loading history...
235
	}
236
237
	/**
238
	 * see https://www.php.net/manual/en/function.file_put_contents.php
239
	 *
240
	 * @param string $path
241
	 * @param mixed $data
242
	 * @return int|false
243
	 */
244
	public function file_put_contents($path, $data) {
245
		// file put content will always be translated to a stream write
246
		$handle = $this->fopen($path, 'w');
247
		if (is_resource($handle)) {
248
			$written = fwrite($handle, $data);
249
			fclose($handle);
250
			return $written;
251
		}
252
253
		return false;
254
	}
255
256
	/**
257
	 * see https://www.php.net/manual/en/function.unlink.php
258
	 *
259
	 * @param string $path
260
	 * @return bool
261
	 */
262
	public function unlink($path) {
263
		$fullPath = $this->getFullPath($path);
264
		if ($this->util->isExcluded($fullPath)) {
265
			return $this->storage->unlink($path);
266
		}
267
268
		$encryptionModule = $this->getEncryptionModule($path);
269
		if ($encryptionModule) {
270
			$this->keyStorage->deleteAllFileKeys($fullPath);
271
		}
272
273
		return $this->storage->unlink($path);
274
	}
275
276
	/**
277
	 * see https://www.php.net/manual/en/function.rename.php
278
	 *
279
	 * @param string $source
280
	 * @param string $target
281
	 * @return bool
282
	 */
283
	public function rename($source, $target) {
284
		$result = $this->storage->rename($source, $target);
285
286
		if ($result &&
287
			// versions always use the keys from the original file, so we can skip
288
			// this step for versions
289
			$this->isVersion($target) === false &&
290
			$this->encryptionManager->isEnabled()) {
291
			$sourcePath = $this->getFullPath($source);
292
			if (!$this->util->isExcluded($sourcePath)) {
293
				$targetPath = $this->getFullPath($target);
294
				if (isset($this->unencryptedSize[$sourcePath])) {
295
					$this->unencryptedSize[$targetPath] = $this->unencryptedSize[$sourcePath];
296
				}
297
				$this->keyStorage->renameKeys($sourcePath, $targetPath);
298
				$module = $this->getEncryptionModule($target);
299
				if ($module) {
300
					$module->update($targetPath, $this->uid, []);
301
				}
302
			}
303
		}
304
305
		return $result;
306
	}
307
308
	/**
309
	 * see https://www.php.net/manual/en/function.rmdir.php
310
	 *
311
	 * @param string $path
312
	 * @return bool
313
	 */
314
	public function rmdir($path) {
315
		$result = $this->storage->rmdir($path);
316
		$fullPath = $this->getFullPath($path);
317
		if ($result &&
318
			$this->util->isExcluded($fullPath) === false &&
319
			$this->encryptionManager->isEnabled()
320
		) {
321
			$this->keyStorage->deleteAllFileKeys($fullPath);
322
		}
323
324
		return $result;
325
	}
326
327
	/**
328
	 * check if a file can be read
329
	 *
330
	 * @param string $path
331
	 * @return bool
332
	 */
333
	public function isReadable($path) {
334
		$isReadable = true;
335
336
		$metaData = $this->getMetaData($path);
337
		if (
338
			!$this->is_dir($path) &&
339
			isset($metaData['encrypted']) &&
340
			$metaData['encrypted'] === true
341
		) {
342
			$fullPath = $this->getFullPath($path);
343
			$module = $this->getEncryptionModule($path);
344
			$isReadable = $module->isReadable($fullPath, $this->uid);
345
		}
346
347
		return $this->storage->isReadable($path) && $isReadable;
348
	}
349
350
	/**
351
	 * see https://www.php.net/manual/en/function.copy.php
352
	 *
353
	 * @param string $source
354
	 * @param string $target
355
	 */
356
	public function copy($source, $target): bool {
357
		$sourcePath = $this->getFullPath($source);
358
359
		if ($this->util->isExcluded($sourcePath)) {
360
			return $this->storage->copy($source, $target);
361
		}
362
363
		// need to stream copy file by file in case we copy between a encrypted
364
		// and a unencrypted storage
365
		$this->unlink($target);
366
		return $this->copyFromStorage($this, $source, $target);
367
	}
368
369
	/**
370
	 * see https://www.php.net/manual/en/function.fopen.php
371
	 *
372
	 * @param string $path
373
	 * @param string $mode
374
	 * @return resource|bool
375
	 * @throws GenericEncryptionException
376
	 * @throws ModuleDoesNotExistsException
377
	 */
378
	public function fopen($path, $mode) {
379
		// check if the file is stored in the array cache, this means that we
380
		// copy a file over to the versions folder, in this case we don't want to
381
		// decrypt it
382
		if ($this->arrayCache->hasKey('encryption_copy_version_' . $path)) {
383
			$this->arrayCache->remove('encryption_copy_version_' . $path);
384
			return $this->storage->fopen($path, $mode);
385
		}
386
387
		$encryptionEnabled = $this->encryptionManager->isEnabled();
388
		$shouldEncrypt = false;
389
		$encryptionModule = null;
390
		$header = $this->getHeader($path);
391
		$signed = isset($header['signed']) && $header['signed'] === 'true';
392
		$fullPath = $this->getFullPath($path);
393
		$encryptionModuleId = $this->util->getEncryptionModuleId($header);
394
395
		if ($this->util->isExcluded($fullPath) === false) {
396
			$size = $unencryptedSize = 0;
397
			$realFile = $this->util->stripPartialFileExtension($path);
398
			$targetExists = $this->is_file($realFile) || $this->file_exists($path);
399
			$targetIsEncrypted = false;
400
			if ($targetExists) {
401
				// in case the file exists we require the explicit module as
402
				// specified in the file header - otherwise we need to fail hard to
403
				// prevent data loss on client side
404
				if (!empty($encryptionModuleId)) {
405
					$targetIsEncrypted = true;
406
					$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
407
				}
408
409
				if ($this->file_exists($path)) {
410
					$size = $this->storage->filesize($path);
411
					$unencryptedSize = $this->filesize($path);
412
				} else {
413
					$size = $unencryptedSize = 0;
414
				}
415
			}
416
417
			try {
418
				if (
419
					$mode === 'w'
420
					|| $mode === 'w+'
421
					|| $mode === 'wb'
422
					|| $mode === 'wb+'
423
				) {
424
					// if we update a encrypted file with a un-encrypted one we change the db flag
425
					if ($targetIsEncrypted && $encryptionEnabled === false) {
426
						$cache = $this->storage->getCache();
427
						if ($cache) {
0 ignored issues
show
introduced by
$cache is of type OC\Files\Cache\Cache, thus it always evaluated to true.
Loading history...
428
							$entry = $cache->get($path);
429
							$cache->update($entry->getId(), ['encrypted' => 0]);
430
						}
431
					}
432
					if ($encryptionEnabled) {
433
						// if $encryptionModuleId is empty, the default module will be used
434
						$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
435
						$shouldEncrypt = $encryptionModule->shouldEncrypt($fullPath);
436
						$signed = true;
437
					}
438
				} else {
439
					$info = $this->getCache()->get($path);
440
					// only get encryption module if we found one in the header
441
					// or if file should be encrypted according to the file cache
442
					if (!empty($encryptionModuleId)) {
443
						$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
444
						$shouldEncrypt = true;
445
					} elseif (empty($encryptionModuleId) && $info['encrypted'] === true) {
446
						// we come from a old installation. No header and/or no module defined
447
						// but the file is encrypted. In this case we need to use the
448
						// OC_DEFAULT_MODULE to read the file
449
						$encryptionModule = $this->encryptionManager->getEncryptionModule('OC_DEFAULT_MODULE');
450
						$shouldEncrypt = true;
451
						$targetIsEncrypted = true;
452
					}
453
				}
454
			} catch (ModuleDoesNotExistsException $e) {
455
				$this->logger->warning('Encryption module "' . $encryptionModuleId . '" not found, file will be stored unencrypted', [
456
					'exception' => $e,
457
					'app' => 'core',
458
				]);
459
			}
460
461
			// encryption disabled on write of new file and write to existing unencrypted file -> don't encrypt
462
			if (!$encryptionEnabled || !$this->shouldEncrypt($path)) {
463
				if (!$targetExists || !$targetIsEncrypted) {
464
					$shouldEncrypt = false;
465
				}
466
			}
467
468
			if ($shouldEncrypt === true && $encryptionModule !== null) {
469
				$this->encryptedPaths->set($this->util->stripPartialFileExtension($path), true);
0 ignored issues
show
Bug introduced by
true of type true is incompatible with the type OCP\Cache\T expected by parameter $value of OCP\Cache\CappedMemoryCache::set(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

469
				$this->encryptedPaths->set($this->util->stripPartialFileExtension($path), /** @scrutinizer ignore-type */ true);
Loading history...
470
				$headerSize = $this->getHeaderSize($path);
471
				$source = $this->storage->fopen($path, $mode);
472
				if (!is_resource($source)) {
473
					return false;
474
				}
475
				$handle = \OC\Files\Stream\Encryption::wrap($source, $path, $fullPath, $header,
476
					$this->uid, $encryptionModule, $this->storage, $this, $this->util, $this->fileHelper, $mode,
477
					$size, $unencryptedSize, $headerSize, $signed);
478
479
				return $handle;
480
			}
481
		}
482
483
		return $this->storage->fopen($path, $mode);
484
	}
485
486
487
	/**
488
	 * perform some plausibility checks if the the unencrypted size is correct.
489
	 * If not, we calculate the correct unencrypted size and return it
490
	 *
491
	 * @param string $path internal path relative to the storage root
492
	 * @param int $unencryptedSize size of the unencrypted file
493
	 *
494
	 * @return int unencrypted size
495
	 */
496
	protected function verifyUnencryptedSize(string $path, int $unencryptedSize): int {
497
		$size = $this->storage->filesize($path);
498
		$result = $unencryptedSize;
499
500
		if ($unencryptedSize < 0 ||
501
			($size > 0 && $unencryptedSize === $size) ||
502
			$unencryptedSize > $size
503
		) {
504
			// check if we already calculate the unencrypted size for the
505
			// given path to avoid recursions
506
			if (isset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]) === false) {
507
				$this->fixUnencryptedSizeOf[$this->getFullPath($path)] = true;
508
				try {
509
					$result = $this->fixUnencryptedSize($path, $size, $unencryptedSize);
510
				} catch (\Exception $e) {
511
					$this->logger->error('Couldn\'t re-calculate unencrypted size for ' . $path, ['exception' => $e]);
512
				}
513
				unset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]);
514
			}
515
		}
516
517
		return $result;
518
	}
519
520
	/**
521
	 * calculate the unencrypted size
522
	 *
523
	 * @param string $path internal path relative to the storage root
524
	 * @param int $size size of the physical file
525
	 * @param int $unencryptedSize size of the unencrypted file
526
	 *
527
	 * @return int calculated unencrypted size
528
	 */
529
	protected function fixUnencryptedSize(string $path, int $size, int $unencryptedSize): int {
530
		$headerSize = $this->getHeaderSize($path);
531
		$header = $this->getHeader($path);
532
		$encryptionModule = $this->getEncryptionModule($path);
533
534
		$stream = $this->storage->fopen($path, 'r');
535
536
		// if we couldn't open the file we return the old unencrypted size
537
		if (!is_resource($stream)) {
538
			$this->logger->error('Could not open ' . $path . '. Recalculation of unencrypted size aborted.');
539
			return $unencryptedSize;
540
		}
541
542
		$newUnencryptedSize = 0;
543
		$size -= $headerSize;
544
		$blockSize = $this->util->getBlockSize();
545
546
		// if a header exists we skip it
547
		if ($headerSize > 0) {
548
			$this->fread_block($stream, $headerSize);
0 ignored issues
show
Bug introduced by
$stream of type resource is incompatible with the type OC\Files\Storage\Wrapper\the expected by parameter $handle of OC\Files\Storage\Wrapper\Encryption::fread_block(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

548
			$this->fread_block(/** @scrutinizer ignore-type */ $stream, $headerSize);
Loading history...
549
		}
550
551
		// fast path, else the calculation for $lastChunkNr is bogus
552
		if ($size === 0) {
553
			return 0;
554
		}
555
556
		$signed = isset($header['signed']) && $header['signed'] === 'true';
557
		$unencryptedBlockSize = $encryptionModule->getUnencryptedBlockSize($signed);
558
559
		// calculate last chunk nr
560
		// next highest is end of chunks, one subtracted is last one
561
		// we have to read the last chunk, we can't just calculate it (because of padding etc)
562
563
		$lastChunkNr = ceil($size / $blockSize) - 1;
564
		// calculate last chunk position
565
		$lastChunkPos = ($lastChunkNr * $blockSize);
566
		// try to fseek to the last chunk, if it fails we have to read the whole file
567
		if (@fseek($stream, $lastChunkPos, SEEK_CUR) === 0) {
0 ignored issues
show
Bug introduced by
$lastChunkPos of type double is incompatible with the type integer expected by parameter $offset of fseek(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

567
		if (@fseek($stream, /** @scrutinizer ignore-type */ $lastChunkPos, SEEK_CUR) === 0) {
Loading history...
568
			$newUnencryptedSize += $lastChunkNr * $unencryptedBlockSize;
569
		}
570
571
		$lastChunkContentEncrypted = '';
572
		$count = $blockSize;
573
574
		while ($count > 0) {
575
			$data = $this->fread_block($stream, $blockSize);
576
			$count = strlen($data);
577
			$lastChunkContentEncrypted .= $data;
578
			if (strlen($lastChunkContentEncrypted) > $blockSize) {
579
				$newUnencryptedSize += $unencryptedBlockSize;
580
				$lastChunkContentEncrypted = substr($lastChunkContentEncrypted, $blockSize);
581
			}
582
		}
583
584
		fclose($stream);
585
586
		// we have to decrypt the last chunk to get it actual size
587
		$encryptionModule->begin($this->getFullPath($path), $this->uid, 'r', $header, []);
588
		$decryptedLastChunk = $encryptionModule->decrypt($lastChunkContentEncrypted, $lastChunkNr . 'end');
589
		$decryptedLastChunk .= $encryptionModule->end($this->getFullPath($path), $lastChunkNr . 'end');
590
591
		// calc the real file size with the size of the last chunk
592
		$newUnencryptedSize += strlen($decryptedLastChunk);
593
594
		$this->updateUnencryptedSize($this->getFullPath($path), $newUnencryptedSize);
595
596
		// write to cache if applicable
597
		$cache = $this->storage->getCache();
598
		if ($cache) {
0 ignored issues
show
introduced by
$cache is of type OC\Files\Cache\Cache, thus it always evaluated to true.
Loading history...
599
			$entry = $cache->get($path);
600
			$cache->update($entry['fileid'], [
601
				'unencrypted_size' => $newUnencryptedSize
602
			]);
603
		}
604
605
		return $newUnencryptedSize;
606
	}
607
608
	/**
609
	 * fread_block
610
	 *
611
	 * This function is a wrapper around the fread function.  It is based on the
612
	 * stream_read_block function from lib/private/Files/Streams/Encryption.php
613
	 * It calls stream read until the requested $blockSize was received or no remaining data is present.
614
	 * This is required as stream_read only returns smaller chunks of data when the stream fetches from a
615
	 * remote storage over the internet and it does not care about the given $blockSize.
616
	 *
617
	 * @param handle the stream to read from
0 ignored issues
show
Bug introduced by
The type OC\Files\Storage\Wrapper\the was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
618
	 * @param int $blockSize Length of requested data block in bytes
619
	 * @return string Data fetched from stream.
620
	 */
621
	private function fread_block($handle, int $blockSize): string {
622
		$remaining = $blockSize;
623
		$data = '';
624
625
		do {
626
			$chunk = fread($handle, $remaining);
0 ignored issues
show
Bug introduced by
$handle of type OC\Files\Storage\Wrapper\the is incompatible with the type resource expected by parameter $stream of fread(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

626
			$chunk = fread(/** @scrutinizer ignore-type */ $handle, $remaining);
Loading history...
627
			$chunk_len = strlen($chunk);
628
			$data .= $chunk;
629
			$remaining -= $chunk_len;
630
		} while (($remaining > 0) && ($chunk_len > 0));
631
632
		return $data;
633
	}
634
635
	/**
636
	 * @param Storage\IStorage $sourceStorage
637
	 * @param string $sourceInternalPath
638
	 * @param string $targetInternalPath
639
	 * @param bool $preserveMtime
640
	 * @return bool
641
	 */
642
	public function moveFromStorage(
643
		Storage\IStorage $sourceStorage,
644
		$sourceInternalPath,
645
		$targetInternalPath,
646
		$preserveMtime = true
647
	) {
648
		if ($sourceStorage === $this) {
0 ignored issues
show
introduced by
The condition $sourceStorage === $this is always false.
Loading history...
649
			return $this->rename($sourceInternalPath, $targetInternalPath);
650
		}
651
652
		// TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
653
		// - call $this->storage->moveFromStorage() instead of $this->copyBetweenStorage
654
		// - copy the file cache update from  $this->copyBetweenStorage to this method
655
		// - copy the copyKeys() call from  $this->copyBetweenStorage to this method
656
		// - remove $this->copyBetweenStorage
657
658
		if (!$sourceStorage->isDeletable($sourceInternalPath)) {
659
			return false;
660
		}
661
662
		$result = $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, true);
663
		if ($result) {
664
			if ($sourceStorage->is_dir($sourceInternalPath)) {
665
				$result &= $sourceStorage->rmdir($sourceInternalPath);
666
			} else {
667
				$result &= $sourceStorage->unlink($sourceInternalPath);
668
			}
669
		}
670
		return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result also could return the type integer which is incompatible with the documented return type boolean.
Loading history...
671
	}
672
673
674
	/**
675
	 * @param Storage\IStorage $sourceStorage
676
	 * @param string $sourceInternalPath
677
	 * @param string $targetInternalPath
678
	 * @param bool $preserveMtime
679
	 * @param bool $isRename
680
	 * @return bool
681
	 */
682
	public function copyFromStorage(
683
		Storage\IStorage $sourceStorage,
684
		$sourceInternalPath,
685
		$targetInternalPath,
686
		$preserveMtime = false,
687
		$isRename = false
688
	) {
689
		// TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
690
		// - call $this->storage->copyFromStorage() instead of $this->copyBetweenStorage
691
		// - copy the file cache update from  $this->copyBetweenStorage to this method
692
		// - copy the copyKeys() call from  $this->copyBetweenStorage to this method
693
		// - remove $this->copyBetweenStorage
694
695
		return $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename);
696
	}
697
698
	/**
699
	 * Update the encrypted cache version in the database
700
	 *
701
	 * @param Storage\IStorage $sourceStorage
702
	 * @param string $sourceInternalPath
703
	 * @param string $targetInternalPath
704
	 * @param bool $isRename
705
	 * @param bool $keepEncryptionVersion
706
	 */
707
	private function updateEncryptedVersion(
708
		Storage\IStorage $sourceStorage,
709
		$sourceInternalPath,
710
		$targetInternalPath,
711
		$isRename,
712
		$keepEncryptionVersion
713
	) {
714
		$isEncrypted = $this->encryptionManager->isEnabled() && $this->shouldEncrypt($targetInternalPath);
715
		$cacheInformation = [
716
			'encrypted' => $isEncrypted,
717
		];
718
		if ($isEncrypted) {
719
			$sourceCacheEntry = $sourceStorage->getCache()->get($sourceInternalPath);
720
			$targetCacheEntry = $this->getCache()->get($targetInternalPath);
721
722
			// Rename of the cache already happened, so we do the cleanup on the target
723
			if ($sourceCacheEntry === false && $targetCacheEntry !== false) {
724
				$encryptedVersion = $targetCacheEntry['encryptedVersion'];
725
				$isRename = false;
726
			} else {
727
				$encryptedVersion = $sourceCacheEntry['encryptedVersion'];
728
			}
729
730
			// In case of a move operation from an unencrypted to an encrypted
731
			// storage the old encrypted version would stay with "0" while the
732
			// correct value would be "1". Thus we manually set the value to "1"
733
			// for those cases.
734
			// See also https://github.com/owncloud/core/issues/23078
735
			if ($encryptedVersion === 0 || !$keepEncryptionVersion) {
736
				$encryptedVersion = 1;
737
			}
738
739
			$cacheInformation['encryptedVersion'] = $encryptedVersion;
740
		}
741
742
		// in case of a rename we need to manipulate the source cache because
743
		// this information will be kept for the new target
744
		if ($isRename) {
745
			$sourceStorage->getCache()->put($sourceInternalPath, $cacheInformation);
746
		} else {
747
			$this->getCache()->put($targetInternalPath, $cacheInformation);
748
		}
749
	}
750
751
	/**
752
	 * copy file between two storages
753
	 *
754
	 * @param Storage\IStorage $sourceStorage
755
	 * @param string $sourceInternalPath
756
	 * @param string $targetInternalPath
757
	 * @param bool $preserveMtime
758
	 * @param bool $isRename
759
	 * @return bool
760
	 * @throws \Exception
761
	 */
762
	private function copyBetweenStorage(
763
		Storage\IStorage $sourceStorage,
764
		$sourceInternalPath,
765
		$targetInternalPath,
766
		$preserveMtime,
767
		$isRename
768
	) {
769
		// for versions we have nothing to do, because versions should always use the
770
		// key from the original file. Just create a 1:1 copy and done
771
		if ($this->isVersion($targetInternalPath) ||
772
			$this->isVersion($sourceInternalPath)) {
773
			// remember that we try to create a version so that we can detect it during
774
			// fopen($sourceInternalPath) and by-pass the encryption in order to
775
			// create a 1:1 copy of the file
776
			$this->arrayCache->set('encryption_copy_version_' . $sourceInternalPath, true);
777
			$result = $this->storage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
778
			$this->arrayCache->remove('encryption_copy_version_' . $sourceInternalPath);
779
			if ($result) {
780
				$info = $this->getCache('', $sourceStorage)->get($sourceInternalPath);
781
				// make sure that we update the unencrypted size for the version
782
				if (isset($info['encrypted']) && $info['encrypted'] === true) {
783
					$this->updateUnencryptedSize(
784
						$this->getFullPath($targetInternalPath),
785
						$info->getUnencryptedSize()
786
					);
787
				}
788
				$this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename, true);
789
			}
790
			return $result;
791
		}
792
793
		// first copy the keys that we reuse the existing file key on the target location
794
		// and don't create a new one which would break versions for example.
795
		$mount = $this->mountManager->findByStorageId($sourceStorage->getId());
796
		if (count($mount) === 1) {
797
			$mountPoint = $mount[0]->getMountPoint();
798
			$source = $mountPoint . '/' . $sourceInternalPath;
799
			$target = $this->getFullPath($targetInternalPath);
800
			$this->copyKeys($source, $target);
801
		} else {
802
			$this->logger->error('Could not find mount point, can\'t keep encryption keys');
803
		}
804
805
		if ($sourceStorage->is_dir($sourceInternalPath)) {
806
			$dh = $sourceStorage->opendir($sourceInternalPath);
807
			if (!$this->is_dir($targetInternalPath)) {
808
				$result = $this->mkdir($targetInternalPath);
809
			} else {
810
				$result = true;
811
			}
812
			if (is_resource($dh)) {
813
				while ($result and ($file = readdir($dh)) !== false) {
814
					if (!Filesystem::isIgnoredDir($file)) {
815
						$result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file, false, $isRename);
816
					}
817
				}
818
			}
819
		} else {
820
			try {
821
				$source = $sourceStorage->fopen($sourceInternalPath, 'r');
822
				$target = $this->fopen($targetInternalPath, 'w');
823
				[, $result] = \OC_Helper::streamCopy($source, $target);
824
			} finally {
825
				if (is_resource($source)) {
826
					fclose($source);
827
				}
828
				if (is_resource($target)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $target does not seem to be defined for all execution paths leading up to this point.
Loading history...
829
					fclose($target);
830
				}
831
			}
832
			if ($result) {
833
				if ($preserveMtime) {
834
					$this->touch($targetInternalPath, $sourceStorage->filemtime($sourceInternalPath));
835
				}
836
				$this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename, false);
837
			} else {
838
				// delete partially written target file
839
				$this->unlink($targetInternalPath);
840
				// delete cache entry that was created by fopen
841
				$this->getCache()->remove($targetInternalPath);
842
			}
843
		}
844
		return (bool)$result;
845
	}
846
847
	public function getLocalFile($path) {
848
		if ($this->encryptionManager->isEnabled()) {
849
			$cachedFile = $this->getCachedFile($path);
850
			if (is_string($cachedFile)) {
851
				return $cachedFile;
852
			}
853
		}
854
		return $this->storage->getLocalFile($path);
855
	}
856
857
	public function isLocal() {
858
		if ($this->encryptionManager->isEnabled()) {
859
			return false;
860
		}
861
		return $this->storage->isLocal();
862
	}
863
864
	public function stat($path) {
865
		$stat = $this->storage->stat($path);
866
		if (!$stat) {
867
			return false;
868
		}
869
		$fileSize = $this->filesize($path);
870
		$stat['size'] = $fileSize;
871
		$stat[7] = $fileSize;
872
		$stat['hasHeader'] = $this->getHeaderSize($path) > 0;
873
		return $stat;
874
	}
875
876
	public function hash($type, $path, $raw = false) {
877
		$fh = $this->fopen($path, 'rb');
878
		$ctx = hash_init($type);
879
		hash_update_stream($ctx, $fh);
880
		fclose($fh);
881
		return hash_final($ctx, $raw);
882
	}
883
884
	/**
885
	 * return full path, including mount point
886
	 *
887
	 * @param string $path relative to mount point
888
	 * @return string full path including mount point
889
	 */
890
	protected function getFullPath($path) {
891
		return Filesystem::normalizePath($this->mountPoint . '/' . $path);
892
	}
893
894
	/**
895
	 * read first block of encrypted file, typically this will contain the
896
	 * encryption header
897
	 *
898
	 * @param string $path
899
	 * @return string
900
	 */
901
	protected function readFirstBlock($path) {
902
		$firstBlock = '';
903
		if ($this->storage->is_file($path)) {
904
			$handle = $this->storage->fopen($path, 'r');
905
			$firstBlock = fread($handle, $this->util->getHeaderSize());
906
			fclose($handle);
907
		}
908
		return $firstBlock;
909
	}
910
911
	/**
912
	 * return header size of given file
913
	 *
914
	 * @param string $path
915
	 * @return int
916
	 */
917
	protected function getHeaderSize($path) {
918
		$headerSize = 0;
919
		$realFile = $this->util->stripPartialFileExtension($path);
920
		if ($this->storage->is_file($realFile)) {
921
			$path = $realFile;
922
		}
923
		$firstBlock = $this->readFirstBlock($path);
924
925
		if (substr($firstBlock, 0, strlen(Util::HEADER_START)) === Util::HEADER_START) {
926
			$headerSize = $this->util->getHeaderSize();
927
		}
928
929
		return $headerSize;
930
	}
931
932
	/**
933
	 * parse raw header to array
934
	 *
935
	 * @param string $rawHeader
936
	 * @return array
937
	 */
938
	protected function parseRawHeader($rawHeader) {
939
		$result = [];
940
		if (substr($rawHeader, 0, strlen(Util::HEADER_START)) === Util::HEADER_START) {
941
			$header = $rawHeader;
942
			$endAt = strpos($header, Util::HEADER_END);
943
			if ($endAt !== false) {
944
				$header = substr($header, 0, $endAt + strlen(Util::HEADER_END));
945
946
				// +1 to not start with an ':' which would result in empty element at the beginning
947
				$exploded = explode(':', substr($header, strlen(Util::HEADER_START) + 1));
948
949
				$element = array_shift($exploded);
950
				while ($element !== Util::HEADER_END) {
951
					$result[$element] = array_shift($exploded);
952
					$element = array_shift($exploded);
953
				}
954
			}
955
		}
956
957
		return $result;
958
	}
959
960
	/**
961
	 * read header from file
962
	 *
963
	 * @param string $path
964
	 * @return array
965
	 */
966
	protected function getHeader($path) {
967
		$realFile = $this->util->stripPartialFileExtension($path);
968
		$exists = $this->storage->is_file($realFile);
969
		if ($exists) {
970
			$path = $realFile;
971
		}
972
973
		$result = [];
974
975
		$isEncrypted = $this->encryptedPaths->get($realFile);
976
		if (is_null($isEncrypted)) {
977
			$info = $this->getCache()->get($path);
978
			$isEncrypted = isset($info['encrypted']) && $info['encrypted'] === true;
979
		}
980
981
		if ($isEncrypted) {
982
			$firstBlock = $this->readFirstBlock($path);
983
			$result = $this->parseRawHeader($firstBlock);
984
985
			// if the header doesn't contain a encryption module we check if it is a
986
			// legacy file. If true, we add the default encryption module
987
			if (!isset($result[Util::HEADER_ENCRYPTION_MODULE_KEY]) && (!empty($result) || $exists)) {
988
				$result[Util::HEADER_ENCRYPTION_MODULE_KEY] = 'OC_DEFAULT_MODULE';
989
			}
990
		}
991
992
		return $result;
993
	}
994
995
	/**
996
	 * read encryption module needed to read/write the file located at $path
997
	 *
998
	 * @param string $path
999
	 * @return null|\OCP\Encryption\IEncryptionModule
1000
	 * @throws ModuleDoesNotExistsException
1001
	 * @throws \Exception
1002
	 */
1003
	protected function getEncryptionModule($path) {
1004
		$encryptionModule = null;
1005
		$header = $this->getHeader($path);
1006
		$encryptionModuleId = $this->util->getEncryptionModuleId($header);
1007
		if (!empty($encryptionModuleId)) {
1008
			try {
1009
				$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
1010
			} catch (ModuleDoesNotExistsException $e) {
1011
				$this->logger->critical('Encryption module defined in "' . $path . '" not loaded!');
1012
				throw $e;
1013
			}
1014
		}
1015
1016
		return $encryptionModule;
1017
	}
1018
1019
	/**
1020
	 * @param string $path
1021
	 * @param int $unencryptedSize
1022
	 */
1023
	public function updateUnencryptedSize($path, $unencryptedSize) {
1024
		$this->unencryptedSize[$path] = $unencryptedSize;
1025
	}
1026
1027
	/**
1028
	 * copy keys to new location
1029
	 *
1030
	 * @param string $source path relative to data/
1031
	 * @param string $target path relative to data/
1032
	 * @return bool
1033
	 */
1034
	protected function copyKeys($source, $target) {
1035
		if (!$this->util->isExcluded($source)) {
1036
			return $this->keyStorage->copyKeys($source, $target);
1037
		}
1038
1039
		return false;
1040
	}
1041
1042
	/**
1043
	 * check if path points to a files version
1044
	 *
1045
	 * @param $path
1046
	 * @return bool
1047
	 */
1048
	protected function isVersion($path) {
1049
		$normalized = Filesystem::normalizePath($path);
1050
		return substr($normalized, 0, strlen('/files_versions/')) === '/files_versions/';
1051
	}
1052
1053
	/**
1054
	 * check if the given storage should be encrypted or not
1055
	 *
1056
	 * @param $path
1057
	 * @return bool
1058
	 */
1059
	protected function shouldEncrypt($path) {
1060
		$fullPath = $this->getFullPath($path);
1061
		$mountPointConfig = $this->mount->getOption('encrypt', true);
1062
		if ($mountPointConfig === false) {
1063
			return false;
1064
		}
1065
1066
		try {
1067
			$encryptionModule = $this->getEncryptionModule($fullPath);
1068
		} catch (ModuleDoesNotExistsException $e) {
1069
			return false;
1070
		}
1071
1072
		if ($encryptionModule === null) {
1073
			$encryptionModule = $this->encryptionManager->getEncryptionModule();
1074
		}
1075
1076
		return $encryptionModule->shouldEncrypt($fullPath);
1077
	}
1078
1079
	public function writeStream(string $path, $stream, int $size = null): int {
1080
		// always fall back to fopen
1081
		$target = $this->fopen($path, 'w');
1082
		[$count, $result] = \OC_Helper::streamCopy($stream, $target);
1083
		fclose($stream);
1084
		fclose($target);
1085
1086
		// object store, stores the size after write and doesn't update this during scan
1087
		// manually store the unencrypted size
1088
		if ($result && $this->getWrapperStorage()->instanceOfStorage(ObjectStoreStorage::class)) {
1089
			$this->getCache()->put($path, ['unencrypted_size' => $count]);
1090
		}
1091
1092
		return $count;
1093
	}
1094
1095
	public function clearIsEncryptedCache(): void {
1096
		$this->encryptedPaths->clear();
1097
	}
1098
}
1099