Passed
Push — master ( 7d3375...7251ed )
by Robin
15:19 queued 13s
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
	 * @param string $path
138
	 * @return int
139
	 */
140
	public function filesize($path) {
141
		$fullPath = $this->getFullPath($path);
142
143
		/** @var CacheEntry $info */
144
		$info = $this->getCache()->get($path);
145
		if (isset($this->unencryptedSize[$fullPath])) {
146
			$size = $this->unencryptedSize[$fullPath];
147
			// update file cache
148
			if ($info instanceof ICacheEntry) {
0 ignored issues
show
introduced by
$info is always a sub-type of OCP\Files\Cache\ICacheEntry.
Loading history...
149
				$info['encrypted'] = $info['encryptedVersion'];
150
			} else {
151
				if (!is_array($info)) {
152
					$info = [];
153
				}
154
				$info['encrypted'] = true;
155
				$info = new CacheEntry($info);
156
			}
157
158
			if ($size !== $info->getUnencryptedSize()) {
159
				$this->getCache()->update($info->getId(), [
160
					'unencrypted_size' => $size
161
				]);
162
			}
163
164
			return $size;
165
		}
166
167
		if (isset($info['fileid']) && $info['encrypted']) {
168
			return $this->verifyUnencryptedSize($path, $info->getUnencryptedSize());
169
		}
170
171
		return $this->storage->filesize($path);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->storage->filesize($path) also could return the type boolean which is incompatible with the documented return type integer.
Loading history...
172
	}
173
174
	/**
175
	 * @param string $path
176
	 * @param array $data
177
	 * @return array
178
	 */
179
	private function modifyMetaData(string $path, array $data): array {
180
		$fullPath = $this->getFullPath($path);
181
		$info = $this->getCache()->get($path);
182
183
		if (isset($this->unencryptedSize[$fullPath])) {
184
			$data['encrypted'] = true;
185
			$data['size'] = $this->unencryptedSize[$fullPath];
186
		} else {
187
			if (isset($info['fileid']) && $info['encrypted']) {
188
				$data['size'] = $this->verifyUnencryptedSize($path, $info->getUnencryptedSize());
189
				$data['encrypted'] = true;
190
			}
191
		}
192
193
		if (isset($info['encryptedVersion']) && $info['encryptedVersion'] > 1) {
194
			$data['encryptedVersion'] = $info['encryptedVersion'];
195
		}
196
197
		return $data;
198
	}
199
200
	public function getMetaData($path) {
201
		$data = $this->storage->getMetaData($path);
202
		if (is_null($data)) {
203
			return null;
204
		}
205
		return $this->modifyMetaData($path, $data);
206
	}
207
208
	public function getDirectoryContent($directory): \Traversable {
209
		$parent = rtrim($directory, '/');
210
		foreach ($this->getWrapperStorage()->getDirectoryContent($directory) as $data) {
211
			yield $this->modifyMetaData($parent . '/' . $data['name'], $data);
212
		}
213
	}
214
215
	/**
216
	 * see https://www.php.net/manual/en/function.file_get_contents.php
217
	 *
218
	 * @param string $path
219
	 * @return string
220
	 */
221
	public function file_get_contents($path) {
222
		$encryptionModule = $this->getEncryptionModule($path);
223
224
		if ($encryptionModule) {
225
			$handle = $this->fopen($path, "r");
226
			if (!$handle) {
227
				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...
228
			}
229
			$data = stream_get_contents($handle);
230
			fclose($handle);
231
			return $data;
232
		}
233
		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...
234
	}
235
236
	/**
237
	 * see https://www.php.net/manual/en/function.file_put_contents.php
238
	 *
239
	 * @param string $path
240
	 * @param mixed $data
241
	 * @return int|false
242
	 */
243
	public function file_put_contents($path, $data) {
244
		// file put content will always be translated to a stream write
245
		$handle = $this->fopen($path, 'w');
246
		if (is_resource($handle)) {
247
			$written = fwrite($handle, $data);
248
			fclose($handle);
249
			return $written;
250
		}
251
252
		return false;
253
	}
254
255
	/**
256
	 * see https://www.php.net/manual/en/function.unlink.php
257
	 *
258
	 * @param string $path
259
	 * @return bool
260
	 */
261
	public function unlink($path) {
262
		$fullPath = $this->getFullPath($path);
263
		if ($this->util->isExcluded($fullPath)) {
264
			return $this->storage->unlink($path);
265
		}
266
267
		$encryptionModule = $this->getEncryptionModule($path);
268
		if ($encryptionModule) {
269
			$this->keyStorage->deleteAllFileKeys($fullPath);
270
		}
271
272
		return $this->storage->unlink($path);
273
	}
274
275
	/**
276
	 * see https://www.php.net/manual/en/function.rename.php
277
	 *
278
	 * @param string $source
279
	 * @param string $target
280
	 * @return bool
281
	 */
