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

Encryption::filesize()   B

Complexity

Conditions 8
Paths 9

Size

Total Lines 34
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 8
eloc 20
c 1
b 1
f 0
nc 9
nop 1
dl 0
loc 34
rs 8.4444
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