282
	public function rename($source, $target) {
283
		$result = $this->storage->rename($source, $target);
284
285
		if ($result &&
286
			// versions always use the keys from the original file, so we can skip
287
			// this step for versions
288
			$this->isVersion($target) === false &&
289
			$this->encryptionManager->isEnabled()) {
290
			$sourcePath = $this->getFullPath($source);
291
			if (!$this->util->isExcluded($sourcePath)) {
292
				$targetPath = $this->getFullPath($target);
293
				if (isset($this->unencryptedSize[$sourcePath])) {
294
					$this->unencryptedSize[$targetPath] = $this->unencryptedSize[$sourcePath];
295
				}
296
				$this->keyStorage->renameKeys($sourcePath, $targetPath);
297
				$module = $this->getEncryptionModule($target);
298
				if ($module) {
299
					$module->update($targetPath, $this->uid, []);
300
				}
301
			}
302
		}
303
304
		return $result;
305
	}
306
307
	/**
308
	 * see https://www.php.net/manual/en/function.rmdir.php
309
	 *
310
	 * @param string $path
311
	 * @return bool
312
	 */
313
	public function rmdir($path) {
314
		$result = $this->storage->rmdir($path);
315
		$fullPath = $this->getFullPath($path);
316
		if ($result &&
317
			$this->util->isExcluded($fullPath) === false &&
318
			$this->encryptionManager->isEnabled()
319
		) {
320
			$this->keyStorage->deleteAllFileKeys($fullPath);
321
		}
322
323
		return $result;
324
	}
325
326
	/**
327
	 * check if a file can be read
328
	 *
329
	 * @param string $path
330
	 * @return bool
331
	 */
332
	public function isReadable($path) {
333
		$isReadable = true;
334
335
		$metaData = $this->getMetaData($path);
336
		if (
337
			!$this->is_dir($path) &&
338
			isset($metaData['encrypted']) &&
339
			$metaData['encrypted'] === true
340
		) {
341
			$fullPath = $this->getFullPath($path);
342
			$module = $this->getEncryptionModule($path);
343
			$isReadable = $module->isReadable($fullPath, $this->uid);
344
		}
345
346
		return $this->storage->isReadable($path) && $isReadable;
347
	}
348
349
	/**
350
	 * see https://www.php.net/manual/en/function.copy.php
351
	 *
352
	 * @param string $source
353
	 * @param string $target
354
	 */
355
	public function copy($source, $target): bool {
356
		$sourcePath = $this->getFullPath($source);
357
358
		if ($this->util->isExcluded($sourcePath)) {
359
			return $this->storage->copy($source, $target);
360
		}
361
362
		// need to stream copy file by file in case we copy between a encrypted
363
		// and a unencrypted storage
364
		$this->unlink($target);
365
		return $this->copyFromStorage($this, $source, $target);
366
	}
367
368
	/**
369
	 * see https://www.php.net/manual/en/function.fopen.php
370
	 *
371
	 * @param string $path
372
	 * @param string $mode
373
	 * @return resource|bool
374
	 * @throws GenericEncryptionException
375
	 * @throws ModuleDoesNotExistsException
376
	 */
377
	public function fopen($path, $mode) {
378
		// check if the file is stored in the array cache, this means that we
379
		// copy a file over to the versions folder, in this case we don't want to
380
		// decrypt it
381
		if ($this->arrayCache->hasKey('encryption_copy_version_' . $path)) {
382
			$this->arrayCache->remove('encryption_copy_version_' . $path);
383
			return $this->storage->fopen($path, $mode);
384
		}
385
386
		$encryptionEnabled = $this->encryptionManager->isEnabled();
387
		$shouldEncrypt = false;
388
		$encryptionModule = null;
389
		$header = $this->getHeader($path);
390
		$signed = isset($header['signed']) && $header['signed'] === 'true';
391
		$fullPath = $this->getFullPath($path);
392
		$encryptionModuleId = $this->util->getEncryptionModuleId($header);
393
394
		if ($this->util->isExcluded($fullPath) === false) {
395
			$size = $unencryptedSize = 0;
396
			$realFile = $this->util->stripPartialFileExtension($path);
397
			$targetExists = $this->is_file($realFile) || $this->file_exists($path);
398
			$targetIsEncrypted = false;
399
			if ($targetExists) {
400
				// in case the file exists we require the explicit module as
401
				// specified in the file header - otherwise we need to fail hard to
402
				// prevent data loss on client side
403
				if (!empty($encryptionModuleId)) {
404
					$targetIsEncrypted = true;
405
					$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
406
				}
407
408
				if ($this->file_exists($path)) {
409
					$size = $this->storage->filesize($path);
410
					$unencryptedSize = $this->filesize($path);
411
				} else {
412
					$size = $unencryptedSize = 0;
413
				}
414
			}
415
416
			try {
417
				if (
418
					$mode === 'w'
419
					|| $mode === 'w+'
420
					|| $mode === 'wb'
421
					|| $mode === 'wb+'
422
				) {
423
					// if we update a encrypted file with a un-encrypted one we change the db flag
424
					if ($targetIsEncrypted && $encryptionEnabled === false) {
425
						$cache = $this->storage->getCache();
426
						if ($cache) {
0 ignored issues
show
introduced by
$cache is of type OC\Files\Cache\Cache, thus it always evaluated to true.
Loading history...
427
							$entry = $cache->get($path);
428
							$cache->update($entry->getId(), ['encrypted' => 0]);
429
						}
430
					}
431
					if ($encryptionEnabled) {
432
						// if $encryptionModuleId is empty, the default module will be used
433
						$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
434
						$shouldEncrypt = $encryptionModule->shouldEncrypt($fullPath);
435
						$signed = true;
436
					}
437
				} else {
438
					$info = $this->getCache()->get($path);
439
					// only get encryption module if we found one in the header
440
					// or if file should be encrypted according to the file cache
441
					if (!empty($encryptionModuleId)) {
442
						$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
443
						$shouldEncrypt = true;
444
					} elseif (empty($encryptionModuleId) && $info['encrypted'] === true) {
445
						// we come from a old installation. No header and/or no module defined
446
						// but the file is encrypted. In this case we need to use the
447
						// OC_DEFAULT_MODULE to read the file
448
						$encryptionModule = $this->encryptionManager->getEncryptionModule('OC_DEFAULT_MODULE');
449
						$shouldEncrypt = true;
450
						$targetIsEncrypted = true;
451
					}
452
				}
453
			} catch (ModuleDoesNotExistsException $e) {
454
				$this->logger->warning('Encryption module "' . $encryptionModuleId . '" not found, file will be stored unencrypted', [
455
					'exception' => $e,
456
					'app' => 'core',
457
				]);
458
			}
459
460
			// encryption disabled on write of new file and write to existing unencrypted file -> don't encrypt
461
			if (!$encryptionEnabled || !$this->shouldEncrypt($path)) {
462
				if (!$targetExists || !$targetIsEncrypted) {
463
					$shouldEncrypt = false;
464
				}
465
			}
466
467
			if ($shouldEncrypt === true && $encryptionModule !== null) {
468
				$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

468
				$this->encryptedPaths->set($this->util->stripPartialFileExtension($path), /** @scrutinizer ignore-type */ true);
Loading history...
469
				$headerSize = $this->getHeaderSize($path);
470
				$source = $this->storage->fopen($path, $mode);
471
				if (!is_resource($source)) {
472
					return false;
473
				}
474
				$handle = \OC\Files\Stream\Encryption::wrap($source, $path, $fullPath, $header,
475
					$this->uid, $encryptionModule, $this->storage, $this, $this->util, $this->fileHelper, $mode,
476
					$size, $unencryptedSize, $headerSize, $signed);
477
478
				return $handle;
479
			}
480
		}
481
482
		return $this->storage->fopen($path, $mode);
483
	}
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(string $path, int $unencryptedSize): int {
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);
508
				} catch (\Exception $e) {
509
					$this->logger->error('Couldn\'t re-calculate unencrypted size for ' . $path, ['exception' => $e]);
510
				}
511
				unset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]);
512
			}
513
		}
514
515
		return $result;
516
	}
517
518
	/**
519
	 * calculate the unencrypted size
520
	 *
521
	 * @param string $path internal path relative to the storage root
522
	 * @param int $size size of the physical file
523
	 * @param int $unencryptedSize size of the unencrypted file
524
	 *
525
	 * @return int calculated unencrypted size
526
	 */
527
	protected function fixUnencryptedSize(string $path, int $size, int $unencryptedSize): int {
528
		$headerSize = $this->getHeaderSize($path);
529
		$header = $this->getHeader($path);
530
		$encryptionModule = $this->getEncryptionModule($path);
531
532
		$stream = $this->storage->fopen($path, 'r');
533
534
		// if we couldn't open the file we return the old unencrypted size
535
		if (!is_resource($stream)) {
536
			$this->logger->error('Could not open ' . $path . '. Recalculation of unencrypted size aborted.');
537
			return $unencryptedSize;
538
		}
539
540
		$newUnencryptedSize = 0;
541
		$size -= $headerSize;
542
		$blockSize = $this->util->getBlockSize();
543
544
		// if a header exists we skip it
545
		if ($headerSize > 0) {
546
			$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

546
			$this->fread_block(/** @scrutinizer ignore-type */ $stream, $headerSize);
Loading history...
547
		}
548
549
		// fast path, else the calculation for $lastChunkNr is bogus
550
		if ($size === 0) {
551
			return 0;
552
		}
553
554
		$signed = isset($header['signed']) && $header['signed'] === 'true';
555
		$unencryptedBlockSize = $encryptionModule->getUnencryptedBlockSize($signed);
556
557
		// calculate last chunk nr
558
		// next highest is end of chunks, one subtracted is last one
559
		// we have to read the last chunk, we can't just calculate it (because of padding etc)
560
561
		$lastChunkNr = ceil($size / $blockSize) - 1;
562
		// calculate last chunk position
563
		$lastChunkPos = ($lastChunkNr * $blockSize);
564
		// try to fseek to the last chunk, if it fails we have to read the whole file
565
		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

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

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